【这里是的实现,指的是针对各个数据访问框架的一个基础实现】

目标

  •   定义仓储/QueryEntry的基本功能
  •   实现仓储的基本功能,以利于复用
  •   实现一些常用的功能
  •   提供一些便利的功能

目标框架

博主使用的ORM框架是EF6.x,使用MAP来配置模型和数据库之间的映射(因为模型是定义在领域层[CQRS]的),所以不打算使用声明式的Attribute。使用code first来生成数据库。

仓储基本功能

使用一个泛型接口定义了一个仓储需要实现的功能:

public interface IBasicReponsitory<T>
{
void Insert(T item);
void Delete(T item);
void Delete(Guid aggregateId);
void Update(T category);
T Fetch(Guid aggregateId);
T TryFetch(Guid aggregateId);
bool Exists(Expression<Func<T, bool>> predict); /*以下是额外的一些接口方法,待商榷*/
IQueryable<T> Query();
Task<T> FetchAsync(Guid id);
Task<T> TryFetchAsync(Guid id);
Task<IEnumerable<T>> RetriveAsync(Expression<Func<T, bool>> predict);
}

以及一个QueryEntry需要实现的一些基本功能:

public interface IQueryEntry<T> where T : IHasPrimaryKey
{
T TryFetch(Guid id);
Task<T> TryFetchAsync(Guid id); bool Exsits(Guid id);
bool Exsits(Expression<Func<T, bool>> selector);
}

随着时间的推移,这个接口会发生变更,添加一些更多的功能。同时,并不要求所有的仓储或者QueryEntry继承接口,基本接口的定义和实现仅仅是为了提供便利。

为了方便QueryEntry的实现,提供了一个抽象类:

public abstract class ReponsitoryBasedQueryEntry<T> : IQueryEntry<T> where T : IHasPrimaryKey
{
public abstract IBasicReponsitory<T> BasicReponsitory { get; } public T TryFetch(Guid id)
{
return BasicReponsitory.TryFetch(id);
} public Task<T> TryFetchAsync(Guid id)
{
return BasicReponsitory.TryFetchAsync(id);
} public bool Exsits(Guid id)
{
return BasicReponsitory.Query().Any(i => i.Id == id);
} public bool Exsits(System.Linq.Expressions.Expression<Func<T, bool>> selector)
{
return BasicReponsitory.Query().Any(selector);
}
}

基本实现

public class BasicEntityFrameworkReponsitory<T> : IBasicReponsitory<T> where T : class, IHasPrimaryKey
{
public BasicEntityFrameworkReponsitory()
{
Table = StorageConfiguration.DbContext.Set<T>();
} public DbSet<T> Table { get; private set; } public virtual void Insert(T item)
{
Table.Add(item);
} public virtual void Delete(T item)
{
Table.Remove(item);
} public virtual void Delete(Guid aggregateId)
{
var item = TryFetch(aggregateId);
Delete(item);
} public virtual void Update(T category)
{
//do nothing...
} public T Fetch(Guid aggregateId)
{
var item = TryFetch(aggregateId);
if (item == null)
{
throw new AggregateRootNotFoundException(aggregateId);
}
return item;
} public T TryFetch(Guid aggregateId)
{
var item = Query().FirstOrDefault(i => i.Id == aggregateId);
return item;
} public virtual IQueryable<T> Query()
{
return Table;
} public async Task<T> FetchAsync(Guid id)
{
return await Table.FirstAsync(i => i.Id == id);
} public async Task<T> TryFetchAsync(Guid id)
{
return await Table.FirstOrDefaultAsync(i => i.Id == id);
} public bool Exists(Expression<Func<T, bool>> predict)
{
return Table.Any(predict);
} public async Task<IEnumerable<T>> RetriveAsync(Expression<Func<T, bool>> predict)
{
return await Table.Where(predict).ToArrayAsync();
}
}

这部分代码表达了个人的几个想法:
1.DbContext的生命周期是由Storage自行管理的。当然,可以通过一定的方式指定。

2.提供了基础的Query()方法,并设置为虚方法。个人并不抵制使用IQueryable对象进行查询。我觉得可以把使用IQueryable对象进行查询的代码片段看作匿名方法。

常用的功能:软删除

这里是继承基本实现的一个实现:

public class SoftDeleteEntityFrameworkReponsitory<T> : BasicEntityFrameworkReponsitory<T>
where T : class, IHasPrimaryKey, ISoftDelete
{
public override IQueryable<T> Query()
{
return base.Query().Where(i => !i.IsDeleted);
} public override void Delete(T item)
{
item.IsDeleted = true;
Update(item);
}
}

