第5章 设计一个O/R Mapping框架
在本章中,我们将设计一个可用的O/R Mapping框架,来详细讨论一下在O/R Mapping中可能用到的一些技术,以及一些问题的处理对策。
整个框架,我们会使用C#语言来编写,并且,会以Websharp框架作为实际的例子,关于Websharp框架的信息和源代码,可以从www.websharp.org下载。
5.1封装数据库访问层
一个好的O/R Mapping框架,应当做到数据库无关性,这就要求对数据库的访问做一个封装,能够屏蔽不同数据库之间的差异,这样,在更换后台数据库的时候,能够不用重新修改代码。
在.Net中,微软提供的基础数据库访问技术是ADO.Net。ADO.NET 是基于 .NET 的应用程序的数据访问模型。可以使用它来访问关系数据库系统(如 SQL Server 2000、Oracle)和其他许多具有 OLE DB 或 ODBC 提供程序的数据源。在某种程度上,ADO.NET 代表 ADO 技术的最新进展。不过,ADO.NET 引入了一些重大变化和革新,旨在解决 Web 应用程序的松耦合特性以及在本质上互不关联的特性。
ADO.NET 依赖于 .NET 数据提供程序的服务。这些提供程序提供对基础数据源的访问,并且包括五个主要对象(Connection、Command、DataSet、DataReader 和 DataAdapter)。
目前,ADO.NET 随附了两类提供程序:Bridge提供程序和Native提供程序。通过Bridge 提供程序(如那些为 OLE DB 和 ODBC 提供的提供程序),可以使用为以前的数据访问技术设计的数据库。Native 提供程序(如 SQL Server 和 Oracle 提供程序)通常能够提供性能方面的改善,部分原因在于少了一个抽象层。
? SQL Server .NET 数据提供程序。这是一个用于 Microsoft SQL Server 7.0 和更高版本数据库的提供程序。它被进行了优化以便访问 SQL Server,并且它通过使用 SQL Server 的本机数据传输协议来直接与 SQL Server 进行通讯。当连接到 SQL Server 7.0 或 SQL Server 2000 时,应当始终使用该提供程序。
? Oracle .NET 数据提供程序。用于 Oracle 的 .NET 框架数据提供程序通过 Oracle 客户端连接软件支持对 Oracle 数据源的数据访问。该数据提供程序支持 Oracle 客户端软件版本 8.1.7 及更高版本。
? OLE DB .NET 数据提供程序。这是一个用于 OLE DB 数据源的托管提供程序。它的效率要比 SQL Server .NET 数据提供程序稍微低一些,因为它在与数据库通讯时通过 OLE DB 层进行调用。请注意,该提供程序不支持用于开放式数据库连接 (ODBC) 的 OLE DB 提供程序 MSDASQL。对于 ODBC 数据源,请改为使用 ODBC .NET 数据提供程序(稍后将加以介绍)。
? ODBC .NET 数据提供程序。用于 ODBC 的 .NET 框架数据提供程序使用本机 ODBC 驱动程序管理器 (DM) 来支持借助于 COM 互操作性进行的数据访问。还有其他一些目前正处于测试阶段的 .NET 数据提供程序。
与各个 .NET 数据提供程序相关联的类型(类、结构、枚举等)位于其各自的命名空间中:
? System.Data.SqlClient:包含SQL Server .NET数据提供程序类型。
? System.Data.OracleClient:包含 Oracle .NET数据提供程序。
? System.Data.OleDb:包含 OLE DB .NET 数据提供程序类型。
? System.Data.Odbc:包含ODBC .NET数据提供程序类型。
? System.Data:包含独立于提供程序的类型,如DataSet 和 DataTable。
在各自的关联命名空间内,每个提供程序都提供了对 Connection、Command、DataReader 和 DataAdapter 对象的实现。SqlClient 实现的前缀为“Sql”,而 OleDb 实现的前缀为“OleDb”。例如,Connection 对象的 SqlClient 实现是 SqlConnection,而 OleDb 实现则为 OleDbConnection。同样,DataAdapter 对象的两个实现分别为 SqlDataAdapter 和 OleDbDataAdapter。
为了屏蔽不同数据库之间的差异,我们首先要设计数据库访问的接口。把这个接口名为DataAccess,定义如下:
public interface DataAccess { #region Support Property & Method DatabaseType DatabaseType{get;} IDbConnection DbConnection{get;} IDbTransaction BeginTransaction(); void Open(); void Close(); bool IsClosed{get;} #endregion #region ExecuteNonQuery int ExecuteNonQuery(CommandType commandType, string commandText); int ExecuteNonQuery(string commandText); int ExecuteNonQuery(string commandText, QueryParameterCollection commandParameters); int ExecuteNonQuery(CommandType commandType, string commandText, QueryParameterCollection commandParameters); #endregion ExecuteNonQuery //……因篇幅的原因,这里没有列出所有的方法,关于其他方法的定义请参见源代码。 } |
在这个接口之下,定义AbstractDataAccsee类,实现一些公用的数据方法,在AbstractDataAccsee类之下,再扩展出各个具体的DataAccsee实现类。整个结构可以用下面的图(图3.1)来表示:
图3.1
DataAccsee类的代码片断如下:
public abstract class AbstractDataAccess : DataAccess { #region DataAccess #region Support Property & method public abstract DatabaseType DatabaseType{get;} public abstract IDbConnection DbConnection{get;}
public void Close() { this.DbConnection.Close(); } public void Open() { if(this.DbConnection.State.Equals(ConnectionState.Closed)) this.DbConnection.Open(); } …… #endregion Support Property & method #region ExecuteNonQuery public int ExecuteNonQuery(CommandType commandType, string commandText) { return this.ExecuteNonQuery(commandType, commandText, null); } public int ExecuteNonQuery(string commandText) { return this.ExecuteNonQuery(CommandType.Text, commandText, null); } public int ExecuteNonQuery(string commandText, QueryParameterCollection commandParameters) { return this.ExecuteNonQuery(CommandType.Text, commandText, commandParameters); } public abstract int ExecuteNonQuery(CommandType commandType, string commandText, QueryParameterCollection commandParameters); #endregion ExecuteNonQuery protected void SyncParameter(QueryParameterCollection commandParameters) { if((commandParameters!=null) && (commandParameters.Count>0) ) { for(int i=0;i<commandParameters.Count;i++) { commandParameters[i].SyncParameter(); } } } } |
然后,我们可以实现具体的数据库访问的方法。例如,SQL Server的数据库访问类可以实现如下:
public sealed class MSSqlDataAccess : AbstractDataAccess { #region Constructor public MSSqlDataAccess(SqlConnection conn) { this.m_DbConnection=conn; } public MSSqlDataAccess(string connectionString) { this.m_DbConnection=new SqlConnection(connectionString); } #endregion
#region DataAccess #region Support Property & method public override DatabaseType DatabaseType { get{return DatabaseType.MSSQLServer;} } private SqlConnection m_DbConnection; public override IDbConnection DbConnection { get { return m_DbConnection; } }
private SqlTransaction trans=null; public override IDbTransaction BeginTransaction() { trans=m_DbConnection.BeginTransaction(); return trans; } #endregion Support Property & method #region ExecuteNonQuery public override int ExecuteNonQuery(CommandType commandType, string commandText, QueryParameterCollection commandParameters) { SqlCommand cmd=new SqlCommand(); PrepareCommand(cmd,commandType, commandText,commandParameters); int tmpValue=cmd.ExecuteNonQuery(); SyncParameter(commandParameters); cmd.Parameters.Clear(); return tmpValue; } #endregion ExecuteNonQuery #region ExecuteDataSet public override DataSet ExecuteDataset(CommandType commandType, string commandText, QueryParameterCollection commandParameters,DataSet ds,string tableName) { SqlCommand cmd=new SqlCommand(); PrepareCommand(cmd,commandType, commandText,commandParameters); SqlDataAdapter da=new SqlDataAdapter(cmd); if(Object.Equals(tableName,null) || (tableName.Length<1)) da.Fill(ds); else da.Fill(ds,tableName); SyncParameter(commandParameters); cmd.Parameters.Clear(); return ds; } #endregion ExecuteDataSet #region ExecuteReader public override IDataReader ExecuteReader(CommandType commandType, string commandText, QueryParameterCollection commandParameters) { SqlCommand cmd=new SqlCommand(); PrepareCommand(cmd,commandType, commandText,commandParameters); SqlDataReader dr=cmd.ExecuteReader(); SyncParameter(commandParameters); cmd.Parameters.Clear(); return dr; } #endregion ExecuteReader #region ExecuteScalar public override object ExecuteScalar(CommandType commandType, string commandText, QueryParameterCollection commandParameters) { SqlCommand cmd=new SqlCommand(); PrepareCommand(cmd,commandType, commandText,commandParameters); object tmpValue=cmd.ExecuteScalar(); SyncParameter(commandParameters); cmd.Parameters.Clear(); return tmpValue; } #endregion ExecuteScalar #region ExecuteXmlReader public override XmlReader ExecuteXmlReader(CommandType commandType, string commandText, QueryParameterCollection commandParameters) { SqlCommand cmd=new SqlCommand(); PrepareCommand(cmd,commandType, commandText,commandParameters); XmlReader reader=cmd.ExecuteXmlReader(); SyncParameter(commandParameters); cmd.Parameters.Clear(); return reader; } #endregion ExecuteXmlReader #endregion private void PrepareCommand(SqlCommand cmd,CommandType commandType, string commandText, QueryParameterCollection commandParameters) { cmd.CommandType=commandType; cmd.CommandText=commandText; cmd.Connection=this.m_DbConnection; cmd.Transaction=trans; if((commandParameters!=null) && (commandParameters.Count>0) ) { for(int i=0;i<commandParameters.Count;i++) { commandParameters[i].InitRealParameter(DatabaseType.MSSQLServer); cmd.Parameters.Add(commandParameters[i].RealParameter as SqlParameter); } } } } |
现在,我们已经有了数据库访问的接口和具体的实现类,为了管理这些类,并且提供可扩展性,我们需要创建一个类来提供获取具体实现类的方法,这个类就是Factory类。这个类很简单,主要的功能就是根据参数,判断使用什么数据库,然后,返回适当的DataAccess类。这是典型的Factory设计模式。关于设计模式的更多资料,可以参考《设计模式——可复用面向对象设计基础》一书。
这个类的定义如下:
public sealed class DataAccessFactory { private DataAccessFactory(){} private static DatabaseProperty defaultDatabaseProperty; public static DatabaseProperty DefaultDatabaseProperty { get{return defaultDatabaseProperty;} set{defaultDatabaseProperty=value;} } public static DataAccess CreateDataAccess(DatabaseProperty pp) { DataAccess dataAccess; switch(pp.DatabaseType) { case(DatabaseType.MSSQLServer): dataAccess = new MSSqlDataAccess(pp.ConnectionString); break; case(DatabaseType.Oracle): dataAccess = new OracleDataAccess(pp.ConnectionString); break; case(DatabaseType.OleDBSupported): dataAccess = new OleDbDataAccess(pp.ConnectionString); break; default: dataAccess=new MSSqlDataAccess(pp.ConnectionString); break; } return dataAccess; } public static DataAccess CreateDataAccess() { return CreateDataAccess(defaultDatabaseProperty); } } |
关于DatabaseProperty和DatabaseType的定义,可以参见相关源代码。
数据访问功能的调用形式如下:
DataAccess dao=DataAccessFactory.CreateDataAccess(persistenceProperty); db.Open(); db.需要的操作 db.Close(); |
当数据库发生变化的时候,只需要修改相应的DatabaseProperty参数,DataAccessFactory会根据参数的不同,自动调用相应的类,客户端不会感觉到变化,也不用去关心。这样,实现了良好的封装性。当然,前提是,你在编写程序的时候,没有用到特定数据库的特性,例如,Sql Server的专用函数。
5.2设计映射
映射部分,完成对象和关系型数据库之间映射关系的表达。前面探讨过,在.Net环境中,可以使用Attribute来描述。在Websharp框架中,我们设计了以下Attribute来描述对象和关系型数据库之间的映射。
? TableMapAttribute,
这个Attribute描述对象和数据库表的映射关系,这个类有两个属性,TableName属性指明和某个类所对应的数据库表,PrimaryKeys用来描述表的主关键字。这个类的定义如下:
[AttributeUsage(AttributeTargets.Class)] public class TableMapAttribute : Attribute { private string tableName; private string[] primaryKeys; public TableMapAttribute(string tableName,params string[] primaryKeys) { this.tableName = tableName; this.primaryKeys = primaryKeys; } public string TableName { get{return tableName;} set{tableName = value;} }
public string[] PrimaryKeys { get{return primaryKeys;} set{primaryKeys = value;} } } |
? ColumnMapAttribute
这个Attribute描述对象属性和数据库中表的字段之间的映射关系,这个类有三个属性,ColumnName属性指明和某个属性所对应的字段,DbType属性指明数据库字段的数据类型,DefaultValue指明字段的默认值。这个类的定义如下:
[AttributeUsage(AttributeTargets.Property)] public class ColumnMapAttribute : Attribute { private string columnName; private DbType dbtype; private object defaultValue; public ColumnMapAttribute(string columnName,DbType dbtype) { this.columnName = columnName; this.dbtype = dbtype; } public ColumnMapAttribute(string columnName,DbType dbtype,object defaultValue) { this.columnName = columnName; this.dbtype = dbtype; this.defaultValue = defaultValue; } public string ColumnName { get{return columnName;} set{columnName = value;} } public DbType DbType { get{return dbtype;} set{dbtype = value;} }
public object DefaultValue { get{return defaultValue;} set{defaultValue = value;} } } |
? ReferenceObjectAttribute
ReferenceObjectAttribute指示该属性是引用的另外一个对象,因此,在执行持久化操作的时候,需要根据参数进行额外的处理。默认情况下,当持久化实体对象的时候,ReferenceObjectAttribute指示的属性,不进行操作。这个类有三个属性,ReferenceType指明所引用的对象的类型,PrimaryKey和ForeignKey用来指明两个类之间进行关联的主键和外键。这个类的定义如下:
[AttributeUsage(AttributeTargets.Property)] public class ReferenceObjectAttribute : Attribute { private Type referenceType; private string primaryKey; private string foreignKey; public ReferenceObjectAttribute(Type referenceType,string primaryKey,string foreignKey) { this.referenceType = referenceType; this.primaryKey = primaryKey; this.foreignKey = foreignKey; } public ReferenceObjectAttribute(){} public Type ReferenceType { get{return referenceType;} set{referenceType = value;} } public string PrimaryKey { get{return primaryKey;} set{primaryKey = value;} } public string ForeignKey { get{return foreignKey;} set{foreignKey = value;} } } |
? SubObjectAttribute
SubObjectAttribute指示该属性是引用的是子对象,因此,在执行持久化操作的时候,需要根据参数进行额外的处理。默认情况下,当持久化实体对象的时候,SubObjectAttribute指示的属性,不进行操作。
这个类的定义如下:
[AttributeUsage(AttributeTargets.Property)] public class SubObjectAttribute : Attribute { private Type subObjectType; private string primaryKey; private string foreignKey; public SubObjectAttribute(Type subObjectType, string primaryKey, string foreignKey) { this.subObjectType = subObjectType; this.primaryKey = primaryKey; this.foreignKey = foreignKey; } public SubObjectAttribute(){} public Type SubObjectType { get { return subObjectType; } set { subObjectType = value; } } public string PrimaryKey { get{return primaryKey;} set{primaryKey = value;} } public string ForeignKey { get{return foreignKey;} set{foreignKey = value;} } } |
? AutoIncreaseAttribute
AutoIncreaseAttribute指示该属性是自动增长的。自动增长默认种子为
这个类的定义如下:
[AttributeUsage(AttributeTargets.Property)] public class AutoIncreaseAttribute : Attribute { private int step = 1; public AutoIncreaseAttribute(){} public AutoIncreaseAttribute(int step) { this.step = step; } public int Step { get{return step;} set{step = value;} } } |
设计好映射的方法后,我们就可以来定义实体类以及同数据库之间的映射。下面是一个例子:
//订单类别 [TableMap("OrderType","ID")] public class OrderType { private int m_ID; private string m_Name; [ColumnMap("ID",DbType.Int32)] public int ID { get { return m_ID; } set { m_ID = value; } } [ColumnMap("Name", DbType.String)] public string Name { get { return m_Name; } set { m_Name = value; } } } //订单 [TableMap("Order", "OrderID")] public class Order { private int m_OrderID; private OrderType m_OrderType; private string m_Title; private DateTime m_AddTime; private bool m_IsSigned; private List<OrderDetail> m_Details; [ColumnMap("OrderID",DbType.Int32)] [AutoIncrease] public int OrderID { get { return m_OrderID; } set { m_OrderID = value; } } [ReferenceObject(typeof(OrderType),"ID","TypeID")] [ColumnMap("TypeID",DbType.String)] public OrderType OrderType { get { return m_OrderType; } set { m_OrderType = value; } } [ColumnMap("Title", DbType.String)] public string Title { get { return m_Title; } set { m_Title = value; } } [ColumnMap("AddTime", DbType.DateTime)] public DateTime AddTime { get { return m_AddTime; } set { m_AddTime = value; } } [ColumnMap("AddTime", DbType.Boolean)] public bool IsDigned { get { return m_IsSigned; } set { m_IsSigned = value; } } [SubObject(typeof(OrderDetail),"OrderID","OrderID")] public List<OrderDetail> Details { get { return m_Details; } set { m_Details = value; } } } //订单明细 public class OrderDetail { private int m_DetailID; private int m_OrderID; private string m_ProductName; private int m_Amount; [ColumnMap("ID", DbType.Int32)] [AutoIncrease] public int DetailID { get { return m_DetailID; } set { m_DetailID = value; } } [ColumnMap("OrderID", DbType.Int32)] public int OrderID { get { return m_OrderID; } set { m_OrderID = value; } } [ColumnMap("ProductName", DbType.String)] public string ProductName { get { return m_ProductName; } set { m_ProductName = value; } } [ColumnMap("Amount", DbType.Int32)] public int Amount { get { return m_Amount; } set { m_Amount = value; } } } |
Order中
[ReferenceObject(typeof(OrderType),"ID","TypeID")] [ColumnMap("TypeID",DbType.String)] public OrderType OrderType { get { return m_OrderType; } set { m_OrderType = value; } } |
这段代码表明,OrderType这个属性,引用了OrderType这个对象,同OrderType相关联的,是OrderType的主键ID和Order的外键TypeID。
[SubObject(typeof(OrderDetail),"OrderID","OrderID")] public List<OrderDetail> Details { get { return m_Details; } set { m_Details = value; } } |
这段代码表明,Details这个属性,由子对象OrderDetail的集合组成,其中,两个对象通过Order类的OrderID主键和OrderDetail的外键OrderID相关联。
有了以上的类结构,我们可以为他们生成相应的数据库操作的SQL语句。在上面的三个对象中,分别对应的SQL语句是(以SQL Server为例):
OrderType: INSERT INTO OrderType(ID,Name) VALUES(@ID,@NAME) UPDATE OrderType SET Name=@Name Where ID=@ID DELETE FROM OrderType Where ID=@ID |
Order: INSERT INTO Order(TypeID,Title,AddTime,IsSigned) VALUES (@TypeID, @Title, @AddTime, @IsSigned) ; SELECT @OrderID=@@IDENTITY ‘其中@OrderID为传出参数 UPDATE Order SET TypeID=@TypeID, Title=@Title, AddTime=@AddTime, IsSigned=@IsSigned WHERE OrderID=@OrderID DELETE FROM Order WHERE OrderID=@OrderID |
OederDetail: INSERT INTO OederDetail(OrderID,ProductName,Amount) VALUES (@OrderID, @ProductName, @Amount); SELECT @ID=@@IDENTITY ‘其中@ID为传出参数 UPDATE OederDetail SET OrderID=@OrderID, ProductName=@ProductName, Amount=@Amount WHERE ID=@ID DELETE FROM OederDetail WHERE ID=@ID |
5.3 对继承的支持
Websharp框架在设计的时候,要求能够支持面向对象语言中的继承。前面已经讨论过,在O/R Mapping框架中,一般说来,有三种继承模式:ONE_INHERITANCE_TREE_ONE_TABLE、ONE_INHERITANCE_PATH_ONE_TABLE和ONE_CLASS_ONE_TABLE。在Websharp框架中可以实现对这三种模式的支持。我们依然以前面的第三章第3.2节的例子为例:
ONE_INHERITANCE_TREE_ONE_TABLE
这种映射模式将具有相同父类的所有类都映射到一个数据库表中。数据库结构如下图:
在Websharp中,只需要对每个类都指明具有相同值的TableMap特性就可以了。如下面的代码:
[TableMap("Table1", "Property1")] public class Parent { private string property1; private string property2; [ColumnMap("Column1", DbType.String)] public string Property1 { get { return property1; } set { property1=value; } } [ColumnMap("Column2", DbType.String)] public string Property2 { get { return property2; } set { property2 = value; } } } [TableMap("Table1", "Property1")] public class Child1 : Parent { private string property3; [ColumnMap("Column3", DbType.String)] public string Property3 { get { return property3; } set { property3 = value; } } } [TableMap("Table1", "Property1")] public class Child2 : Parent { private string property4; [ColumnMap("Column4", DbType.String)] public string Property4 { get { return property4; } set { property4 = value; } } } |
此时,当按照如下的代码初始化一个Child1对象,
Child1 c1 = new Child1(); c1.Property1 = "P11"; c1.Property2 = "P12"; c1.Property3 = "P13"; |
并保存到数据库中的时候,数据库中的记录应该是:
Column1 |
Column2 |
Column3 |
Column4 |
P11 |
P12 |
P13 |
NULL |
如果按照如下的代码初始化一个Child2对象:
Child2 c1 = new Child2(); c2.Property1 = "P21"; c2.Property2 = "P22"; c2.Property4 = "P24"; |
并保存到数据库中的时候,数据库中的记录应该是:
Column1 |
Column2 |
Column3 |
Column4 |
P21 |
P22 |
NULL |
P24 |
ONE_INHERITANCE_PATH_ONE_TABLE
这种映射模式将一个继承路径映射到一个表,这种情况下的数据库的结构是:
这种情况下,实际上Parent类并不映射到实际的表,Child1和Child2类分别映射到Child1和Child2表。因此,在这种情况下,需要把Parent类的TableMap特性设置为Null,而Child1和Child2类的TableMap特性分别设置为Child1和Child2,代码如下面所示:
[TableMap(null, "Property1")] public class Parent { private string property1; private string property2; [ColumnMap("Column1", DbType.String)] public string Property1 { get { return property1; } set { property1=value; } } [ColumnMap("Column2", DbType.String)] public string Property2 { get { return property2; } set { property2 = value; } } } [TableMap("Child1", "Property1")] public class Child1 : Parent { private string property3; [ColumnMap("Column3", DbType.String)] public string Property3 { get { return property3; } set { property3 = value; } } } [TableMap("Child2", "Property1")] public class Child2 : Parent { private string property4; [ColumnMap("Column4", DbType.String)] public string Property4 { get { return property4; } set { property4 = value; } } } |
此时,当按照如下的代码初始化一个Child1对象,
Child1 c1 = new Child1(); c1.Property1 = "P11"; c1.Property2 = "P12"; c1.Property3 = "P13"; |
并保存到数据库中的时候,数据库中应该只在Child1表中添加下面的数据:
Column1 |
Column2 |
Column3 |
P11 |
P12 |
P13 |
如果保存的是一个Child2对象,那么,应该只在Child2表中添加数据。Child1表和Child2表是互相独立的,并不会互相影响。
ONE_CLASS_ONE_TABLE
这种映射模式将每个类映射到对应的一个表,对于上面的类结构,数据库的结构是:
这种映射模式,我们只需要分别对每个类设定各自映射的表就可以了。代码如下面所示:
[TableMap("Parent", "Property1")] public class Parent { private string property1; private string property2; [ColumnMap("Column1", DbType.String)] public string Property1 { get { return property1; } set { property1=value; } } [ColumnMap("Column2", DbType.String)] public string Property2 { get { return property2; } set { property2 = value; } } } [TableMap("Child1", "Property1")] public class Child1 : Parent { private string property3; [ColumnMap("Column3", DbType.String)] public string Property3 { get { return property3; } set { property3 = value; } } } [TableMap("Child2", "Property1")] public class Child2 : Parent { private string property4; [ColumnMap("Column4", DbType.String)] public string Property4 { get { return property4; } set { property4 = value; } } } |
此时,当按照如下的代码初始化一个Child1对象,
Child1 c1 = new Child1(); c1.Property1 = "P11"; c1.Property2 = "P12"; c1.Property3 = "P13"; |
并保存到数据库中的时候,数据库中的记录应该是:
Parent表:
Column1 |
Column2 |
P11 |
P12 |
Child1表:
Column1 |
Column3 |
P11 |
P13 |
同样的,如果保存的是一个Child2对象,那么,将在Parent表和Child2表中添加记录。
5.4设计对象操纵框架
由于我们采用了对象同操作分开的方式,因此,需要设计一个统一的接口来完成对对象的操纵。为了使用的方便性,我们尽量设计少的接口。我们设计以下三个主要接口:
1、 PersistenceManager:这类完成所有对对象的增加、修改以及删除的操作,并提供简单的查询功能。
2、 Query:这个接口完成对对象的查询功能。
3、 Transaction:这个接口负责处理事务。
另外,为了描述对象的状态,定义了EntityState枚举。为了简单化,这里只定义了四个状态,如下面的定义:
public enum EntityState{Transient,New,Persistent,Deleted} |
对上面几个接口的说明分别如下:
? PersistenceManager
这个接口的定义如下:
public enum PersistOptions{SelfOnly,IncludeChildren,IncludeReference,Full} public interface PersistenceManager : IDisposable { void Close(); bool IsClosed{get;} Transaction CurrentTransaction{ get;} bool IgnoreCache{get;set;} void PersistNew(object entity); void PersistNew(object entity,PersistOptions options); void Persist(object entity); void Persist(object entity,PersistOptions options); void Persist(object entity,params string[] properties); void Persist(object entity,PersistOptions options,params string[] properties); void Delete(object entity); void Delete(object entity,PersistOptions options); void Attach(object entity); void Attach(object entity,PersistOptions options); void Reload(object entity); void Reload(object entity,PersistOptions options); void Evict (object entity); void EvictAll (object[] pcs); void EvictAll (ICollection pcs); void EvictAll (); object FindByPrimaryKey(Type entityType,object id); object FindByPrimaryKey(Type entityType,object id,PersistOptions options); T FindByPrimaryKey<T>(object id); T FindByPrimaryKey<T>(object id, PersistOptions options); object GetReference(object entity); object GetReference(object entity,Type[] parents); object GetChildren(object entity); object GetChildren(object entity,Type[] children); EntityState GetState(object entity); ICollection GetManagedEntities(); bool Flush(); Query NewQuery(); Query NewQuery(Type entityType); Query NewQuery(Type entityType,string filter); Query NewQuery(Type entityType,string filter,QueryParameterCollection paramColletion); Query<T> NewQuery<T>(); Query<T> NewQuery<T>(string filter); Query<T> NewQuery<T>(string filter, QueryParameterCollection paramColletion); } |
对于这个接口的几个主要方法说明如下:
PersistNew方法将一个新的实体对象转换成可持续对象,这个对象在事务结束的时候,会被Insert到数据库中。调用这个方法后,该对象的状态为EntityState.New。如果一个对象的状态为EntityState.Persistent,那么,这个方法将抛出一个EntityIsPersistentException异常。
Persist方法将一个实体对象保存到数据库中。如果一个对象是Trasient的,则将其转换为EntityState.New状态。在事务结束的时候,会被Insert到数据库中;否则,其状态就是EntityState.Persist,就更新到数据库中。如果一个Trasient对象实际上已经存在于数据库中,由于Persist方法并不检查实际的数据库,因此,调用这个方法,将会抛出异常。这个时候,应该使用先使用Attach方法,然后调用Persist。Persist方法主要用于已受管的对象的更新。
Delete方法删除一个对象。一个对象被删除后,其状态变成EntityState.Deleted,在事务结束的时候,会被从数据库中删除。 如果一个对象不是持久的,那么,这个方法将抛出异常。
Attach方法将一个对象标记为可持续的。如果这个对象已经存在于实际的数据库中,那么,这个对象的状态就是EntityState.Persistent,否则,这个对象的状态就是EntityState.New。
Reload方法重新从数据库中载入这个对象,这意味着重新给对象的各个属性赋值。
Evict方法从缓存中把某个对象移除。
FindByPrimaryKey方法根据主键查找某个对象,如果主键是多个字段的,主键必须是PrimaryKey数组,否则抛出异常。
? Query:
这个接口的定义如下:
public interface Query { Type EntityType{get;set;} string EntityTypeName{get;set;} string Filter{get;set;} QueryParameterCollection Parameters{get;set;} string Ordering{get;set;} bool IgnoreCache{get;set;} PersistOptions Options { get;set;} ICollection QueryObjects(); DataSet QueryDataSet(); object GetChildren(object entity); object GetChildren(DataSet dst); object GetChildren(object entity, Type[] children); object GetChildren(DataSet entity, Type[] children); object GetReference(object entity); object GetReference(DataSet entity); object GetReference(object entity, Type[] parents); object GetReference(DataSet entity, Type[] parents); PersistenceManager PersistenceManager{get;} bool IsClosed{get;} void Close (); void Open(); } public interface Query<T> : Query { new ICollection<T> QueryObjects(); } |
Query接口的主要使用方法,是设定需要查询的对象的类型,以及过滤条件,然后执行QueryObjects方法,就可以得到相应的复合条件的对象。
? Transaction
这个接口主要用于处理事务,提供的功能比较简单,包括事务的开始、提交以及回滚三个主要功能。这个接口的定义如下:
public interface Transaction { void Begin(); void Commit(); void Rollback(); PersistenceManager PersistenceManager{get;} } |
定义好了接口,下面准备实现。这将在下面的小节中描述。
下面的例子展示了一个利用Websharp框架保存一个Order对象的过程:
DatabaseProperty dbp = new DatabaseProperty(); dbp.DatabaseType = DatabaseType.MSSQLServer; dbp.ConnectionString = "Server=127.0.0.1;UID=sa;PWD=sa;Database=WebsharpTest;"; PersistenceManager pm = PersistenceManagerFactory.Instance().Create(dbp); Order o = new Order(); o.OrderType = new OrderType(3, "音响"); o.OrderID = 3; o.Title = "SecondOrder"; o.IsDigned = false; o.AddTime = DateTime.Now; o.Details = new List<OrderDetail>(2); for (int j = 1; j < 3; j++) { OrderDetail od= new OrderDetail(); od.OrderID = 3; od.ProductID = j; od.Amount = j; o.Details.Add(od); } pm.PersistNew(o, PersistOptions.IncludeChildren); pm.Flush(); pm.Close(); |
5.5实现对象操纵框架
前面,我们已经定义好了O/R Mapping的基本框架,下面,我们来具体讨论实现这个框架需要的一些主要工作。
在实现中,以下几个方面是比较主要的:
? MetaData
? StateManager
? SqlGenerator
? IEntityOperator
MetaData用来记录对象和数据库之间映射的元数据信息,包括两个部分的元数据:
1、 对象和表映射的信息
2、 对象属性和字段映射的信息。
关于MetaData的定义可以参见Websharp的源代码。
MetaData数据,由专门的类来进行解析,并进行缓存处理。在Websharp中,由MetaDataManager来完成这个任务。MetaDataManager通过反射,读取实体类的信息,来得到MetaData数据。下面的代码片断演示了这个过程的主要内容。
首先,ParseMetaData方法读取类的信息:
private static MetaData ParseMetaData(Type t, DatabaseType dbType) { MetaData m = new MetaData(); m.ClassName = t.Name; //类名 m.EntityType = t; //实体类的类型 m.MapTable = GetMappedTableName(t); //实体类映射的表 m.PrimaryKeys = GetKeyColumns(t); //主键 if (m.PrimaryKeys.Length > 1) m.IsMultiPrimaryKey = true; else m.IsMultiPrimaryKey = false; …… } |
然后,读取每个字段的信息。这个部分的代码比较长,下面,只列出部分代码:
PropertyInfo[] pinfos = t.GetProperties(); m.FieldMetaDatas = new Dictionary<string,FieldMetadata>(pinfos.Length); foreach (PropertyInfo pinfo in pinfos) { FieldMetadata fd = new FieldMetadata(); fd.PropertyName = pinfo.Name; ColumnMapAttribute cattr = Attribute.GetCustomAttribute(pinfo, typeof(ColumnMapAttribute)) as ColumnMapAttribute; if(!Object.Equals(null,cattr)) { fd.ColumnName = cattr.ColumnName; fd.DbType = cattr.DbType; fd.DefaultValue = cattr.DefaultValue; } else { fd.ColumnName = fd.PropertyName ; fd.DbType = DbType.String; fd.DefaultValue = String.Empty; } …… } |
最后,根据映射信息,构建同数据库进行交互时候的SQL语句。O/R Mapping框架的最后操作,还是回归到根据SQL语句来进行对数据库的操作。
SqlGenerator sg = SqlGenerator.Instance(dbType); m.SelectSql = sg.GenerateSql(t, OperationType.SelectByKey); |
SQL语句的具体构建,由SqlGenerator来完成。
SqlGenerator是一个抽象类,定义了构建同数据库进行交互的方法接口。在这个抽象类的下面,根据不同的数据库,扩展出针对不同数据库的SQL语句生成器。例如,一个SQL Server的SqlGenerator可以这样来生成插入一条记录需要的SQL语句:
private SqlStruct GenerateInsertSql(Type entityType) { string autoP; bool autoInscrease = MetaDataManager.GetAutoInscreaseProperty(entityType, out autoP); List<string> lcolumns = MetaDataManager.GetDbColumns(entityType); string[] parameters = new string[lcolumns.Count]; ParamField[] paramField = new ParamField[lcolumns.Count]; if (autoInscrease) { lcolumns.Remove(autoP); } string[] columns = lcolumns.ToArray(); for (int i = 0; i < columns.Length; i++) { parameters[i] = "@" + columns[i]; paramField[i] = new ParamField(parameters[i], columns[i]); } if (autoInscrease) { parameters[parameters.Length-1] = "@" + autoP; paramField[parameters.Length-1] = new ParamField(parameters[parameters.Length - 1], autoP); } string tableName = MetaDataManager.GetMappedTableName(entityType); StringBuilder strSql = new StringBuilder("INSERT INTO ").Append(tableName).Append("(").Append(string.Join(",", columns)).Append(") VALUES(").Append(string.Join(",", parameters)).Append(")"); if (autoInscrease) { strSql.Append(";SELECT @").Append(autoP).Append("=@@IDENTITY"); } return new SqlStruct(strSql.ToString(),paramField); } |
前面的章节讨论过对象的状态问题。在Websharp中,因为采用了普通的类就可以持久化的操作的方式,因此,需要另外的机制来管理对象的状态。
在Websharp中,为了简化,只定义了四种对象的状态,分别是Transient,New,Persistent和Deleted,定义如下:
public enum EntityState{Transient,New,Persistent,Deleted} |
在实现中,定义了StateManager类来管理对象的状态,这个类的定义如下:
public class StateManager { public StateManager(object entity) { this.m_Entity = entity; } public StateManager(object entity,EntityState state) { this.m_Entity = entity; this.m_State = state; } private object m_Entity; public object Entity { get { return m_Entity; } set { m_Entity = value; } } private EntityState m_State; public EntityState State { get { return m_State; } set { m_State = value; } } } |
在PersistenceManager里面,持久化一个对象的时候,如果这个对象不是受管理的,则PersistenceManager会给这个对象分配一个StateManager。例如,当对一个对象执行PersistNew操作的时候,PersistenceManager将首先检查这个对象是否是受管理的,如果不是,则为这个对象分配一个StateManager,并且其状态为EntityState.New,然后,将这个对象添加到待操作列表中,在执行Flush方法的时候,会对这个对象执行一个新增的操作。代码如下:
public void PersistNew(object entity, PersistOptions options) { //首先,检查这个对象是否已经是受管理的对象 StateManager smanager; if (IsManagedBySelf(entity,out smanager)) { throw new EntityIsPersistentException(); } //将对象标记为受管理的,并且状态是EntityState.New smanager = new StateManager(entity,EntityState.New); stateManagers.Add(smanager); //添加到操作列表中 opEntityList.Add(new OperationInfo(smanager,options)); } |
最后,在执行Flush方法的时候,PersistenceManager会把所有的对象的变化反应到数据库中。
foreach (OperationInfo opInfo in opEntityList) { IEntityOperator op = EntityOperatorFactory.CreateEntityOperator(dao, opInfo.StateManager.State); op.Execute(opInfo.StateManager.Entity, dao); CacheProxy.CacheEntity(opInfo.StateManager.Entity); } |
可以看到,具体的对数据库的操作,通过IEntityOperator接口来完成。IEntityOperator接口定义了执行某个具体的对象同数据库进行交互的接口,在这个接口的下面,扩展出针对各个数据库的具体的实现类。这个部分的结构可以用下面的图来表示: