WinForms版本OLAP快速入门 > 大数据源 |
到目前为止,所有的例子都在讨论如何加载所有的数据到内存中。这是一个简单而且合适的方式,它在很多情况下都能够很好的工作。
在部分情况下,然而,这里可能有太多的数据以至于无法一次性加载到内存中。设想一下如果一个表中包含一百万甚至更多的行。即使你能够将所有的数据加载到内存中,也将会耗费大量的时间。
你有很多种方法来处理这一问题。你可以创建查询来汇总数据并在服务端缓存这些数据,或者可以使用专业OLAP数据提供者。无论哪种方式,你最终都将获得可以在C1Olap中使用的表。
但是这里仍然有更简单的方法。假设数据库中包含了数千家公司的信息,但是用户每次仅需要看到一小部分。除了在客户端中依赖C1Olap的数据过滤能力外,你还可以将一部分工作委派给服务端,只加载用户想要看到的公司信息。这个很容易实现,并且不需要任何特殊的软件或者服务端配置。
例如,阅读并思考下面的CachedDataTable类(该类在C1Olap安装的”SqlFilter”例子中):
/// <summary>
/// Extends the <see cref="DataTable"/> class to load and cache
/// data on demand using a <see cref="Fill"/> method that takes
/// a set of keys as a parameter.
/// </summary>
class CachedDataTable : DataTable
{
public string ConnectionString { get; set; }
public string SqlTemplate { get; set; }
public string WhereClauseTemplate { get; set; }
Dictionary<object, bool> _values =
new Dictionary<object, bool>();
// constructor
public CachedDataTable(string sqlTemplate,
string whereClauseTemplate, string connString)
{
ConnectionString = connString;
SqlTemplate = sqlTemplate;
WhereClauseTemplate = whereClauseTemplate;
}
// populate the table by adding any missing values
public void Fill(IEnumerable filterValues, bool reset)
{
// reset table if requested
if (reset)
{
_values.Clear();
Rows.Clear();
}
// get a list with the new values
List<object> newValues = GetNewValues(filterValues);
if (newValues.Count > 0)
{
// get sql statement and data adapter
var sql = GetSqlStatement(newValues);
using (var da = new OleDbDataAdapter(sql, ConnectionString))
{
// add new values to the table
int rows = da.Fill(this);
}
}
}
public void Fill(IEnumerable filterValues)
{
Fill(filterValues, false);
}
该类继承了常规DataTable类,然后提供一个Fill方法用于完全重新注入表数据,或者通过列表的形式将值添加到新的记录。例如,你可以先用两个客户数据(从几千名之中选出)填充表,然后仅在用户需要的时候再添加更多的信息。
需要注意的是上述代码使用了OleDbDataAdapter。这是因为本示例中使用了一个MDB文件作为数据源,还使用了OleDb-style连接字符串。想要在sql server数据源中使用这个类,你需要将OleDbDataAdapter 替换成 SqlDataAdapter.
上方的代码缺少的两个简单方法实现过程如下所示:
// gets a list with the filter values that are not already in the
// current values collection;
// and add them all to the current values collection.
List<object> GetNewValues(IEnumerable filterValues)
{
var list = new List<object>();
foreach (object value in filterValues)
{
if (!_values.ContainsKey(value))
{
list.Add(value);
_values[value] = true;
}
}
return list;
}
// gets a sql statement to add new values to the table
string GetSqlStatement(List<object> newValues)
{
return string.Format(SqlTemplate, GetWhereClause(newValues));
}
string GetWhereClause(List<object> newValues)
{
if (newValues.Count == 0 || string.IsNullOrEmpty(WhereClauseTemplate))
{
return string.Empty;
}
// build list of values
StringBuilder sb = new StringBuilder();
foreach (object value in newValues)
{
if (sb.Length > 0) sb.Append(", ");
if (value is string)
{
sb.AppendFormat("'{0}'", ((string)value).Replace("'", "''"));
}
else
{
sb.Append(value);
}
}
// build where clause
return string.Format(WhereClauseTemplate, sb);
}
}
GetNewValues方法将以列表的形式返回用户所需且并不存在于当前DataTable中的值。这些值将被添加到表中。
GetSqlStatement方法创建一个新的SQL声明,使用WHERE子句加载用户所需而又没有加载的记录。它使用构造函数调用者提供的字符串模板,这样显得这个类更符合常规。
现在CachedDataTable已经准备就绪,下一步就是使用C1Olap连接它,让用户可以清晰的分析这些数据,就好像它们已经加载到内存中一样。
想要实现这一功能,打开主表单,添加一个 C1OlapPage控件,然后将以下代码添加到表单中:
public partial class Form1 : Form
{
List<string> _customerList;
List<string> _activeCustomerList;
const int MAX_CUSTOMERS = 12;
这些字段将包含一个数据库内所有客户的完整列表,一个用户选中客户列表以及用户一次可选中客户的最大数量。将一次选中客户数量设定一个相对小点的值,从而让用户不能一次性加载太多的数据到应用中。
接下来,我们需要从数据库中获取一个所有客户的列表,让用户选择那些他希望看到的。需要注意的是。这是一个很长但又很紧凑的列表。它只包含客户名称,并没有其他相关细节,如订单,订单详情等等。下面的代码将实现加载所有客户列表功能:
public Form1()
{
InitializeComponent();
// get complete list of customers
_customerList = new List<string>();
var sql = @"SELECT DISTINCT Customers.CompanyName" +
"AS [Customer] FROM Customers";
var da = new OleDbDataAdapter(sql, GetConnectionString());
var dt = new DataTable();
da.Fill(dt);
foreach (DataRow dr in dt.Rows)
{
_customerList.Add((string)dr["Customer"]);
}
下一步,我们需要一个用户希望看到的客户列表。我们将这个列表持久化成一个属性设置,因此它可以保存多个会话。这一设置命名为”Customers”,是一个”StringCollection”类型数据。你可以通过右键单击解决方案浏览器中的项目节点,选择”Properties”,然后选择”Settings”标签来创建它:
下面的代码将通过新的设置实现加载活跃客户列表:
// get active customer list
_activeCustomerList = new List<string>();
foreach (string customer in Settings.Default.Customers)
{
_activeCustomerList.Add(customer);
}
现在我们已经创建了一个CachedDataTable,并且将其赋值给DataSource属性:
// get data into the CachedDataTable
var dtSales = new CachedDataTable(
Resources.SqlTemplate,
Resources.WhereTemplate,
GetConnectionString());
dtSales.Fill(_activeCustomerList);
// assign data to C1OlapPage control
_c1OlapPage.DataSource = dtSales;
// show default view
var olap = _c1OlapPage.OlapEngine;
olap.BeginUpdate();
olap.RowFields.Add("Customer");
olap.ColumnFields.Add("Category");
olap.ValueFields.Add("Sales");
olap.EndUpdate();
CachedDataTable构造函数使用三个参数:
现在,数据源已经就绪,我们需要将其连接到C1Olap上来确保:
想要完成步骤1,我们需要将完整客户列表赋值给C1OlapField.Values属性。这个属性包含一个以列表形式显示在过滤器中的客户信息,C1Olap将这些列表值放入到行数据中。在本例中,行数据仅包含一个局部列表,所以我们需要提供完整版本进行替代。
想要完成步骤2,我们需要监听PropertyChanged事件,当用户修改任何字段属性,包括过滤器时将会触发这一事件。当触发后,我们将检索用户选择的客户列表,将列表转移到数据源中。
下述代码将实现该功能:
// custom filter: customers in the list, customers currently active
var field = olap.Fields["Customer"];
var filter = field.Filter;
filter.Values = _customerList;
filter.ShowValues = _activeCustomerList.ToArray();
filter.PropertyChanged += filter_PropertyChanged;
下面是当过滤器改变时更新数据源的事件句柄:
// re-query database when list of selected customers changes
void filter_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// get reference to parent filter
var filter = sender as C1.Olap.C1OlapFilter;
// get list of values accepted by the filter
_activeCustomerList.Clear();
foreach (string customer in _customerList)
{
if (filter.Apply(customer))
{
_activeCustomerList.Add(customer);
}
}
// skip if no values were selected
if (_activeCustomerList.Count == 0)
{
MessageBox.Show(
"No customers selected, change will not be applied.",
"No Customers");
return;
}
// trim list if necessary
if (_activeCustomerList.Count > MAX_CUSTOMERS)
{
MessageBox.Show(
"Too many customers selected, list will be trimmed.",
"Too Many Customers");
_activeCustomerList.RemoveRange(MAX_CUSTOMERS,
_activeCustomerList.Count - MAX_CUSTOMERS);
}
// get new data
var dt = _c1OlapPage.DataSource as CachedDataTable;
dt.Fill(_activeCustomerList);
}
代码首先检索了字段的过滤器,然后调用字段的Apply方法来创建一个用户选择的客户列表。执行完一些绑定检查后,列表将转移到CachedDataTable表中,在这里将检索是否存在数据丢失。新数据加载后,C1OlapPage将得到通知,并自动刷新视图。
在运行程序之前,完成最后一步。如果字段在视图中处于激活状态,C1OlapEngine将只考虑该字段的Filter属性。这里激活是指字段属于RowFields, ColumnFields, ValueFields 或者 FilterFields集合中的一员。在本例中,”Customers”字段有一个特殊的过滤器,应该一直处于激活状态。为了确保这一点,我们必须调用引擎的Updating事件,确保”Customers”字段一直处于激活状态。
确保”Customers”字段一直处于激活状态的代码如下所示:
public Form1()
{
InitializeComponent();
// ** no changes here **
// make sure Customer field is always in the view
// (since it is always used at least as a filter)
_c1OlapPage.Updating += _c1OlapPage_Updating;
}
// make sure Customer field is always in the view
// (since it is always used at least as a filter)
void _c1OlapPage_Updating(object sender, EventArgs e)
{
var olap = _c1OlapPage.OlapEngine;
var field = olap.Fields["Customer"];
if (!field.IsActive)
{
olap.FilterFields.Add(field);
}
}
如果你现在运行应用,你会注意到只有包含在”Customers”设置中的客户信息才能显示在视图中:
上图和之前看到的类似,不同之处在于这一次的过滤由服务端实现。大多数的客户数据并没有加载到应用中。
想要看到其他客户信息,右键单击”Customers”字段,选择”Field Settings”选项。然后编辑过滤器选择指定客户或者定义一个显示条件。效果如下图所示:
当你单击OK按钮后,应用将检测到改动,然后会从CachingDataTable对象中请求附加数据。一旦新数据加载进来后,C1Olap将自动检测到改动,然后自动更新OLAP表。