这里要求仓储对应的模型实现接口ISoftDelete,为软删除提供支持:

public interface ISoftDelete
{
bool IsDeleted { get; set; }
}

同时override了Query()方法,过滤了已删除的内容。

常用的功能:操作跟踪

好吧,这应该是事件溯源干的事,然而事件溯源目前太难了。原理和软删除差不多:

/// <summary>
/// 既然开启了跟踪,那么这条数据必然是不能硬删除的
/// </summary>
/// <typeparam name="T"></typeparam>
public class TraceEnabledEntityFrameworkReponsitory<T> : SoftDeleteEntityFrameworkReponsitory<T>
where T : class, ISoftDelete, ITrackLastModifying, IHasPrimaryKey
{
/// <summary>
/// 开启跟踪时,不允许匿名操作
/// </summary>
[Dependency]
public IDpfbSession Session { get; set; } public override void Update(T item)
{
if (!Session.UserId.HasValue)
throw new Exception(); //todo 提供一个明确的异常
item.LastModifiedBy = Session.UserId.Value;
item.LastModifiedTime = DateTime.Now;
} public override void Insert(T item)
{
if (!Session.UserId.HasValue)
throw new Exception(); //todo 提供一个明确的异常
item.LastModifiedBy = Session.UserId.Value;
item.LastModifiedTime = DateTime.Now;
base.Insert(item);
}
}

不过这个功能的侵入性很强,Storage应该无法感知“用户”这种概念才对。

便利的功能:动态仓储(DynamicReponsitory)

前一篇文章中说过,引入QueryEntry是为了将查询和提交分来,同时为查询操作提供更大的优化空间。在面对数据库的查询中,多表联查是非常普遍的。所以打算针对多表联查提供一个遍历的组件。同时,直接提交语句查询是和数据库相关的,所以要针对不同的数据库提供不同的DynamicReponsitory。

这个组件解决的问题是:直接提交数据库多表联查,查询结果自动转换模型,提供分页支持。

模型转换

先来解决这个比较有趣的问题:将一个DataReader转换为一个值或者一个可枚举的集合。直接上实现代码:

public class DataReaderTransfer<T> : CacheBlock<string, Func<IDataReader, T>> where T : new()
{
protected DataReaderTransfer()
{
} /// <summary>
///
/// </summary>
/// <param name="filedsNameArray"></param>
/// <param name="key">编译缓存所使用的key,建议使用查询字符串的hash</param>
/// <returns></returns>
public Func<IDataReader, T> Compile(string[] filedsNameArray, string key)
{
var outType = typeof (T);
var func = ConcurrentDic.GetOrAdd(key, k =>
{
var expressions = new List<Expression>();
//public T xxx(IDataReader reader){
var param = Expression.Parameter(typeof (IDataReader)); //var instance = new T();
var newExp = Expression.New(outType);
var varExp = Expression.Variable(outType, "instance");
var varAssExp = Expression.Assign(varExp, newExp);
expressions.Add(varAssExp); var indexProp = typeof (IDataRecord).GetProperties().Last(p => p.Name == "Item"); //表示 reader[""]
foreach (var fieldName in filedsNameArray)
{
//if(xxx)xxx.xxx=null;else xxx.xxx = (xxx)value; var prop = outType.GetProperty(fieldName);
if (prop == null)
continue;
var propExp = Expression.PropertyOrField(varExp, fieldName);
Expression value = Expression.MakeIndex(param, indexProp,
new Expression[] {Expression.Constant(fieldName)}); //处理空值
var defaultExp = Expression.Default(prop.PropertyType);
var isDbNullExp = Expression.TypeIs(value, typeof (DBNull)); //处理枚举以及可空枚举
if (prop.PropertyType.IsEnum ||
prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericArguments()[].IsEnum)
{
value = Expression.Convert(value, typeof (int));
}
var convertedExp = Expression.Convert(value, prop.PropertyType);
//读取到dbnull的时候,使用一个默认值
var condExp = Expression.IfThenElse(isDbNullExp,
Expression.Assign(propExp, defaultExp),
Expression.Assign(propExp, convertedExp)); expressions.Add(condExp);
} //return instance;
var retarget = Expression.Label(outType);
var returnExp = Expression.Return(retarget, varExp);
expressions.Add(returnExp); //}
var relabel = Expression.Label(retarget, Expression.Default(outType));
expressions.Add(relabel); var blockExp = Expression.Block(new[] {varExp}, expressions);
var expression = Expression.Lambda<Func<IDataReader, T>>(blockExp, param);
return expression.Compile();
});
return func;
} public Func<IDataReader, T> Compile(IDataReader reader, string key)
{
var length = reader.FieldCount;
var names = Enumerable.Range(, length).Select(i => reader.GetName(i - )).ToArray();
return Compile(names, key);
} public static DataReaderTransfer<T> Instance = new DataReaderTransfer<T>(); //基于反射的映射....
//private static T DynamicMap<T>(IDataReader reader) where T : new()
//{
// var instance = new T();
// var count = reader.FieldCount;
// while (count-- > 1)
// {
// object value = reader[count - 1];
// var name = reader.GetName(count - 1);
// var prop = typeof (T).GetProperty(name);
// if (prop == null)
// {
// continue;
// }
// if (value is DBNull)
// {
// value = null;
// }
// prop.SetValue(instance, value);
// }
// return instance;
//}
}

