.NET
业务框架开发实战之九 Mapping属性原理和验证规则的实现策略
1. 框架的借鉴
一个框架的产生不是那么简单的,有很多的问题需要Richard去考虑:
避免重新造轮子
借鉴现有的成熟的框架的思想
在开发的过程中,Richard一直使用Visual Studio IDE开发。而且每次随着VS新版本的发布,总是伴随着新技术的产生。很多的时候,开发人员只是关注在新技术的使用和学习上。但是对于新技术,还有另外一方面是很值得关注的:实现的原理,和为什么这样实现,即,思想。新技术,毫无疑问是一些大师们思考的结果,从他们的思想中借鉴,益处是很大的。
在Richard学习的过程中,有一个地方特别引起来他的关注:那就是依赖属性概念的提出,先是WPF,然后在他学习WF的时候,也看到了依赖属性的再次使用。他考虑,把依赖属性的思想使用到自己正在开发的业务框架中来。
首先,他分析了现在的依赖属性的实现方式(以WPF为例),
代码
public class FrameworkElement: UIElement,
...
{
public static readonly DependencyProperty
MarginProperty;
...
}
public Thickness Margin
{
set { SetValue(MarginProperty, value);
}
get { return (Thickness)GetValue(MarginProperty);
}
}
static FrameworkElement()
{
FrameworkPropertyMetadata metadata
= new FrameworkPropertyMetadata(
new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);
MarginProperty = DependencyProperty.Register("Margin",
typeof(Thickness), typeof(FrameworkElement),
metadata,
new ValidateValueCallback(FrameworkElement.IsMarginValid));
...
}
在Richard一直认为,一个属性的声明是很简单的,而且长期以来Richard都一直使用下面的方式:
public Thickness Margin
{
get;set;
}
比较而言,依赖属性最大的好处就于:给普通的属性提供更加多的信息,而且提供了更多的功能:验证,触发回调事件。当然,使用普通的属性也能达到:
public Thickness Margin
{
get{return margin;}
set
{
if(margin<0)
...
...
}
}
相比而言,依赖的属性的方式更加优雅,而且扩展性也好。
还有一点比较重要的就是,一旦把一个属性变为依赖属性,那么.NET Framework就开始管理这个属性,如自动的验证,值的改变和跟踪。这样就把任务交给了Framework,开发人员做事情就比较方便了。
Richard想起了之前在开发业务类中遇到的问题:例如下面的代码:
public class Product
{
public string ProductName{get;set;}
public double Price{get;set;}
}
很多的时候,在增加或者更新一个Product的时候,由于逻辑的需要,往往要判断ProductName不为空,而且Price要大于零等。所以每次都需要写代码判断:
public void Add()
{
if(string.IsNullOrEmpty(this.ProductName){...}
if(this.Price<0){...}
}
问题还不止这些,如果在其他的业务类中也需要同样,而且类似的验证,那只有一行行的写类似的代码,最好的情况就是copy一些代码。
这样写代码确实很累,后面Richard也想用一些方式来改进,用到了Enterprise
Library中的Validation验证模块,于是代码就变成了下面的样子:
public class Product
{
[NotNullValidator]
public string ProductName{get;set;}
public double Price{get;set;}
}
使用声明的开发,AOP的思想,其实这样的方式相比之前而言,确实已经很不错了。在把业务类的数据保存的时候只要调用Validation验证模块的Validate()方法就行了。确实很方便,但是存在的问题就是:每次调用Validate()方法时候,就会把这个业务类的所有属性都会检查一遍(那些加了验证标签的属性),这样,性能方面不好,而且还不能针对某一个属性单独的验证。
2. 综合考虑
Richard还考虑到了另外的一点:之前一直在解决mapping的问题,说到底就是把从DAL中拿到的数据赋值给业务类的属性。而且还要基于业务类创建查询对象,最后把查询对象解析为SQL语句,所以还要保存业务属性和DAL中数据实体属性的对应关系,即哪个业务属性对应哪个数据实体属性(也是表字段)。
综合上面的考虑,Richard决定把依赖属性的优势利用起来(自动的验证,数据改变跟踪,另外加上权限的验证),而且给依赖属性更多的元数据信息:把mapping的字段信息保存在依赖属性中。所以,现在属性的声明如下:
public static readonly PropertyInfo<int>
ProductIdProperty = RegisterProperty<Product>(
new PropertyInfo<int>("ProductId",typeof(M_Product)","Id"));
public string ProductId
{
get { return ReadProperty(ProductIdProperty);
}
set { LoadProperty(ProductIdProperty, value); }
}
在上面的属性声明中,就指定从业务类(如,Product)的属性从哪个数据实体(typeof(m_Product))的哪个属性(如,Id)取值。
RegisterProperty就是把属性的信息保存在一个字典中:
Dictionary<Type,List< IPropertyInfo>>
其中PropertyInfo继承了IPropertyInfo接口。
最后的结果就是:所有业务类的mapping属性都被保存在了一个全局的静态字典中。
另外还有一个全局的静态字典用来保存每个属性所对应的验证规则:
Dictionary< IPropertyInfo,List<ICheckRule>>
所有的验证规则都是从ICheckRule接口继承。
一个比较强大的属性就产生了。当然,在mapping属性中的验证只是基本的验证,还有更加复杂的业务验证将会放在其他的地方,实现方式或者类似WPF那么:采用回调,如new
ValidateValueCallback(FrameworkElement.IsMarginValid)。
所以,借鉴于mapping属性就解决了三个问题:
mapping和查询对象的实现
部分验证规则的声明
业务属性的管理
.NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇)
1. 打通业务层和数据层
首先,回顾之前的文章一直讨论的问题:
1. 如何使得数据层”以不变应万变”。
2. 条件对象如何实现
3. 如何在业务层和数据层之间mapping数据
本篇就告诉大家,如何切切实实的解决上面三个问题。
首先,从一个图示开始讲述。
从上面的图中可以看出,架起在BLL和DAL之前的桥梁的就是中间的那个条件对象。正是因为有了这个,所以上面的提出的问题才得以解决。
下面先从操作上总体来讲述一下这个图的具体流程:
a. 在业务类中创建一个方法。例如在业务类User中,定义如下:
现在只看GetUserByAge这个方法:在方法中构造出一个条件对象,大家第一眼能看出来:是Linq的实现。其实最后的实现只是借用了Linq的思想,仅此而已。
b. 解析条件对象。在上面的构造的条件对象中,Age,Name等,都是业务类User的字段,这些字段的值肯定最终是从数据库中的表字段中获取的(或者是通过数据库中的值算出来的),所以在解析条件对象的时候,就要知道这些业务属性对应数据库中哪个表的哪个字段。因此在业务类中声明每个属性的时候就要同时保留它所对应的数据库字段的信息。一旦业务类的属性中保存了这些信息
c. 数据层操作SQL语句。在解析条件对象的时候,就会最终得到相对应的SQL语句,然后在数据层中执行这些SQL语句。
可能上面讲的比较的抽象,因为本篇的要讲述的东西确实比较的多(Word中写了超过了10页),上面讲述的三个步骤也是先按大家有个印象。不是很懂也没有关系。
2. 打通方法的选择和实现
接下来就是方法的探索和思考,以及实现的过程。我是想带着个大家跟着一起看看,为什么最后会采用这个解决方案的。
首先,就从条件对象开始看起。
在实现条件对象(条件对象和查询对象的区别之前讲过,这里重述一下:查询对象只是条件对象的一个子集,查询对象用来在查询的使用构造查询条件;条件对象不仅仅在查询时构造条件,而且在增加,删除,,修改时候也使用,例如:只修改Name=”admin”的数据,在修改数据库的时候也用了一定的条件。所以条件对象>查询对象)的时候,也是参看了其他开源框架的一些实现(Nhibernate中查询对象的,CSLA)。
同时要明白一点:设计出来的框架是给开发人员使用的,所以要考虑如何使得开发人员最快最好的使用框架,所以要从开发人员的角度看(这一点也是很重要的)。例如在Nhibernate中查询对象,使用的方法如下(仅仅是简单的举例而已):
IListCustomer> customers
= _session.CreateCriteria(typeof(Customer))
.Add(Restrictions.Like("Firstname", "Xiaoyang%"))
.Add(Restrictions.Between("Lastname", "A%",
"Y%"))
.ListCustomer>();
其实本系列中的业务框架之前的条件对象的构造也是参看了Nhibernate中查询对象的方法来实现和使用的,如下:
ICriteria condition=CriteriaFactory.Create(typeof(ProductBL).Where("ProductName",
Operation.Equal,"book");
因为现在.NET中的开发人员对Linq的一些操作比较的熟悉,而且如果把条件对象的使用方式改为下面的方式:
ICriteria<ProductBL> condition=CriteriaFactory.Create<ProductBL>(o
=> o.ProductName == "book");
那么开发人员的学习成本就几乎为零(因为他们熟悉Linq,如果条件对象也采用这种比较统一的方法实现,他们就可以采用”以此类推”的思想来使用框架),更多的好处我就不说了,大家可以自己体会或者参看本系列之前的文章。
接下来就探索实现条件对象的方法(Linq to XXX篇)。
熟悉Linq的朋友可以看出:可以使条件对象实现IQueryable接口,然后采用实现linq
to XXX的方法,实现自己的Linq Provider。这个方法是否可行,下面,我们就深入linq to
XXX的来看一看,看完之后,结果就很清楚了。
首先来看下面两个接口:
public interface IQueryable : IEnumerable
{
Type ElementType { get; }
Expression Expression { get; }
IQueryProvider Provider { get; }
}
public interface IQueryable<T>
: IEnumerable<T>, IQueryable, IEnumerable {}
public interface IQueryProvider {
IQueryable CreateQuery(Expression expression);
IQueryable<TElement> CreateQuery<TElement>(Expression
expression);
object Execute(Expression expression);
TResult Execute<TResult>(Expression
expression);
}
Linq的出现,使得很多东西都和linq扯上了关系:Linq to sql,
Linq to Google, Linq to javascript, Linq to Nhibernate......
所列出来的这些,我们统称为linq to XXX。这些Linq to XXX都是实现了上面两个接口。
下面就通过自己实现linq to sql来举例分析。
从总体来看:linq to sql的本质就是:把操作转换为sql语句,然后用ADO.NET执行,最后把结果转换为实体返回。
其实下面列出了这么多的代码,其中最关键的其实就是QueryProvider中的Execute方法:这个方法负责把你的操作进行解析,其实真正负责解析的QueryTranslator。
代码
public class Query<T>
: IQueryable<T>, IQueryable, IEnumerable<T>,
IEnumerable, IOrderedQueryable<T>, IOrderedQueryable
{
QueryProvider provider;
Expression expression;
public Query(QueryProvider provider) {
if (provider == null) {
throw new ArgumentNullException("provider");
}
this.provider = provider;
this.expression = Expression.Constant(this);
}
public Query(QueryProvider provider, Expression expression)
{
if (provider == null) {
throw new ArgumentNullException("provider");
}
if (expression == null) {
throw new ArgumentNullException("expression");
}
if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type))
{
throw new ArgumentOutOfRangeException("expression");
}
this.provider = provider;
this.expression = expression;
}
Expression IQueryable.Expression {
get { return this.expression; }
}
Type IQueryable.ElementType {
get { return typeof(T); }
}
IQueryProvider IQueryable.Provider {
get { return this.provider; }
}
public IEnumerator<T> GetEnumerator() {
return ((IEnumerable<T>)this.provider.Execute(this.expression)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return ((IEnumerable)this.provider.Execute(this.expression)).GetEnumerator();
}
public override string ToString() {
return this.provider.GetQueryText(this.expression);
}
}public abstract class QueryProvider : IQueryProvider
{
protected QueryProvider() {
}
IQueryable<S> IQueryProvider.CreateQuery<S>(Expression
expression) {
return new Query<S>(this, expression);
}
IQueryable IQueryProvider.CreateQuery(Expression expression)
{
Type elementType = TypeSystem.GetElementType(expression.Type);
try {
return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType),
new object[] { this, expression });
}
catch (TargetInvocationException tie) {
throw tie.InnerException;
}
}
S IQueryProvider.Execute<S>(Expression expression)
{
return (S)this.Execute(expression);
}
object IQueryProvider.Execute(Expression expression)
{
return this.Execute(expression);
}
public abstract string GetQueryText(Expression expression);
public abstract object Execute(Expression expression);
}internal class QueryTranslator : ExpressionVisitor
{
StringBuilder sb;
internal QueryTranslator() {
}
internal string Translate(Expression expression) {
this.sb = new StringBuilder();
this.Visit(expression);
return this.sb.ToString();
}
private static Expression StripQuotes(Expression e)
{
while (e.NodeType == ExpressionType.Quote) {
e = ((UnaryExpression)e).Operand;
}
return e;
}
protected override Expression VisitMethodCall(MethodCallExpression
m) {
if (m.Method.DeclaringType == typeof(Queryable) &&
m.Method.Name == "Where") {
sb.Append("SELECT * FROM (");
this.Visit(m.Arguments[0]);
sb.Append(") AS T WHERE ");
LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
this.Visit(lambda.Body);
return m;
}
throw new NotSupportedException(string.Format("The
method '{0}' is not supported", m.Method.Name));
}
protected override Expression VisitUnary(UnaryExpression
u) {
switch (u.NodeType) {
case ExpressionType.Not:
sb.Append(" NOT ");
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format("The
unary operator '{0}' is not supported", u.NodeType));
}
return u;
}
protected override Expression VisitBinary(BinaryExpression
b) {
sb.Append("(");
this.Visit(b.Left);
switch (b.NodeType) {
case ExpressionType.And:
sb.Append(" AND ");
break;
case ExpressionType.Or:
sb.Append(" OR");
break;
case ExpressionType.Equal:
sb.Append(" = ");
break;
case ExpressionType.NotEqual:
sb.Append(" <> ");
break;
case ExpressionType.LessThan:
sb.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format("The
binary operator '{0}' is not supported", b.NodeType));
}
this.Visit(b.Right);
sb.Append(")");
return b;
}
protected override Expression VisitConstant(ConstantExpression
c) {
IQueryable q = c.Value as IQueryable;
if (q != null) {
// assume constant nodes w/ IQueryables are table references
sb.Append("SELECT * FROM ");
sb.Append(q.ElementType.Name);
}
else if (c.Value == null) {
sb.Append("NULL");
}
else {
switch (Type.GetTypeCode(c.Value.GetType())) {
case TypeCode.Boolean:
sb.Append(((bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format("The
constant for '{0}' is not supported", c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}
protected override Expression VisitMemberAccess(MemberExpression
m) {
if (m.Expression != null && m.Expression.NodeType
== ExpressionType.Parameter) {
sb.Append(m.Member.Name);
return m;
}
throw new NotSupportedException(string.Format("The
member '{0}' is not supported", m.Member.Name));
}
}
对于实现了IQueryable的对象,在他们进行的每一个操作(也就是调用实现这个接口的类上的方法)其实都没有立刻去执行,而且把进行的操作记录下来了,放在一个称为Expression
Tree表达式数的数据结构中,然后再真正执行的时候(就是调用Execute来执行), QueryTranslator对象就遍历表达式树,对操作进行解析,例如linq
to sql就是把表达式树中操作解析为对数据库进行的操作,以sql语句的形式体现出来.
当把操作解析为了sql语句之后,就是用ADO.NET的方法来执行SQL操作,然后通过反射,把ADO.NET执行的结果转换为数据实体。如下:
internal class ObjectReader<T>
: IEnumerable<T>, IEnumerable where T : class,
new() {
Enumerator enumerator;
internal ObjectReader(DbDataReader
reader) {
this.enumerator = new Enumerator(reader);
}
public IEnumerator<T> GetEnumerator()
{
Enumerator e = this.enumerator;
if (e == null) {
throw new InvalidOperationException("Cannot enumerate
more than once");
}
this.enumerator = null;
return e;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
class Enumerator : IEnumerator<T>,
IEnumerator, IDisposable {
DbDataReader reader;
FieldInfo[] fields;
int[] fieldLookup;
T current;
internal Enumerator(DbDataReader
reader) {
this.reader = reader;
this.fields = typeof(T).GetFields();
}
public T Current {
get { return this.current; }
}
object IEnumerator.Current {
get { return this.current; }
}
public bool MoveNext() {
if (this.reader.Read()) {
if (this.fieldLookup == null) {
this.InitFieldLookup();
}
T instance = new T();
for (int i = 0, n = this.fields.Length; i < n;
i++) {
int index = this.fieldLookup[i];
if (index >= 0) {
FieldInfo fi = this.fields[i];
if (this.reader.IsDBNull(index)) {
fi.SetValue(instance, null);
}
else {
fi.SetValue(instance, this.reader.GetValue(index));
}
}
}
this.current = instance;
return true;
}
return false;
}
public void Reset() {
}
public void Dispose() {
this.reader.Dispose();
}
private void InitFieldLookup() {
Dictionary<string, int> map = new Dictionary<string,
int>(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0, n = this.reader.FieldCount; i <
n; i++) {
map.Add(this.reader.GetName(i), i);
}
this.fieldLookup = new int[this.fields.Length];
for (int i = 0, n = this.fields.Length; i < n;
i++) {
int index;
if (map.TryGetValue(this.fields[i].Name, out index))
{
this.fieldLookup[i] = index;
}
else {
this.fieldLookup[i] = -1;
}
}
}
}
}
上面简单的介绍了如何实现linq to sql具体的实现代码,大家可以自己过后慢慢的看或者参看我的另外的一个linq系列,现在我们继续后面的话题。
现在我们回到之前的话题:条件对象是否可以采用这种方式实现?
大家看到上面的代码Query<T>其中的T,和Execute方法的返回值。在上面的代码中,如果T是User类,即,Query<User>,那么最后的Execute方法返回的一定会是User的集合。也就是说:Execute方法已经对数据源(这里是数据库)进行了操作,并且把结果以数据实体的形式已经返回了,并且返回的数据实体的类型就是T,例如User。
但是我们这里的要实现的条件对象只是想把条件构造出来,不是立刻去执行。至于具体的执行数据操作者(DataProvider)可以任意选择的:使用ADO.NET方法,还是EF的方法,还是Nhibernate,都是可以配置的。如下:
public List<User> GetUserByAge()
{
ICriteria<User> conditionPerson =
CriteriaFactory.Create<User>().Where(u =>
u.Age < this.Age).OrderBy<string>(u =>
u.Name).Skip(8).Take(8);
return DataPortal.Query(conditionPerson);
}
上面的代码中,Create方法就是实例化一个ICriteria<User>,此时我们想做的仅仅只是一件事:把在
ICriteria<User>上的操作记录下来而已。然后把记录下来的结果解析,解析的最终结果就是一条sql命令,然后再给不同的DataProvider去执行。也就是说,在DataPortal内部可以配置用什么方法来执行数据操作:是直接使用ADO.NET执行sql命令,还是把sql命令给Entity
Framework...通过配置决定。如果ICriteria<T>是从IQueryable接口进行了继承,那么在ICriteria实现这个结果的过程中就必须要去数据库中进行执行,因为Execute方法返回的是T的集合,而不是sql命令(字符串)。
大家可能想到:那就在Execute方法中去实现不同的DataProvider,例如之前的例子在ObjectReader用ADO.NET实现了,那么也可以在ObjectReader中用EF实现数据操作。这个方法确实可以,也很不错。但是这个方法在分布式开发中(特别是在WCF中)有一点的局限性。例如你有一个界面,上面可以有很多的选项,如下:
在服务接口那边,你肯定不想定义N多差不多的接口方法:如
GetUserByName(string username);
GetUserByEmail(string email);
或者
GetUserByCondition(string username,string
password,string email .....);
这样都是很不灵活的,如果User的属性减少了或者增多了,那么如果要在服务器那边暴露的接口的方法也要修改,这样终究是不好。如下采用下面的方法:
GetUserByCondition(Critera condition);
其中,Critera是条件对象。那么我们在客户端就可以任意构造条件对象,这个条件对象就把在它上面进行的操作记录下来,然后统一的交给GetUserByCondition方法去服务器解释并执行。此时,这个条件对象就是在客户端生成的,而且这个条件对象此时是不用去数据库中去执行的。如果条件对象是从IQueryable接口继承的,那么在客户端构造完条件对象之后,就要去数据库中执行了,如果再在ObjectReader搞个分布式调用,难度不说,也很别扭,这不是我们所要的。
所以,综合上面的一些考虑,那么可以确定:条件对象不继承IQueryable接口。但是我们又希望采用类似linq的操作,那么只有自己实现了。
.NET 业务框架开发 第一阶段总结,深入浅出,水到渠成(后篇)
3. 再次借鉴.NET Framework设计思想
自己实现其实不难,关键看怎么做了。在实现的时候,如果自己单独去搞一套方式,如果设计的不好,可能到后来别人不好理解,甚至连自己也忘记当初自己为什么这样设计。所以,要充分的借鉴已有的好的实现思想。分析了IQueryable,其实最大的区别就是,我们不希望去立刻操作数据源,但是在实现IQueryable过程中是操作数据源的。除此之外,如记录对于实现了IQueryable接口的类的上的操作,这是我们需要的,也就是说我们自己的实现的条件对象也要记录对它对象的操作,然后把这些操作在服务器那边解析执行。
所以,条件对象的接口实现如下:
/// <summary>
/// 所有的查询对象都要从这个接口继承
/// </summary>
public interface ICriteria
{
#region Property
Type ObjectType { get; }
ICriteriaProvider Provider { get; }
#endregion
}
代码
/// <summary>
/// 泛型版的条件对象
/// </summary>
/// <typeparam name="TSource"></typeparam>
public interface ICriteria<T> : ICriteria
{
Dictionary<string, List<Expression>> ExpressionDictionary
{ get; }
ICriteria<T> Where(Expression<Func<T,
bool>> predicate);
ICriteria<T> OrderBy<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> OrderByDescending<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> ThenBy<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> ThenByDescending<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> Skip(int count);
ICriteria<T> Take(int count);
ICriteria<T> First();
ICriteria<T> First(Expression<Func<T,
bool>> predicate);
ICriteria<T> Distinct<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> All(Expression<Func<T, bool>>
predicate);
ICriteria<T> Any(Expression<Func<T, bool>>
predicate);
ICriteria<T> GroupBy<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> Max<K>(Expression<Func<T,
K>> predicate);
ICriteria<T> Min<K>(Expression<Func<T,
K>> predicate);
}
public interface ICriteriaProvider
{
object Execute<T>(ICriteria<T> condition);
}
大家可以看到,上面的接口的声明和IQueryable合QueryProvider的声明很相似。
大家看到了,在上面的ICritera接口声明中,有一些方法,如Where,First等等,其实这些方法在IQueryable也是有的,只不过是以扩展方法的形式出现了。我们这里就直接写在这里了,如果以后要加入更多的方法,我们也可以利用扩展方法的方式。
在IQueryable中,使用 Expression Tree表达式树来记录操作的,那么我们这里也采用这种方式。
还有一点比较重要:在上面的自己的实现linq to sql的例子中,或者微软官方实现的linq
to sql中,操作的都是数据实体和ADO.NET对象之间的关系,例如下面的代码:
public bool MoveNext() {
if (this.reader.Read()) {
if (this.fieldLookup == null) {
this.InitFieldLookup();
}
T instance = new T();
for (int i = 0, n = this.fields.Length; i < n;
i++) {
int index = this.fieldLookup[i];
if (index >= 0) {
FieldInfo fi = this.fields[i];
if (this.reader.IsDBNull(index)) {
fi.SetValue(instance, null);
}
else {
fi.SetValue(instance, this.reader.GetValue(index));
}
}
}
this.current = instance;
return true;
}
return false;
}
从代码中可以看出,在解析表达式树后,执行返回的数据实体的属性的名字和查询出的表的字段名字是相同的。
但是:我们在客户端构造出来的条件对象中使用的字段是业务类的属性,业务类中的属性名字和数据库表字段的名字可能不是一样的,甚至有可能业务类中的一个属性的值是几个表中字段计算而来的。如Age>3,其中在Age属性的值,可能来自己数据库中UserDetail表中age字段。所以,对于在业务类上生成的条件对象,通过解析最后要生成对应表的查询,例如在User类上生成的条件对象,最后生成的sql语句要是:select
* from UserDetail where age>3 ,而不是select * from User
where Age>3. 也就是说,User业务类要清楚的知道自己的数据来自哪个数据库表,而且User类中的属性也要清楚的知道自己的数据到底来自数据表中的哪个字段。
下面就来讲述如何解决上面提到的问题。
Mapping属性
public static readonly PropertyInfo<int>
UserIdProperty = RegisterProperty<User>(
new PropertyInfo<int>("UserId",typeof(M_Product)","Id"));
public string UserId
{
get { return ReadProperty(UserIdProperty
); }
set { LoadProperty(UserIdProperty , value); }
}
从代码中可以看出,业务类的属性的声明和之前有点不一样了。可能一直以来,业务类中的属性都是像下面这样声明的:
public string UserId { get; set; }
但是,为了解决上面的问题:即每个属性都要清楚的知道自己对应数据表中的那个字段,那么每个属性就要保存一些数据库中表的字段的信息,这样也便于条件对象最后生成正确的sql语句。
在框架中,有一个接口的声明:
public interface IPropertyInfo
{
/// <summary>
/// Gets the property name value.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the type of the property.
/// </summary>
Type Type { get; }
/// <summary>
/// Gets the friendly display name
/// for the property.
/// </summary>
string FriendlyName { get; }
/// <summary>
/// Mapping data entity type
/// </summary>
Type DataEntityTyoe { get; set; }
/// <summary>
/// Mapping to data entity name
/// </summary>
string DataEntityPropertyName { get; set; }
/// <summary>
/// Gets the default initial value for the property.
/// </summary>
/// <remarks>
/// This value is used to initialize the property's
/// value, and is returned from a property get
/// if the user is not authorized to
/// read the property.
/// </remarks>
object DefaultValue { get; }
}
这个接口就为是为用来描述业务类中每一个属性的信息的,例如,属性对应哪个数据表中哪个字段。
框架中存在一个全局的字典,用来保存所有业务类的一些属性的信息:如下
Dictionary<Type,List<IPropertyInfo>>
propertyInfomationDictionary;
在条件对象中的表达式树遍历解析的时候,就会使用这个字典中保存的信息来生成正确的sql语句。看看下面条件对象解释器的代码:大家只看Translate方法就行了。
public class CriteriaTranslator :
YYT.Core.ExpressionVisitor
{
#region Fields
StringBuilder sb;
Dictionary<string, string> resultDictionary
= null;
private Type objectType;
#endregion
#region Constructor
internal CriteriaTranslator(Type
type)
{
objectType = type;
}
#endregion
#region Main Methods
internal Dictionary<string, string> Translate(Dictionary<string,
List<Expression>> expressionDictionary)
{
resultDictionary = new Dictionary<string, string>();
foreach (var expressionKeyValePair
in expressionDictionary)
{
this.sb = new StringBuilder();
foreach (Expression expression in expressionKeyValePair.Value)
{
this.Visit(Evaluator.PartialEval(expression));
}
AddExpressionResult(expressionKeyValePair.Key, sb.ToString());
}
return resultDictionary;
}
#endregion
#region Override Methods
protected override Expression VisitMethodCall(MethodCallExpression
m)
{
if (m.Method.DeclaringType == typeof(string))
{
switch (m.Method.Name)
{
case "StartsWith":
sb.Append("(");
this.Visit(m.Object);
return m;
case "Contains":
sb.Append("(");
this.Visit(m.Object);
sb.Append(" LIKE '%' + ");
this.Visit(m.Arguments[0]);
sb.Append(" + '%')");
return m;
}
}
throw new NotSupportedException(string.Format("The
method '{0}' is not supported", m.Method.Name));
}
protected override Expression VisitUnary(UnaryExpression
u)
{
switch (u.NodeType)
{
case ExpressionType.Not:
sb.Append(" NOT ");
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format("The
unary operator '{0}' is not supported", u.NodeType));
}
return u;
}
protected override Expression VisitMemberAccess(MemberExpression
m)
{
if (m.Expression != null && m.Expression.NodeType
== ExpressionType.Parameter)
{
if (this.objectType.IsAssignableFrom(typeof(BusinessBase)))
{
List<IPropertyInfo> propertyInfoList = PropertyInfoManager.GetRegisteredProperties(this.objectType);
var property = propertyInfoList.Where(u => u.Name
== m.Member.Name).SingleOrDefault();
if (property != null)
{
var dataEntityName = property.DataEntityPropertyName;
sb.Append(dataEntityName);
AddExpressionResult("Type", property.DataEntityTyoe.Name);
}
}
return m;
}
else
throw new NotSupportedException(string.Format("The
member '{0}' is not supported", m.Member.Name));
}
protected override Expression VisitBinary(BinaryExpression
b)
{
sb.Append("(");
this.Visit(b.Left);
switch (b.NodeType)
{
case ExpressionType.And:
case ExpressionType.AndAlso:
sb.Append(" AND ");
break;
case ExpressionType.Or:
case ExpressionType.OrElse:
sb.Append(" OR");
break;
case ExpressionType.Equal:
sb.Append(" = ");
break;
case ExpressionType.NotEqual:
sb.Append(" <> ");
break;
case ExpressionType.LessThan:
sb.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format("The
binary operator '{0}' is not supported", b.NodeType));
}
this.Visit(b.Right);
sb.Append(")");
return b;
}
protected override Expression VisitConstant(ConstantExpression
c)
{
IQueryable q = c.Value as IQueryable;
if (q != null)
{
// assume constant nodes w/ IQueryables are table
references
sb.Append("SELECT * FROM ");
sb.Append(q.ElementType.Name);
}
else if (c.Value == null)
{
sb.Append("NULL");
}
else
{
switch (Type.GetTypeCode(c.Value.GetType()))
{
case TypeCode.Boolean:
sb.Append(((bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format("The
constant for '{0}' is not supported", c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}
#endregion
#region Assistant Methods
private static Expression StripQuotes(Expression
e)
{
while (e.NodeType == ExpressionType.Quote)
{
e = ((UnaryExpression)e).Operand;
}
return e;
}
private void AddExpressionResult(string key, string
value)
{
string tempValue = string.Empty;
if (!resultDictionary.TryGetValue(key, out tempValue))
{
lock (resultDictionary)
{
if (!resultDictionary.TryGetValue(key, out tempValue))
{
resultDictionary.Add(key, value);
}
}
}
}
#endregion
}
VisitMemberAccess方法就是用来把业务属性的名字换成对应数据库中表字段的名字的。
protected override Expression VisitMemberAccess(MemberExpression
m)
{
if (m.Expression != null && m.Expression.NodeType
== ExpressionType.Parameter)
{
if (this.objectType.IsAssignableFrom(typeof(BusinessBase)))
{
List<IPropertyInfo> propertyInfoList = PropertyInfoManager.GetRegisteredProperties(this.objectType);
var property = propertyInfoList.Where(u => u.Name
== m.Member.Name).SingleOrDefault();
if (property != null)
{
var dataEntityName = property.DataEntityPropertyName;
sb.Append(dataEntityName);
AddExpressionResult("Type", property.DataEntityTyoe.Name);
}
}
return m;
}
else
throw new NotSupportedException(string.Format("The
member '{0}' is not supported", m.Member.Name));
}
上面的代码中,并没有立刻就把所有操作拼接成sql语句,而且把操作分类的记录下来在一个字典中,这个字典最后会被数据层那里使用。例如,字典中可能保存的值如下:
最后这个字典会在数据层那边被使用:ICriteria包含CriteriaProvider,然后DAL代码调用Icriteria.CriteriaProvider.Execute(),获得字典,然后执行真正的数据库操作。
下面就看看数据层的一些接口声明就清楚了。
代码
/// <summary>
/// 数据提供者要实现的借口
/// </summary>
public interface IDataProvider
{
DataResult<TEntity> Add<TEntity>(TEntity
entity) where TEntity : IDataEntity;
DataResult<TEntity> Add<TEntity>(List<TEntity>
entityList) where TEntity : IDataEntity;
DataResult<TEntity> Update<TEntity>(TEntity
entity) where TEntity : IDataEntity;
DataResult<TEntity> Update<TEntity>(List<TEntity>
entityList) where TEntity : IDataEntity;
bool Update(ICriteria condiftion, object value);
DataResult<TEntity> Delete<TEntity>(TEntity
entity) where TEntity : IDataEntity;
DataResult<TEntity> Delete<TEntity>(List<TEntity>
entityList) where TEntity : IDataEntity;
bool Delete(ICriteria condiftion);
int GetCount(ICriteria condition);
DataResult<TEntity> GetOne<TEntity>(ICriteria
condition) where TEntity : IDataEntity;
DataResult<TEntity> GetList<TEntity>(ICriteria
condition) where TEntity : IDataEntity;
DataResult<TEntity> GetPageData<TEntity>(ICriteria
condition, int pageIndex, int pageSize, ref int entityCount)
where TEntity : IDataEntity;
List<object> GetCustomData(ICriteria condiftion);
}
4. 水到渠成
到这里,一切就很清楚了。如果有不明白的地方或者需要讨论的,大家可以给我留言。我会及时的解答。
5. 代码版本的说明
大家之后可以下载到代码的 V1.0版本,下载代码之后可以看到,这个V1.0的版本的代码中存在着CSLA的影子,确实是这样的。本框架在V1.0中确实融合了很多的开源框架,所以不要奇怪。对CSLA不懂,也是没有问题的。
下面我就讲述一下以后版本的一些特性和本系列文章的走向:
V1.0: 存在CLSA的影子。
V2.0:去掉CLSA的影子,重构,并且使用VS开发出DSL工具,图形化的业务类开发和代码的生成
V3.0: 设计和开发出一种基于领域的语言和这个语言的编译器。
V4.0: 设计和开发领域语言的代码IDE。
V5.0: 使用领域语言开发项目
其中使用领域的语言开发时 目标:例如,可能最后的代码是这样的写的:
Define a use who name is “xiaoyang”and
email is “yangyang4502@yahoo.com.cn”
然后我们的领域语言编译器把上面的语句解释为代码,然后进行操作,最终的结果就是在数据库中的UserDetail表中添加了一条记录。
也就是我们梦寐以求的那种“自然语言编程”,是的业务人员来写程序。
文章以后的难度和深度会越来越大,我们一起加油,也希望大家及时的反馈。
呵呵,现在比较的兴奋,说句”胡话”:咱们搞开发的,不仅仅只是懂得使用别人的开源框架和开源技术,咱们自己也可以成为技术的创建者。共勉! |