对象关系映射概念性示例(怀疑ORM者请进)
***引言***
过去我一直怀疑ORM,感觉它充其量可以用在小型的、具有简单业务模型和数据关系的项目中。复杂的项目,即使我们用尽了各种复杂的SQL“技巧”,还嫌不够,ORM再聪明,怎么可以生成那么复杂,有时简直是任意复杂的SQL语句呢?
直到有一天,我妻子无意中告诉我:织一件带花样图案的毛衣,其实只需要两根竹钎和一条毛线,织女甚至可以心不在焉地织!我终于恍然大悟,我们用惯了各种复杂SQL语句和DBMS提供的专有技术,是因为我们的业务模型和业务逻辑的设计实在太差,甚至没有设计!我们是被迫用千百根针和千百条线痛苦地编制只有我们自己才能理解的图案。
—— ORM不但适用于小项目,更适用于大项目。越是复杂的项目,受益越明显。
***业务对象模型***
我们将采用一个定单的数据模型。这个数据模型其实很复杂,涉及若干个业务对象(类和相应的数据库表),至少应该有5个:
1. 定单(Orders)
2. 定单明细(Order Details)
3. 厂家(Manufacturers)
4. 客户(Customers)
5. 产品(Products)
这5个业务对象以及它们之间的关系分别用下面5个C#类来大致表示:
[Table(”TBL_MANUFACTURER”)]
public class ManufacturerClass //厂家
{
[Column(”ID”)] public string ID;
[Column(”NAME”)] public string Name;
[Column(”CITY”)] public string City;
[Column(”ADDRESS”)] public string Address;
[Column(”BANK_AACOUNT”)] public string BankAccount;
[Column(”TAX_ACCOUNT”)] public string TaxAccount;
......
}
[Table(”TBL_CUSTOMER”)]
public class CustomerClass // 客户
{
[Column(”ID”)] public string ID;
[Column(”NAME”)] public string Name;
[Column(”CITY”)] public string City;
[Column(”ADDRESS”)] public string Address;
[Column(”BANK_AACOUNT”)] public string BankAccount;
[Column(”TAX_ACCOUNT”)] public string TaxAccount;
......
}
[Table(”TBL_PRODUCT”)]
public class ProductClass // 产品
{
[Column(”ID”)] public string ID;
[Column(”NAME”)] public string Name;
[Column(”PRICE”)]decimal Price;
[RELATION(KeyColumn=”MANUFACTURER_ID”)]
public ManufacturerClass Manufacturer; //厂家
......
}
[Table(”TBL_ORDER”)]
public class OrderClass // 定单
{
[Column(”ID”)] public string ID;
[Column(”DATE”)] datetime string Date;
[Column(”DELIVERY_ADDRESS”)] public string DelieveryAddress;
[Relation(KeyColumn=”CUSTOMER_ID”)] public CustomerClass Customer; //客户
[Relation(KeyColumn=”ORDER_ID”, IsDetail=true)]
public OrderDetailClass[] Details; //定单明细,数组,一对多主从关系
......
}
[Table(”TBL_ORDERDETAIL”)]
public class OrderDetailClass //定单明细
{
[Relation(KeyColumn=”ORDER_ID”)] public OrderClass Order;// 定单对象
[Relation(KeyColumn=”PRODUCT_ID”)] public ProductClass Product; //产品
[Column(“QUANTITY”)] public int Quantity;
[Column(“PRICE”)] public decimal Price;
/////////////////
public decimal ItemTotalPrice {get return (Quantity * Price)} //非映射属性
......
}
我们使用了Table/Column/Relation三个Attributes分别来作表映射、列映射和关系映射的元数据,三个Attribute的语义和语法一目了然,稍微值得注意是对主从关系(OrderClass.Details属性)的映射。
***CRUD操作***
下面展示如何通过ORM对上面的数据模型进行CRUD四项基本操作:
[1] 添加定单:
获得ORM的入口对象
ORMContext ormContext =ORMContext.GetContext(…);
建立定单对象
OrderClass order = new OrderClass();
order.ID = "0001 ";
order.Date = ......;
order.Customer = ormContext.Load <CustomerClass> ( "033232 "); //通过客户代码获得一个
order.DelieveryAddress = "北京市黄浦区…… ";
客户对象
建立一个定单明细对象
OrderDetailClass orderItem = new OrderDetailClass()
orderItem.Product = ormContext.Load <ProductClass> ( "020021 "); //通过产品代码获得一个产品对象
orderItem.Quantity = 100;
orderItem.Price = orderItem.Product.Price; //注意这一句
order.Details.Add(orderItem);
建立更多的定单明细对象
.......
保存定单,完成操作
ormContext.Save <OrderClass> (orderItem);
[2] 读取定单
OrderClass order = ormContext.Load( "0001 ");
然后,我们可以访问各种来自上面5个类(表)的信息了,例如:
1) 定单的客户的银行帐号:
string bankAccount = order.Customer.BankAccount;
2) 定单中的第1个产品的厂家的地址
string ManufacturerAddress = order.Details[0].Manufacturer.Address;
[3] 修改定单
先加载一个定单:
OrderClass order = ormContext.Load( "0001 ");
我们要修改定单的发货地址,并且需要删除第六个产品:
order.DelieveryAddress = "北京市朝阳区…… ";
order.Details.Delete(order.Details[5]);
保存一下,就完成了修改:
ormContext.Save <OrderClass> (orderItem);
[4] 删除定单
一个语句,就把定单和明细全部删除了
ormContext.Delete <OrderClass> (orderItem);
***对象查询***
下面进一步说明如何通过对象查询语言(Object Query Language, OQL)实现典型的二维报表。
例如我们要查询2005年第一季度来自“北京”的客户订购某一编号为“20039023”的厂家的产品的情况。
先定义一个类来组织报表数据集合:
public class OrderReportLine
{
string OrderID, //定单号
datetime Date, //日期
string CustomerName, //客户名称
string CustomerAddress, //客户地址
int Quantity, //数量
decimal Price //单价
}
准备一个OQL查询语句:
string queryText = @”
select Order.ID, Order.Date, Order.Customer.Name,
Order.Customer.Address, Order.Details.Quantity, Order.Details.Price
from Order
where Order.Date > =’20050101’ and Order.Date <’20050401’ and Order.Customer.City = ‘Beijing’ and Order.Details.Product.Manufacturer.ID = ‘20039023’”;
执行查询,获得结果:
OrderReportLine[] orderReport
= ormContext.ExecuteQuery <OrderReportLine> (queryText);
查询结果可以通过数组orderReport进行访问。
注意几点:
1. OQL的基本思想和语法与SQL非常相似,但也有本质区别。OQL的操作对象是类而不是表。from子句后面的Order是c#类名,不是表名。select和where子句里引用的各种名字也是类象名或属性名,不是表名、表别名或列名。另外虽然本例中select子句所列为标量数值,OQL也可以返回整个对象。
2. from子句没有使用join操作,所有的数据都是来自Order对象,和前面CRUD操作 的情形相似,Order类管理了5个业务对象类(表)之间的关系。
注:
文中使用的语法只是概念性的,并不严格符合某种特定ORM工具的具体语法。但各种ORM工具的基本思想都大致和本文所讨论的类似。
本文作者 夏成斌 .NET软件工程师 13764438712
[解决办法]
谢谢,frank_lee_cn
[解决办法]
和我开发的很类似.
不过我的OQL要这样写:select [ID], [Date], [Name] from [Order]
where [Date] > =’20050101’”;也就是说写实体对象的属性名称,并要加 "[] ",
否则会当成标准的SQL来执行.如果要写标准SQL,那就要注意了.因为我的ORM支持多种数据库库.
实体对象通过生成器来生成的代码:
using System;
using System.Collections.Generic;
using System.Text;
using Echo.EnterpriseLibrary.ORM;
namespace Echo.EnterpriseLibrary.Log.Entity
{
/// <summary>
/// 表 Biz_Log 的实体对象; 公司:杭州爱科电脑技术有限公司,作者:李中华,日期:07-05-15
/// </summary>
[Table(Biz_Log.EntityInfo.Schema, Biz_Log.EntityInfo.EntityName)]
public class Biz_Log
{
/// <summary>
/// 实体信息
/// </summary>
public struct EntityInfo
{
public const string Schema = "dbo ";
public const string EntityName = "Biz_Log ";
}
/// <summary>
/// 实体行信息
/// </summary>
public struct EntityColumnsInfo
{
public const string LogID = "LogID ";
public const string Author = "Author ";
public const string logTitle = "logTitle ";
public const string OpObjID = "OpObjID ";
public const string LogContent = "LogContent ";
public const string EditDateTime = "EditDateTime ";
public const string State = "State ";
public const string Memo = "Memo ";
}
private Guid _LogID;
private string _Author;
private string _logTitle;
private string _OpObjID;
private string _LogContent;
private DateTime? _EditDateTime;
private int? _State;
private string _Memo;
[Primary(true, EntityPrimaryInfo.LogID)]
public Guid LogID
{
get { return _LogID; }
set { _LogID = value; }
}
[Column(EntityColumnsInfo.Author, true)]
public string Author
{
get { return _Author; }
set { _Author = value; }
}
[Column(EntityColumnsInfo.logTitle, true)]
public string logTitle
{
get { return _logTitle; }
set { _logTitle = value; }
}
[Column(EntityColumnsInfo.OpObjID, true)]
public string OpObjID
{
get { return _OpObjID; }
set { _OpObjID = value; }
}
[Column(EntityColumnsInfo.LogContent, true)]
public string LogContent
{
get { return _LogContent; }
set { _LogContent = value; }
}
[Column(EntityColumnsInfo.EditDateTime, true)]
public DateTime? EditDateTime
{
get { return _EditDateTime; }
set { _EditDateTime = value; }
}
[Column(EntityColumnsInfo.State, true)]
public int? State
{
get { return _State; }
set { _State = value; }
}
[Column(EntityColumnsInfo.Memo, true)]
public string Memo
{
get { return _Memo; }
set { _Memo = value; }
}
/// <summary>
/// 表 Biz_Log 的实体对象; 公司:杭州爱科电脑技术有限公司,作者:李中华,日期:07-05-15
/// </summary>
public Biz_Log()
{
_LogID = Guid.Empty;
_Author = null;
_logTitle = null;
_OpObjID = " ";
_LogContent = null;
_EditDateTime = null;
_State = null;
_Memo = null;
}
}
}
操作的方法主要有
Add(object);
Edit(object);
Delete(object);
Query(object);
Query <T> (string)// " select [LogID] from [Biz_Log] where .... " 或者 标准的SQL语法.
还有类似与System.Data.SqlClinet.SqlCommand.Exec*()的几个方法.
另外有实体对象与DataTable的转换方法,方便程序员使用.
里面用到了反射,对实体类型的反射会缓存起来,这样就提升了性能.使用这ORM,只是在系统第一次运行的时候会稍微慢一点.对于很多系统,这一点点性能是可以忽略不记得.
现在正在加入IChangeTracking ,IListSource 的功能,实现从DataGrid内直接提交对实体内容的修改.
并计划把一些内容用XML来配置,降低数据库与C#代码的耦合.
在这个版本从C#1.1的基础上升级到2.0的时候,创建泛型类型的动态代理遇到了问题(ORM和AOP结合起来的系统日志应用组件)
我的联系QQ: 289111533 , 白天不会在线,晚上我一般潜水.