主要的思想是:在运行期间对一个特定的模型分析一次,分析构造这个模型需要如何访问DataReader,并将访问操作编译为Func<>,通过一个静态字典缓存。下一次构造的时候,直接访问静态字典的Func<>,将DataReader的行转换为模型。这个耗时,大概是硬编码转换的2倍,可以获得比反射好的性能受益。

链式调用以及延时查询

先来看一段调用代码:

[TestClass]
public class DynamicReponsitorySamples
{
static DynamicReponsitorySamples()
{
//DbContext 配置
StorageConfiguration.Config.Use<DbContext, ObjectManageContext>(new ContainerControlledLifetimeManager());
//无法使用.UseDbContext<ObjectManageContext>(),因为无法提供基于HTTP生命周期的管理对象
DynamicReponsitory = new DynamicReponsitory();
} public static DynamicReponsitory DynamicReponsitory { get; set; } [TestMethod]
public void Query()
{
//直接提交一个SQL查询,并映射到实体
var queryText =
"SELECT A.*,D.Name AS DepartmentName FROM [ADMIN] A LEFT JOIN [Department] D ON A.DepartmentCode = D.Code";
var query = DynamicReponsitory.Query<AdminListItem>(queryText);
//QueryResult对象遵循延时查询的规则,直到执行枚举才会执行查询操作
query.Foreach(i => Trace.WriteLine(string.Format("{0}\t{1}", i.UserName, i.RealName)));
} [TestMethod]
public void Count()
{
//可以直接执行一个COUNT(*)语句
var countQueryText = "SELECT COUNT(*) FROM [ADMIN]";
var countQuery = DynamicReponsitory.Count(countQueryText);
Trace.WriteLine("Count:" + countQuery.Value);
//可以提供一个SELECT * 语句
countQueryText = "SELECT * FROM [ADMIN]";
//但是需要将重载的第二个参数置为true
countQuery = DynamicReponsitory.Count(countQueryText, true);
Trace.WriteLine("Count:" + countQuery.Value);
//可以对一个query对象执行CmountAmount()扩展方法,但是这个query对象代表的查询必须很普通
var query = DynamicReponsitory.Query<AdminListItem>(
"SELECT A.*,D.Name AS DepartmentName FROM [ADMIN] A LEFT JOIN [Department] D ON A.DepartmentCode = D.Code");
countQuery = query.CountAmount();
//Value的值同样遵循延时查询的规则,但是重复访问会导致访问内存中缓存的数据
Trace.WriteLine("Count:" + countQuery.Value);
Trace.WriteLine("Count:" + countQuery.Value);
//如果需要重新查询,可以调用Result.ReQuery()方法
var reQuery = countQuery.ReQuery();
Trace.WriteLine("Count:" + reQuery.Value);
} /// <summary>
/// 分页调用,支持分页信息和分页列表信息的无序访问
/// </summary>
[TestMethod]
public void Page()
{
//可以对所有的query对象执行Page()扩展方法,从而进行分页
//必须执行要求OrderBy参数的重载,否则会进行内存分页(加载所有行)
var query = DynamicReponsitory.Query<AdminListItem>(
"SELECT A.*,D.Name AS DepartmentName FROM [ADMIN] A LEFT JOIN [Department] D ON A.DepartmentCode = D.Code");
var paged = query.Page("ORDER BY DepartmentName", , ); /*
* 以下表示支持分页信息和分页列表信息的无序访问
* 如果使用一条sql同时返回这些信息,必须先枚举集合才能继续访问分页信息
*/ Trace.WriteLine(string.Format("从{0}行到{1}行,在所有的{2}行中", paged.From, paged.To, paged.Amount));
paged.Foreach(i => Trace.WriteLine(string.Format("{0}\t{1}", i.UserName, i.RealName)));
//重复访问会导致访问内存中缓存的数据
var resultArray = paged.Take();
Trace.WriteLine(string.Format("从{0}行到{1}行,在所有的{2}行中", paged.From, paged.To, paged.Amount));
resultArray.Foreach(i => Trace.WriteLine(string.Format("{0}\t{1}", i.UserName, i.RealName)));
}
}

