DataLoaders - 统一数据到对象绑定






4.72/5 (27投票s)
一个能将对象与其数据源完全分离的框架——允许在代码或设计中无需预先考虑即可使用任何数据源。数据库、文本文件、Web 服务以及几乎任何其他来源都可以透明地使用或切换。
数据加载器框架概述
目录
想象一下这个场景
你的老板要求你编写一个新系统,该系统包含一个桌面应用程序,需要通过 Web 服务与一个 Web 应用程序通信,并且所有这些都使用位于乌拉圭公司内部网络上的数据库。这个系统具体做什么并不重要,但我们假设它是一个订单跟踪系统,涵盖从客户在线下单到仓库履行的全过程。你的客户卖的是书。
你的老板希望这个系统在周日晚上之前运行起来,因为你的大客户将在周一早上9点验收。如果他验收不通过,你这个月就拿不到薪水了。
虽然我很乐意以每小时600美元的价格来提供咨询服务……但这可能没有必要。仔细审视这个系统,并将其分解为你认为最大的开发需求。我的清单会是这样:
- 设计数据需求。
- 定义业务逻辑。
- 创建数据库。
- 实现数据访问层。
- 开发 Web 服务、网站和桌面应用程序。
- 预订一个长假。
你的老板刚刚给你发了一份备忘录——距离客户到场还有三十分钟:他们将运行 MySQL 数据库,而不是之前以为的 SQL Server。这不是问题,对吧?你肯定没有使用 SqlConnection
和 T-SQL 特定的查询吧?哦,你用了?我也用了。
退一步思考
现在,想象一下,如果你可以完全忽略数据,直接与真实的对象打交道。不再是 SELECT Field FROM BigTable WHERE Id=1
,而是 new BigObject(1)
。这就是数据加载器(data loader)的用武之地。
广义上说,数据加载器将数据加载到数据项(data item)中。数据项是任何包含了其数据需求定义的对象,通常也包含其所有逻辑。数据项不关心它们从哪里获取数据;数据源可以是 Microsoft Access 数据库、SQL Server,或者是软盘上的一个加密文本文件。数据加载器获取数据项的定义和数据源的路径,并将它们映射在一起。
数据加载器是双向工作的——即从数据项加载和卸载数据。数据项与其数据源没有任何连接,事实上,它们甚至从不知道数据源的存在。你甚至不需要自己设置数据源——数据加载器会负责创建你的数据库结构,确保它们对你的数据需求是有效的。
现在再读一遍,因为这非常重要。
遗憾的是,这几乎好得令人难以置信,所以需要做出一些妥协。如果你遵循指导方针,你将失去一些编写数据库感知应用程序时的功能,比如无法使用存储过程或特定于DBMS的SQL函数;在某些高流量的情况下,如果你不非常小心,还会损失性能。(这对于非数据库数据加载器来说不太可能构成问题。)数据加载器被要求暴露成员,允许你在实现不需要时打破“数据源透明性”规则,但除非必要,否则不推荐这样做,因为你将无法使用其“一次编写,随处使用”的能力。
如前所述,数据加载器可以从任何能够提供数据的地方获取数据。本文附带的源代码包含了针对优化的 SQL Server、MySQL 和 Microsoft Access 数据加载器的实现,但通过 SOAP、使用 XML 文件或使用定制数据源的数据加载器都是可行的。
如果你遵循规则,那么对于快速创建基于数据的对象而又不局限于特定数据源,这几乎是无与伦比的。
跳出油锅
回到我们的示例系统。有了数据加载器,你可以大幅修改你的开发需求:
- 设计数据需求。
- 定义业务逻辑。
- 开发 Web 服务、网站和桌面应用程序。
- 周末休息去吧。
需要注意的是,从你的需求中移除多余的步骤并不会显著增加其他步骤的开发时间——它只是稍微扩大了它们的范围。一旦你创建了几个数据项,你甚至都不会注意到这一点。
又入火坑
让我们看一个非常基础的数据项。这是一个关于书的数据项。我把它分成了几个部分来解释其工作原理。首先,我们定义一个新对象并实现 IDataItem
接口。
public class Book : Bttlxe.Data.IDataItem
{
/// <summary>
/// The book id.
/// </summary>
protected int m_nId = -1;
/// <summary>
/// The book title.
/// </summary>
protected string m_strTitle = "New Book";
/// <summary>
/// The book published date.
/// </summary>
protected DateTime m_dtPublished = DateTime.Now;
/// <summary>
/// The book id.
/// </summary>
public int ID
{
get
{
return m_nId;
}
set
{
m_nId = value;
}
}
/// <summary>
/// The book title.
/// </summary>
public string Title
{
get
{
return m_strTitle;
}
set
{
m_strTitle = value;
}
}
/// <summary>
/// The book published date.
/// </summary>
public DateTime Published
{
get
{
return m_dtPublished;
}
set
{
m_dtPublished = value;
}
}
到目前为止,我们只是建立了一个暴露其数据的标准对象。现在,我们将实现 IDataItem
。在阅读 Data
属性之前,先阅读 Schema
属性的定义可能会有所帮助。
#region IDataItem Members
/// <summary>
/// The data table.
/// </summary>
public virtual DataSet Data
{
get
{
// create the data table
DataTable dt = Schema.Tables["Book"].Clone();
DataRow oRow = dt.NewRow();
oRow["ID"] = m_nId;
oRow["Title"] = m_strTitle;
oRow["Published"] = m_dtPublished;
dt.Rows.Add(oRow);
DataSet ds = new DataSet("Book");
ds.Tables.Add(dt);
return ds;
}
set
{
m_nId = (int)value.Tables["Book"].Rows[0]["ID"];
m_strTitle = (string)value.Tables["Book"].Rows[0]["Title"];
m_dtPublished =
(DateTime)value.Tables["Book"].Rows[0]["Published"];
}
}
/// <summary>
/// A data set containing the schema for this object.
/// </summary>
public virtual DataSet Schema
{
get
{
DataTable dtSchema = new DataTable("Book");
dtSchema.Columns.Add("ID", System.Type.GetType("System.Int32"));
dtSchema.Columns.Add("Title",
System.Type.GetType("System.String"));
dtSchema.Columns.Add("Published",
System.Type.GetType("System.DateTime"));
dtSchema.Columns["ID"].AllowDBNull = false;
dtSchema.Columns["ID"].AutoIncrement = true;
dtSchema.Columns["ID"].AutoIncrementSeed = 1;
dtSchema.Columns["ID"].AutoIncrementStep = 1;
dtSchema.PrimaryKey = new DataColumn[]{dtSchema.Columns["ID"]};
dtSchema.Columns["Title"].AllowDBNull = false;
dtSchema.Columns["Title"].DefaultValue = m_strTitle;
dtSchema.Columns["Published"].AllowDBNull = false;
dtSchema.Columns["Published"].DefaultValue = m_dtPublished;
DataSet ds = new DataSet("Book");
ds.Tables.Add(dtSchema.Copy());
return ds;
}
}
希望上面的代码容易理解。对于描述数据项所用数据的 schema(模式),我们正在创建一个所需格式的数据表,并以标准方式添加列来表示我们正在定义的 Book
对象的属性。主键非常重要,因为它们将被单独用于识别需要唯一标识的操作(如更新)中的数据项。
注意:我们将“ID
”属性设置为在数据源上自增。
Schema
属性将这个 schema 包装到一个普通的 DataSet
对象中。(IDataItem
接口要求使用 DataSet
的原因是,这样数据项可以暴露多个 DataTable
,后面会讨论。)
Data
属性既以 schema 格式暴露数据(供数据加载器使用),又将匹配格式的数据读入对象的属性中(供你的对象使用)。
/// <summary>
/// The condition clause.
/// </summary>
/// <value>
/// An SQL condition clause.
/// </value>
public virtual string Condition
{
get
{
return string.Empty;
}
set
{
}
}
/// <summary>
/// The sort string.
/// </summary>
/// <value>
/// An SQL ORDER BY string.
/// </value>
public virtual string Sort
{
get
{
return string.Empty;
}
set
{
}
}
#endregion
Condition
和 Sort
属性对于代表单个项的对象是多余的。如果这个数据项代表一个书籍列表,你会使用标准的 SQL 条件和 order-by
子句来实现这些属性(如果数据源不是数据库,则使用 XPath 或其他查询语言)。
/// <summary>
/// Create a Book object.
/// </summary>
public Book()
{
}
/// <summary>
/// Create and load a Book object.
/// </summary>
/// <param name="nId">The Book id.</param>
public Book(int nId)
{
string strSql = "SELECT * FROM [Book] WHERE [ID]=" + nId;
this.Data = Bttlxe.Data.GlobalDataLoader.Loader.Execute(strSql).Data;
}
/// <summary>
/// Create and load a Book object.
/// </summary>
/// <param name="nId">The Book id.</param>
/// <param name="loader">The data loader to use.</param>
public Book(int nId, ref Bttlxe.Data.IDataLoader loader)
{
string strSql = "SELECT * FROM [Book] WHERE [ID]='" + nId;
this.Data = loader.Execute(strSql).Data;
}
}
这是“胶水”部分。两个加载 Book
对象的构造函数执行相同的任务。第一个函数中使用的 GlobalDataLoader.Loader
只是一个静态/共享的数据加载器,一旦你设置好数据加载器,就可以用它来避免传递引用。
第一行代码只是构造了一个 SQL 查询,它使用传入参数中的 Book.ID
来选择特定书籍的所有数据。这没什么新意。
第二行调用了你将在下一节创建的数据加载器的 Execute
成员,它返回一个实现了 IDataItem
接口的泛型对象。通过将我们 Book
对象的 Data
属性设置为返回的数据项的 Data
属性,我们就将数据加载到了我们的对象中。
创建一个 Book
对象现在就像调用一样简单:
Book oBook = new Book(1);
但如果你想保存一个 Book
对象呢?这比你想象的要容易——你只需要反向使用数据加载器的 Execute
成员:
Bttlxe.Data.IDataItem di = (Bttlxe.Data.IDataItem)oBook;
Bttlxe.Data.GlobalDataLoader.Loader.Execute(ref di,
Bttlxe.Data.DataOperation.Update);
首先,我们将对象转换回其接口,然后将该数据项传递给数据加载器,并指示它执行“更新”操作。在幕后,数据加载器会构建一个优化的 UPDATE
语句并执行它(假设数据加载器与DBMS协同工作)。
但是等等——更新什么?我们没有任何数据库结构!
Bttlxe.Data.GlobalDataLoader.Loader.Execute(ref di,
Bttlxe.Data.DataOperation.Create);
现在我们有了。(为了简化这个过程,有一个 DataSourceValidator
对象,可以让你检查数据项在数据源上的有效性,并可选择自动创建它或抛出异常。)
Read
、Write
、Update
、Create
、Delete
和 Drop
都是有效的操作。例如,如果你不想在你的 Book
对象构造函数中包含 SQL 语句(比如说你正在使用基于 XML 的数据加载器):
/// <summary>
/// Create and load a Book object.
/// </summary>
/// <param name="nId">The Book id.</param>
/// <param name="loader">The data loader to use.</param>
public Book(int nId, ref Bttlxe.Data.IDataLoader loader)
{
m_nId = nId;
Bttlxe.Data.IDataItem
di = (Bttlxe.Data.IDataItem)this;
loader.Execute(ref di, DataOperation.Read);
}
你现有的对象可以轻松实现 IDataItem
接口来利用这种方法。
创建一个数据加载器
有两种方法可以做到这一点。稍后会讨论一种更具可定制性的方法,但为简单起见,这里介绍如何创建一个处理 SQL Server 数据库的数据加载器:
Bttlxe.Data.SqlDataLoader oLoader = new Bttlxe.Data.SqlDataLoader();
oLoader.Database = "BOOKDB";
oLoader.Server = "SERVERNAME";
oLoader.UserID = "sa";
oLoader.Password = "IShouldntUseBlankPasswords";
oLoader.IntegratedSecurity = false;
oLoader.RemoteServer = "";
Bttlxe.Data.GlobalDataLoader.Loader = oLoader;
在这里,我们创建了一个新的 SqlDataLoader
并告诉它一些关于我们数据库的信息。最后一步是可选的,即将数据加载器存储在静态/共享的 GlobalDataLoader.Loader
对象中。
(请注意,并非所有这些属性都需要设置,这里只是为了完整性而展示。每个数据加载器可以实现不同的属性——这些显然只适用于 DBMS 数据源,所以请查阅数据加载器文档以了解正确的语法。)
作为比较,这是你创建 Microsoft Access 数据加载器的方式:
Bttlxe.Data.AccessDataLoader oLoader = new Bttlxe.Data.AccessDataLoader();
oLoader.Provider = Bttlxe.Data.AccessProviderVersion.MicrosoftJetOLEDB_4_0;
oLoader.DatabasePath = @"c:\mypath\data.mdb";
继续前进
到现在为止,我希望你已经理解了数据加载器为何以及如何有用。真正的威力来自于能够在不依赖于任何数据源先决条件的情况下实现你的对象。更强大的威力来自于能够在你的应用程序中随时切换数据加载器,或者混合搭配它们,而无需对你的实现做任何考虑。我强烈建议你在继续阅读之前,花几分钟时间,用纸和笔思考一下我们的示例系统,以及数据加载器和数据项如何帮助它,从而更好地理解它们的用途。
现在,我们将深入数据加载器内部,探索一些可以用它们执行的更高级的任务。我们先从数据加载器接口和支持类开始。我删掉了一些注释,但请务必阅读剩下的注释,因为我不会过多阐述:
/// <summary>
/// Objects implementing <see cref="IDataLoader"/> can be used as data loaders.
/// </summary>
/// <remarks>
/// <p>A data loader is an object that uses queries
/// to store and retrieve data from a
/// <p>Data loaders should be used when you need to bind data
/// to an object without knowing or wanting to
/// restrict to the source of the data. By abstracting out
/// the actual implementation from a common
/// interface loaders can be written to read and write data from
/// a variety of sources and objects using
/// data loaders can be designed with no consideration for the idioms
/// of the data sources themselves.</p>
/// <p>The query language is specific to each loader but the format
/// the data is provided in must support
/// <see cref="IDataItem"/> objects. Relationships are not supported.</p>
/// </remarks>
public interface IDataLoader
{
/// <summary>
/// Initialise the data loader.
/// </summary>
/// <remarks>
/// Use this method to initialise any connections that should
be present for the lifetime of the data loader.
/// </remarks>
void Initialise();
/// <summary>
/// Terminate the data loader.
/// </summary>
/// <remarks>
/// Use this method to terminate any connections
/// that should be present for the lifetime of the data loader.
/// If the object implementing <see cref="IDataLoader"/> also
/// implements <see cref="IDisposable"/> then this
/// method should be called from
/// the <see cref="IDisposable.Dispose"/> implementation.
/// </remarks>
void Terminate();
/// <summary>
/// Executes a scalar SELECT query.
/// </summary>
/// <param name="strQuery">The query to evaluate.</param>
/// <returns>The first record or the first row returned.</returns>
object ExecuteScalar(string strQuery);
/// <summary>
/// Executes a SELECT query.
/// </summary>
/// <param name="strQuery">The
query to evaluate.</param>
/// <returns>A <see cref="DataTable"/> containing
any result.</returns>
DataTable ExecuteDataTable(string strQuery);
/// <summary>
/// Executes a SELECT query.
/// </summary>
/// <param name="strQuery">The query to evaluate.</param>
/// <returns>A <see cref="IDataReader"/> containing any result.</returns>
IDataReader ExecuteReader(string strQuery);
/// <summary>
/// Executes a SELECT query.
/// </summary>
/// <param name="strQuery">The query to evaluate.</param>
/// <returns>An <see cref="XmlReader"/> containing any result.</returns>
XmlReader ExecuteXmlReader(string strQuery);
/// <summary>
/// Executes a non query (UPDATE, INSERT, or DELETE).
/// </summary>
/// <param name="strQuery">The query to evaluate.</param>
/// <returns>The number of records affected.</returns>
int ExecuteNonQuery(string strQuery);
/// <summary>
/// Executes a SELECT query.
/// </summary>
/// <param name="strQuery">The query to evaluate.</param>
/// <returns>A <see cref="IDataItem"/> containing any result.</returns>
IDataItem Execute(string strQuery);
/// <summary>
/// Executes a query.
/// </summary>
/// <param name="dataItem">The <see cref="IDataItem"/> either
/// containing the data passed to this method
/// or that will receive any records returned.</param>
/// <param name="operation">
/// The <see cref="DataOperation"/> to perform.</param>
void Execute(ref IDataItem dataItem, DataOperation operation);
/// <summary>
/// Event for mapping data item column names to those on the data source.
/// </summary>
/// <remarks>
/// This event is typically consumed by your Global class
/// and provides a way for to specify the column names on the data source
/// that map to those in the data item.
/// </remarks>
event NameMappingCallbackHandler NameMappingCallback;
}
Execute
方法是推荐的将数据加载器与数据项一起使用的方式。其他成员的存在是为了给你更大的控制权,但如果你计划使用不同的数据加载器,必须小心不要在你的代码中引入不兼容性。
名称映射(Name mapping)将在稍后更详细地讨论,但广义上说,它的目的是将数据项模式中的项名称映射到数据源上使用的名称,如果它们不同的话(例如,数据库表中的列名可能遵循一种最佳实践命名约定,而这种约定不能直接转换为你的对象命名约定)。NameMappingCallback
事件定义如下:
public delegate void NameMappingCallbackHandler(string table,
ref DataItemDictionary keys);
最后,DataItemDictionary
对象定义如下:
/// <summary>
/// A strongly typed collection of key-and-value pairs
/// for <see cref="DataItem"/> fields.
/// </summary>
[Serializable()]
publicclass DataItemDictionary : DictionaryBase
{
// ... implemented as a generic DictionaryBase object ...
}
前提知识讲完了;现在来看细节。
数据加载器如何加载数据?
看一下 SqlDataLoader
:
Bttlxe.Data.SqlDataLoader oLoader = new Bttlxe.Data.SqlDataLoader(false, true);
oLoader.Database = "BOOKDB";
oLoader.Server = "SERVERNAME";
oLoader.IntegratedSecurity = true;
一旦创建,当任何需要数据库的方法被调用时,与数据库的连接将被打开或从其先前的状态中重用。SqlDataLoader
的构造函数接受两个可选的布尔参数——第一个参数表示是否在数据加载器的生命周期内保持连接活动,第二个参数表示是否输出跟踪信息以帮助调试。
在我们查看加载和卸载数据项数据的代码之前,我们需要了解数据加载器是如何使用它们的。记住,数据项是任何实现了 IDataItem
接口以暴露描述其数据的模式的对象。数据加载器使用此模式来确定类型、大小和其他元数据,这些元数据对于在数据源上存储和检索你的数据是必需的——在本例中,是一个名为“BOOKDB”的 SQL Server 数据库。模式是使用数据项的说明书,但它不包含在数据源上对它所暴露的数据执行操作的任何指令。这就是每个数据加载器实现介入的地方,它根据被指示执行的操作提供其优化的解决方案:
读取 | 一个读操作,例如从数据源选择数据。 |
Write | 一个写操作,如果数据已存在,将执行一个 Update 。 |
更新 | 一个写操作,它将更新现有数据,如果数据不存在则失败。 |
Create | 一个创建操作,在数据源上创建一个 DataTable 。 |
删除 | 从数据源删除数据。 |
Drop | 从数据源中删除一个表。 |
由于我们正在使用 SqlDataLoader
和之前定义的 Book
对象,将每个操作与其对应的 SQL 语句联系起来思考可能会有所帮助:
读取 | SELECT * FROM Book WHERE ID=1 |
Write | INSERT INTO Book (ID, Title, Published) VALUES (1, 'Title', '1 January 2000') |
更新 | UPDATE Book SET Title='Title', Published='1 January 2000' WHERE ID=1 |
Create | CREATE TABLE Book ( |
删除 | DELETE FROM Book WHERE ID=1 |
Drop | DROP TABLE Book |
(注意,如果数据项已存在于数据源上,Write
操作将执行 Update
。语法和任何必要条件,例如允许设置标识列,都由每个数据加载器内部处理。)
如果我们从 Book
对象的构造函数入手,并逐步分析发生了什么,这个过程应该会变得更清晰:
/// <summary>
/// Create and load a Book object.
/// </summary>
/// <param name="nId">The Book id.</param>
/// <param name="loader">The data loader to use.</param>
public Book(int nId, ref Bttlxe.Data.IDataLoader loader)
{
m_nId = nId;
Bttlxe.Data.IDataItem di = (Bttlxe.Data.IDataItem)this;
loader.Execute(ref di, DataOperation.Read);
}
数据加载器的这个版本的 Execute
方法接受一个数据项的引用以及它应该对其执行的操作;在这种情况下,我们传递了我们的 Book
对象和一个 Read
操作。总的来说,发生的事情如下:
- 数据加载器检索
Book
对象的模式(schema)。 - 如果已向数据加载器注册了名称映射回调事件,它将把数据项的列名转换为数据源上存在的名称。
- 使用模式中提供的元数据构造一个 SQL 语句。由于 Book 的 ID 属性是主键(因此是唯一的),它将被用来追加一个
WHERE
子句。 - 如果数据项提供了
Condition
或Sort
属性,它们会被合并到 SQL 语句中。 - 该操作现在以 SQL Server 特定的格式描述,因此调用
ExecuteDataTable
成员来获取 SQL 查询的结果。此函数将在连接不可用时打开连接并处理请求。 - 返回的数据现在通过其
Data
属性映射回我们传入的数据项中,并在必要时使用反向名称映射。
我们的 Book
对象现在在其自身的属性和特性中包含了数据,准备好被我们的系统使用。
数据加载器的另一个版本的 Execute
方法不接受数据项的引用,而是传递一个特定于数据源的语言语句(例如SQL语句),并使用数据源返回的数据,通过从数据源检索到的模式来构造自己的通用数据项,并返回该数据项。这是在其他 Book
对象构造函数中使用的方法:
/// <summary>
/// Create and load a Book object.
/// </summary>
/// <param name="nId">The Book id.</param>
/// <param name="loader">The data loader to use.</param>
public Book(int nId, ref Bttlxe.Data.IDataLoader loader)
{
string strSql = "SELECT * FROM [Book] WHERE [ID]='" + nId;
this.Data = loader.Execute(strSql).Data;
}
数据加载器如何卸载数据?
与将数据加载到数据项的方式非常相似,数据加载器可以获取数据和模式(schema)来构造 Insert
和 Update
SQL语句,通过SOAP发送对象进行存储,将数据项写入磁盘等。具体的实现超出了本文的范围,但鼓励你阅读数据加载器的实现代码以充分理解它们的工作原理。
介入并掌控
到目前为止我们所涵盖的内容,是你开始使用数据加载器所需要知道的全部。然而,有时我们可能需要比目前为止所拥有的更多的控制权,这就是回调事件发挥作用的地方。
IDataLoader
要求所有数据加载器发出一个 NameMappingCallback
事件,定义如下:
public delegate void NameMappingCallbackHandler(string table,
ref DataItemDictionary keys);
如果我们订阅了这个事件,那么每次调用数据加载器的 Execute
成员之一(包括非数据项函数,如 ExecuteDataTable
或 ExecuteScalar
)时,我们就可以介入,并使用一个 DataItemDictionary
来控制数据项如何映射到数据源上。用一个例子来解释会更容易:
Bttlxe.Data.SqlDataLoader oLoader = new Bttlxe.Data.SqlDataLoader(false, true);
oLoader.Database = "BOOKDB";
oLoader.Server = "SERVERNAME";
oLoader.IntegratedSecurity = true;
oLoader.NameMappingCallback += new
NameMappingCallbackHandler(Loader_NameMappingCallback);
// ...
public void Loader_NameMappingCallback(string table,
ref Bttlxe.Data.DataItemDictionary keys)
{
keys.Clear();
switch (table)
{
case "Book":
keys.Add("ID", "bk_id");
keys.Add("Title", "bk_title");
keys.Add("Published", "bk_published_date");
break;
}
}
使用前面给出的一个例子,有了这个名称映射回调,以下使用数据项名称的 SQL 查询...
INSERT INTO Book (ID, Title,
Published) VALUES (1, 'Title', '1 January 2000')
...将被转换为以下使用数据源名称的 SQL 查询:
INSERT INTO Book (bk_id, bk_title,
bk_published_date) VALUES (1, 'Title', '1 January 2000')
当你的数据加载器连接到现有数据,或者你需要从不同来源获取数据并加载到相同的数据项中时,名称映射特别有用。例如,数据库可能包含书籍信息,但使用Web服务直接从出版商那里返回当前价格。
真正的透明性
虽然我们所讲的内容让你在很大程度上与数据源分离,但你无法轻易地从,比如说,SQL Server 数据源切换到 MySQL 数据源,而不改变所有从 SqlDataLoader
到 MySqlDataLoader
的引用。如果你使用 GlobalDataLoader.Loader
静态/共享对象,那么你可能只需要修改一两行代码,但这仍然不是真正的分离。
这段代码本身不属于数据加载器/数据项框架,但在你希望能够在不修改代码的情况下更换数据加载器时会很有用。App.Configuration
是一个包含应用程序配置属性的对象,它可能会从命令行或配置文件中获取你指定的信息:
switch (App.Configuration.DataSourceType)
{
case DataSourceType.MicrosoftAccess:
{
// create our data loader
AccessDataLoader oLoader = new AccessDataLoader(
App.Configuration.DatabaseKeepAlive,
App.Configuration.DatabaseTraceEnabled);
oLoader.Provider = AccessProviderVersion.MicrosoftJetOLEDB_4_0;
oLoader.DatabasePath = App.Configuration.Database;
oLoader.UserID = App.Configuration.DatabaseUserId;
oLoader.Password = App.Configuration.DatabasePassword;
oLoader.Initialise();
// set the global data loader reference
GlobalDataLoader.Loader = oLoader;
break;
}
case DataSourceType.MySql:
{
// create our data loader
MySqlDataLoader oLoader = new MySqlDataLoader(
App.Configuration.DatabaseKeepAlive,
App.Configuration.DatabaseTraceEnabled);
oLoader.Database = App.Configuration.Database;
oLoader.Server = App.Configuration.DatabaseServer;
oLoader.UserID = App.Configuration.DatabaseUserId;
oLoader.Password = App.Configuration.DatabasePassword;
oLoader.Initialise();
// set the global data loader reference
GlobalDataLoader.Loader = oLoader;
break;
}
case DataSourceType.SqlServer:
{
// create our data loader
SqlDataLoader oLoader = new SqlDataLoader(
App.Configuration.DatabaseKeepAlive,
App.Configuration.DatabaseTraceEnabled);
oLoader.Database = App.Configuration.Database;
oLoader.Server = App.Configuration.DatabaseServer;
oLoader.UserID = App.Configuration.DatabaseUserId;
oLoader.Password = App.Configuration.DatabasePassword;
oLoader.IntegratedSecurity =
App.Configuration.DatabaseIntegratedSecurity;
oLoader.RemoteServer = App.Configuration.DatabaseRemoteServer;
oLoader.Initialise();
// set the global data loader reference
GlobalDataLoader.Loader = oLoader;
break;
}
}
所以如果 App.Configuration
正在读取一个 xml.config 文件,它可能看起来像这样:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<datasource>
type="SqlServer"
database="BOOKDB"
server="SERVERNAME"
intergratedAuthentication="true"
keepAlive="false"
traceEnabled="true"
/>
</datasource>
</configuration>
而且它可以很容易地改成这样:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<datasource>
type="MySql"
database="BOOKDB"
server="MyServer"
userId="username"
password="password"
keepAlive="false"
traceEnabled="true"
/>
</datasource>
</configuration>
性能
这一点对于 Web 应用程序尤其重要:多用缓存,多用缓存。桌面应用程序可以一次性加载一个数据项并将其保存在本地,但 Web 应用程序通常需要在每个请求时加载数据项。对于少量用户负载,这不会构成问题,但如果你每分钟有一千个用户访问你的网站,并且每次都有二十个 book 对象被加载到数据项中,那么你的数据源将会承受巨大的压力。像缓存对象并执行一个简单的“如果对象已缓存则克隆它,否则获取一个新的并缓存它”这样的操作,可以将你的性能提升数百个百分点。
数据项列表
你可能已经发现了到目前为止讨论的方法的一个局限性——如何从数据源中为对象列表选择所有内容?在SQL中,你可以执行 SELECT * FROM Books
来一次性检索所有的书籍。显然,这比为每本书单独访问数据源要好得多。但是你不能用数据项做到这一点。
嗯,如果你从逻辑上思考,你是可以的:创建一个 BookList
数据项,其模式支持 Book
项,并一次性从数据源检索所有的书籍。
考虑以下数据项实现:
/// <summary>
/// A list of <see cref="Book"/> objects.
/// </summary>
/// <remarks>
/// This object supports serialization.
/// </remarks>
[Serializable()]
public class BookList : IEnumerable, IEnumerator, Bttlxe.Data.IDataItem
{
// collection of items in this list
private ArrayList m_aItems = new ArrayList();
private int m_nPosition = -1;
/// <summary>
/// The list condition.
/// </summary>
protected string m_strCondition = string.Empty;
/// <summary>
/// The list sort.
/// </summary>
protected string m_strSort = string.Empty;
由于这个数据项代表了数据源上的一个数据列表,我们将在下面实现 IDataItem
时实现 Condition
和 Sort
属性。
#region Enumerator
/// <summary>
/// Return this class as the enumerator.
/// </summary>
/// <returns>The <see cref="IEnumerator"/> representing this object.</returns>
public IEnumerator GetEnumerator()
{
m_nPosition = -1;
return (IEnumerator)this;
}
/// <summary>
/// Move to the next object in the enumeration.
/// </summary>
/// <returns>Whether the operation was successful.</returns>
public bool MoveNext()
{
m_nPosition++;
if (m_nPosition < m_aItems.Count)
return true;
else
{
m_nPosition = -1;
return false;
}
}
/// <summary>
/// Reset the enumeration by setting the position to -1
/// </summary>
public void Reset()
{
m_nPosition = -1;
}
/// <summary>
/// Return the current object
/// </summary>
/// <value>
/// The current <see cref="Book"/> in the collection.
/// </value>
public object Current
{
get
{
return m_aItems[m_nPosition];
}
}
/// <summary>
/// Access the array directly
/// </summary>
/// <value>
/// The <see cref="ArrayList"/> of <see cref="Book"/> objects in the collection
/// </value>
public ArrayList Items
{
get
{
return m_aItems;
}
}
/// <summary>
/// Add an object to the collection.
/// </summary>
/// <param name="oItem">The <see cref="Book"/> to add.</param>
public void Add(Book oItem)
{
m_aItems.Add(oItem);
}
#endregion
通过实现 .NET Framework 的 IEnumerable
和 IEnumerator
接口,我们可以做一些事情,比如将我们的数据项数据绑定到控件,以及使用 foreach
结构 - foreach (Book oBook in oBooklist)
。
#region IDataItem Members
/// <summary>
/// The data table.
/// </summary>
public DataSet Data
{
get
{
// create the data table
DataTable dt = Schema.Tables["Book"].Copy();
// add our data as a row in the table
foreach (Book oItem in this)
{
DataRow oRow = dt.NewRow();
oRow["ID"] = oItem.ID;
oRow["Title"] = oItem.Title;
oRow["Published"] = oItem.Published;
dt.Rows.Add(oRow);
}
DataSet ds = new DataSet("Book");
ds.Tables.Add(dt);
return ds;
}
set
{
// remove all the existing items in the collection
Items.Clear();
foreach (DataRow oRow in value.Tables["Book"].Rows)
{
Book oItem = new Book();
oItem.ID = (int)oRow["ID"];
oItem.Title = (string)oRow["Title"];
oItem.Published = (DateTime)oRow["Published"];
Add(oItem);
}
}
}
Data
属性使用 DataTable
中的每一行来创建一个新的 Book
对象,并使用其自身的属性来设置其数据,而不是使用 Book
对象的 Data
属性。将数据返回给数据加载器的过程则正好相反。
/// <summary>
/// A data set containing the schema for this object.
/// </SUMMARY>
public DataSet Schema
{
get
{
return new Book().Schema;
}
}
BookList
的模式与 Book
对象完全相同。
/// <summary>
/// The condition clause.
/// </summary>
/// <value>
/// An SQL condition clause.
/// </value>
public string Condition
{
get
{
return m_strCondition;
}
set
{
m_strCondition = value;
}
}
/// <summary>
/// The sort string.
/// </summary>
/// <value>
/// An SQL ORDER BY string.
/// </value>
public string Sort
{
get
{
return m_strSort;
}
set
{
m_strSort = value;
}
}
#endregion
当一个 BookList
被传入数据加载器的 Execute
成员时,Condition
和 Sort
属性(如果已设置)将被用来对这个集合中的 Book
进行排序。
/// <summary>
/// Create and load a BookList object.
/// </summary>
public BookList()
{
string strSql = "SELECT * FROM [Book]";
this.Data = GlobalDataLoader.Loader.Execute(strSql).Data;
}
/// <summary>
/// Create and load a BookList object.
/// </summary>
/// <param name="bAlphabetical">Sort the books by Title.</param>
public BookList(bool bAlphabetical)
{
if (bAlphabetical)
{
string strSql = "SELECT * FROM [Book] ORDER BY [Title]";
this.Data = GlobalDataLoader.Loader.Execute(strSql).Data;
}
else
{
string strSql = "SELECT * FROM [Book]";
this.Data = GlobalDataLoader.Loader.Execute(strSql).Data;
}
}
}
第一个构造函数与第一个 Book
构造函数类似,但没有指定 WHERE
条件,因此数据加载器返回的通用数据项将包含数据源上每本书的数据。
第二个构造函数展示了如何在加载数据时对其进行排序。
请记住,即使你以这种方式指定了特定于数据源的语句(如 SQL 语句),它们仍然会被解析,并且如果为数据项定义了名称映射,它们也会受到名称映射的影响。
限制和注意事项
你不能在你的数据中建立关系。这是一个重要的考虑因素,所以在你尝试使用数据加载器之前,请确保你完全理解这一点:你不能在你的数据中建立关系。
如果你正在使用一个与 DBMS 协同工作的数据加载器,并且在你自己指定的任何查询中使用了连接(joins),那么任何名称映射将只应用于你正在查询的第一个表。大多数 SQL 函数,如 CONVERT
或 SUM
等,应该可以正常工作。
在不了解你将要使用的数据加载器如何处理复杂查询的情况下,不要使用它们。我没有遇到过自己无法轻易解决的问题,但这并不意味着你不会遇到。为你的数据加载器启用跟踪功能,根据你正在开发的应用程序类型,你将在 Web 跟踪或调试器输出窗口中看到数据加载器发送到你的数据源的内容。
Web 应用程序的跟踪示例:
在指定 SQL 时,始终将表名和列名用方括号括起来,例如 SELECT [Title] FROM [Book] WHERE [ID]=1
。数据库数据加载器使用这些方括号来帮助它们分析查询并执行名称和模式映射。如果 DBMS 不支持,相关的数据加载器会在内部将它们剥离。仅在必要时使用空格。
理想情况下,你根本不会编写任何特定于数据源的代码。
请记住,如果你的数据项模式允许某列的值为 DBNull
,那么在加载数据时你必须检查空值。
结论
数据加载器让你可以在构建应用程序时无需考虑数据将如何存储或传输。如果使用得当并出于正确的原因,这可以成为一种强大的方法,用于实现快速且可扩展的数据到对象绑定。
这是我第一次尝试从一个更大的代码库中剥离出一些东西,虽然在发布这篇文章之前我已经测试过它,但重写过程中我可能遗漏了某些东西。随附的开发者文档中包含了报告任何问题的链接,我将尽力解决这些问题。