链式调用是指,我调用了DynamicReponsitory.Query()方法之后,可以紧接着调用Page()或者Count()方法。那么,显而易见,如果查询不是延时的,很容易导致这个问题:我把服务器上1W条数据全down下来了,然后在内存里面数数或者分页。
为了实现延时查询的目标,引入了这几个类型:

public class SqlQueryExpression : ICloneable
{
public SqlQueryExpression()
{
Parameters = new List<object>();
} public SqlQueryExpression(string expressionText) : this()
{
ExpressionText = expressionText;
} public string ExpressionText { get; set; }
public IList<object> Parameters { get; private set; } public IDataReader Read(DbConnection connection)
{
var parameters = Parameters.ToArray();
if (connection.State != ConnectionState.Open)
connection.Open();
//查询,开启最低级别的事务隔离,防止默认事务产生争用锁
var trans = connection.BeginTransaction(IsolationLevel.ReadUncommitted);
var command = connection.CreateCommand();
command.CommandType = CommandType.Text;
command.CommandText = ExpressionText;
command.Parameters.AddRange(parameters);
command.Transaction = trans;
return command.ExecuteReader(CommandBehavior.CloseConnection);
} public virtual object Clone()
{
//实现拷贝接口
var cloned = new SqlQueryExpression(ExpressionText);
Parameters.Foreach(i =>
{
var parameter = (SqlParameter) i;
var clonedParameter = new SqlParameter(parameter.ParameterName, parameter.Value);
clonedParameter.Direction = parameter.Direction;
cloned.Parameters.Add(clonedParameter);
});
return cloned;
}
}
 public class SqlQueryResult
{
public SqlQueryExpression SqlQueryExpression { get; private set; }
public DbConnection DbConnection { get; private set; }
public virtual bool Enumerated { get; protected set; }
protected IDataReader DataReader; protected void Query()
{
DataReader = DataReader ?? SqlQueryExpression.Read(DbConnection);
} public SqlQueryResult(SqlQueryExpression expression, DbConnection connection)
{
SqlQueryExpression = expression;
DbConnection = connection;
}
} /// <summary>
/// 代表DynamicReponsitory的查询结果
/// </summary>
/// <typeparam name="T">代表需要构造的类型</typeparam>
public class SqlQueryResult<T> : SqlQueryResult, IEnumerable<T> where T : new()
{
public SqlQueryResult(SqlQueryExpression expression, DbConnection connection)
: base(expression, connection)
{ } public IEnumerator<T> GetEnumerator()
{
//对于一个Query对象,在第一次访问的时候,要求加载所有数据,防止Skip与Take导致数据丢失
if (!Enumerated)
{
Query();
using (DataReader)
{
Enumerated = true;
var uniqueKey = typeof (T).FullName + SqlQueryExpression.ExpressionText;
var func = DataReaderTransfer<T>.Instance.Compile(DataReader, uniqueKey);
while (DataReader.Read())
{
var item = func(DataReader);
ResultSet.Add(item);
//yield return item;
}
}
}
return ResultSet.GetEnumerator();
//return ((IEnumerable<T>) ResultSet).GetEnumerator();
} protected List<T> ResultSet = new List<T>(); IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
} public SqlQueryResult<T> ReQuery()
{
var exp = SqlQueryExpression.Clone() as SqlQueryExpression;
return new SqlQueryResult<T>(exp, DbConnection);
}
}

SqlQueryExpression存储了将要执行的查询,而SqlQueryResult则存储了查询返回的结果。同时,SqlQueryExpression实现了拷贝,以支持ReQuery()。
关于具体的分页支持,实际上是使用了一个开窗函数,通过注入子查询的方式,从而支持了各种查询的分页(不奇葩的查询)。

为了防止查询被锁住,默认开启了最低的事务隔离级别。

...

【想到什么再补充】

CQRS学习——Storage实现(EF+Code First+DynamicReponsitory)[其四]的更多相关文章

  1. EF Code First学习系列

    EF Model First在实际工作中基本用不到,前段时间学了一下,大概的了解一下.现在开始学习Code First这种方式.这也是在实际工作中用到最多的方式. 下面先给出一些目录: 1.什么是Co ...

  2. EF Code First学习笔记

    EF Code First学习笔记 初识Code First EF Code First 学习笔记:约定配置 Entity Framework 复杂类型 Entity Framework 数据生成选项 ...

  3. EF和MVC系列文章导航:EF Code First、DbContext、MVC

    对于之前一直使用webForm服务器控件.手写ado.net操作数据库的同学,突然来了EF和MVC,好多新概念泉涌而出,的确犹如当头一棒不知所措.本系列文章可以帮助新手入门并熟练使用EF和MVC,有了 ...

  4. EF Code First 初体验

    Code First 顾名思义就是先代码,再由代码生成数据库的开发方式. 废话不多说,直接来一发看看:在VS2010里新建一个空白解决方案,再依次添加两个类库项目:Model.DataAccess和一 ...

  5. 【极力分享】[C#/.NET]Entity Framework(EF) Code First 多对多关系的实体增,删,改,查操作全程详细示例【转载自https://segmentfault.com/a/1190000004152660】

      [C#/.NET]Entity Framework(EF) Code First 多对多关系的实体增,删,改,查操作全程详细示例 本文我们来学习一下在Entity Framework中使用Cont ...

  6. 从零开始,搭建博客系统MVC5+EF6搭建框架(1),EF Code frist、实现泛型数据仓储以及业务逻辑

    前言      从上篇30岁找份程序员的工作(伪程序员的独白),文章开始,我说过我要用我自学的技术,来搭建一个博客系统,也希望大家给点意见,另外我很感谢博客园的各位朋友们,对我那篇算是自我阶段总结文章 ...

  7. EF Code First、DbContext

    EF Code First.DbContext 对于之前一直使用webForm服务器控件.手写ado.net操作数据库的同学,突然来了EF和MVC,好多新概念一下泉涌而出,犹如当头一棒,的确有点不知所 ...

  8. EF Code First连接现有数据库

    作为一个初入学习.net MVC4 的新手,想写一写今天碰到的问题,做一个总结. 首先我想利用EF Code First来连接我现有数据库,而不是通过EF Code First来自动生成数据库和表. ...

  9. Entity Framework 学习系列(4) - EF 增删改

    目录 写在前面 一.开发环境 二.创建项目 三.新增 1.单表新增 2.批量新增 3.多表新增 四.编辑 1.先查询,后编辑 2.创建实体,后编辑 五.删除 写在前面 在上一小节中,学习了如何 通过C ...

随机推荐

  1. Spring与Hibernate整合之通用Dao的实现

    在上一篇文章中写了如何直接利用HibernateTemplate进行数据库操作,但在一般的项目中很少直接得到HibernateTemplate的Bean对象从而操作数据库的,下面就简要介绍一下实现通用 ...

  2. .net 获取网站根目录总结

    一.获取网站根目录的方法有几种如: Server.MapPath(Request.ServerVariables["PATH_INFO"]) //页面详细路 Server.MapP ...

  3. git常用命令行

    查看.添加.提交.删除.找回,重置修改文件 git help <command> # 显示command的help git show # 显示某次提交的内容 git show $id gi ...

  4. JDBC之数据连接

    :数据库语句: create database LandDB; use LandDB; create table T_BL_CANTON_CODE ( CTN_CODE int primary key ...

  5. html dom的加载

    操作HTML DOM文档的一个难题是,你的JavaScript代码可能在DOM完全载入之前运行,这会导致你的代码产生一些问题.页面加载时浏览器内部操作的顺序大致是这样的: 1. HTML被解析. 2. ...

  6. ADO.NET笔记(一)XML导入导出和数据库

    数据导出成XML文件 #region 数据导出成XML文件 string sql = "select Id, CallerNumber, TelNum, StartDateTime, End ...

  7. 【Unix环境高级编程】编写变长参数函数

    文件的格式输入输出函数都支持变长参数.定义时,变长参数列表通过省略号'...'表示, 因此函数定义格式为: type 函数名(parm1, parm2,parmN,...); Unix的变长参数通过v ...

  8. 原生JS面向对象思想封装轮播图组件

    原生JS面向对象思想封装轮播图组件 在前端页面开发过程中,页面中的轮播图特效很常见,因此我就想封装一个自己的原生JS的轮播图组件.有了这个需求就开始着手准备了,代码当然是以简洁为目标,轮播图的各个功能 ...

  9. ecshop中无限处理分类

    数据库表记录结构 <?php $sql = "SELECT c.cat_id, c.cat_name, c.measure_unit, c.parent_id, c.is_show, ...

  10. discuz管理中心无法登陆

    检查下配置文件,当前管理是不是创始人. 如是,那试下修改数据库,在数据表出错时也会这样,还有一个也同时试下 \config\config_global.php 文件 $_config['admincp ...