具有数据访问层的通用存储库

本文关键字:存储 数据 访问 | 更新日期: 2023-09-27 18:13:59

我正在使用业务对象(Employee, Product)创建一个新项目。由于限制,我不使用LINQ到SQL或任何ORM映射器。

我必须手工编写数据访问层。我对使用"存储库模式"很感兴趣。

根据我的理解,我必须创建一个通用的存储库IRepository,它由所有存储库ProductRepository, EmployeeRepository实现。

让我困惑的是不同的业务对象有不同的需求。例如:

ProductRepository

 GetAllProducts ();
 GetProductById (int id);
 GetProductByMaxPrice (double price);
 GetProductByNamePrice (string name, double Price);
 Get... (...);

EmployeeRepository

 GetEmployeeByAge ();
 GetEmployeeByJob (string description);
 GetEmployeeBySalary (double salary);
 Get... (...); //and so on

如何创建通用存储库来满足不同对象的不同数据访问需求?

我已经读了很多关于存储库模式的理论,但是如果有一个实际的例子,我将非常感激。

此外,如果我可以使用通用存储库创建所有存储库,那么使用工厂模式也会变得容易。例如:

interface IRepository
{
    ....
}
ProductRepository : IRepository
{
    ....
}
EmployeeRepository : IRepository
{
    ....
}

那么我们可以有效地使用工厂模式:

IRepository repository;
repository = new ProductRepository ();
repository.Call_Product_Methods ();
repository = new EmployeeRepository ();
repository.Call_Employee_Methods ();

具有数据访问层的通用存储库

存储库模式是一个很好的模式,但如果没有正确地完成它,它将是一个巨大的痛苦,而不是使您的生活更容易!

因此,最好的方法(因为您不想使用EF或其他ORM)是创建一个泛型接口,然后创建一个基本抽象实现。这样你就不需要对每个存储库进行编码,你只需要按类型实例化它们就可以了!

在此之后,如果您有任何特定于某些实体的特定方法,您都可以从Repository继承并根据需要重写或添加方法和属性。

如果你想使用储存库模式,我也建议你使用IUnitOfWork模式,并保持它与储存库分离。

两个接口看起来应该是这样的:

非常简单的IUnitOfWork:
Public interface IUnitOfWork
{
    bool Save();
}

和他们,仓库接口,使用通用的:

public interface IRepository<TEntity> : IDisposable where TEntity : class
    IUnitOfWork Session { get;}
    IList<TEntity> GetAll();
    IList<TEntity> GetAll(string[] include);
    IList<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate);
    bool Add(TEntity entity);
    bool Delete(TEntity entity);
    bool Update(TEntity entity);
    bool IsValid(TEntity entity);
}

方法。add(),。delete()不应该向数据库发送任何东西,但它们应该总是将更改发送到IUnitOfWork(你可以在你的DAL类中实现),只有当你调用IUnitOfWork的。save()方法时,你才会将内容保存到数据库。

我已经使用EntityFramework实现了我的Repository类,这使事情变得更容易,但是你可以用任何你想要的方式来做。

您将使用的代码将是这样的:

void SomeMethod()
{
    using (IUnitOfWork session = new YourUnitOfWorkImplementation())
    {
        using (var rep = new Repository<Client>(session))
        {
            var client1 = new Client("Bob");
            var client2 = new Cliente("John");
            rep.Add(client1);
            rep.Add(client2);
            var clientToDelete = rep.GetAll(c=> c.Name == "Frank").FirstOrDefaut();
            rep.Delete(clientToDelete);
            //Now persist the changes to the database
            session.Save();
        {
    {
}

就像我说的,使用EF和DbContext,这要容易得多,这里是我的Repository类的一小部分:

public class Repository : Component, IRepository
{

    protected DbContext session;
    {
        get
        {
            if (session == null)
                throw new InvalidOperationException("A session IUnitOfWork do repositório não está instanciada.");
            return (session as IUnitOfWork);
        }
    }
    public virtual DbContext Context
    {
        get
        {
            return session;
        }
    }
    public Repository()
        : base()
    {
    }
    public Repository(DbContext instance)
        : this(instance as IUnitOfWork)
    {

    #endregion

    public IList<TEntity> GetAll<TEntity>() where TEntity : class
    {
        return session.Set<TEntity>().ToList();
    }

    public bool Add<TEntity>(TEntity entity) where TEntity : class
    {
        if (!IsValid(entity))
            return false;
        try
        {
            session.Set(typeof(TEntity)).Add(entity);
            return session.Entry(entity).GetValidationResult().IsValid;
        }
        catch (Exception ex)
        {
            if (ex.InnerException != null)
                throw new Exception(ex.InnerException.Message, ex);
            throw new Exception(ex.Message, ex);
        }
    } ...

这样你就不需要构建getemployebyage了,你只需要写:

IEnumerable<Employee> GetEmployee(int age)
{
 return  rep.GetAll<Employee>(e=> e.Age == age);
}

或者直接调用(不需要创建方法)

一般来说,在我看来,一个通用的存储库"基础"接口并不能真正解决那么多问题。有人提到,理论上,它可以提供一个get属性,该属性接受一个整数并返回一条记录。是的,这很好也很方便——而且取决于你的用例,甚至可能是理想的。

我个人划定的界限是Insert, UpdateDelete方法。除了最简单的情况外,在所有情况下,我们都应该确定我们正在做什么。是的,创建一个新的Supplier可能仅仅意味着调用一个Insert操作。但大多数情况下,你会做其他事情。

因此,在设计存储库时,我认为最好确定您想要做的操作,并将方法命名为:

CreateClient(); // Might well just be a single Insert.... might involve other operations
MoveClientToCompany(); // several updates right here
GetContractsForClient(); // explicitly returns contracts belonging to a client

我们现在正在定义我们对数据做什么。一般的Insert、Update和Delete方法不能推断出存储库的使用情况,并且可能会导致开发人员误用,因为他们不了解当我们实际去某事时需要发生哪些辅助的事情。

那么基础存储库的一个好例子是什么呢?那么,实现缓存的存储库呢?基本存储库可以有某种缓存,如果需要的话,派生存储库可以使用该缓存返回过时的数据。

当我们需要回答我们将返回什么时,甚至this[int]默认属性也有复杂的问题。如果它是一个有许多引用的大对象,我们是要返回整个对象及其所有部分,还是返回一个非常简单的POCO,需要进一步的查询来填补空白。一个通用的this[int]不能回答这个问题,但是:
GetBareBonesClient(int id);
GetClientAndProductDetail(int id);
GetClientAndContracts(int id);
在我看来,

定义得很好。在智能感知的今天,针对存储库编码的开发人员将知道他/她需要调用什么才能获得他们想要的结果。如何确定存在多少这样的方法?嗯,你看看你正在开发的产品。你有什么案例可以获取数据…谁在获取这些数据,他们为什么要获取这些数据?大多数时候,这些都是很容易回答的问题。

然而,一个常见的问题是,当我们希望允许用户"浏览"表格中的数据时。"给我"x"条记录,按"x"字段排序,以分页的方式……哦,我可能会也可能不会在某个列中包含某种搜索"。这种代码是您确实不希望为每个存储库实现的。因此,在假设的IRepositoryThatSupportsPagination中可能存在一些锅炉板查询构造的情况。我相信你能想出一个更好的名字。

显然,可能会有更多的病例。但是我绝不会将默认的CRUD操作扔到基础存储库接口/类中,因为除了不重要的、琐碎的情况外,并不意味着任何东西。

[根据MikeSW的输入编辑]我的观点(在这里加入Moo-Juice)是,您需要选择最适合您的实现。存储库模式是一个很好的模式(Gabriel的回答描述了一个很好的实现),但是如果以其纯粹的形式实现,可能会有很多工作要做。orm自动化了很多繁重的工作。

无论您选择哪种方法,您都需要以下组件:

  1. 您的业务接口-您的客户端程序员需要调用的方法,例如GetAllEmployees(标准),UpdateEmployee(雇员雇员)等。如果您有客户机/服务器架构,这些将对应于具有数据契约的服务调用。

  2. 您的内部逻辑,创建适当的输出,以满足您的合同。这将是组成查询或执行多个数据库更新的层,例如,UpdateEmployee可能必须验证雇员是否存在,更新程序是否具有更新权限,然后更新几个表,并将审计记录或记录插入到审查队列中。这将涉及查询和更新,并且将是一个工作单元。

  3. 您的数据访问架构,由您的内部逻辑调用。这就是存储库模式发挥作用的地方。无论您使用什么,这都需要以下内容:

3.1实现工作单元的类。在存储库模式中,这只有Save()—但这需要内存状态管理。对于sql驱动的实现,我更喜欢使用以下接口:
public interface ITransactionContext : IDisposable
{
    IDbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted);
    void CommitTransaction();
    void RollbackTransaction();
    int ExecuteSqlCommand(string sql, params object[] parameters);
    IEnumerable<T> SqlQuery<T>(string sql, params object[] parameters);
    IEnumerable<T> SqlQuery<T>(string sql, object[] parameters, IDictionary<string, string> mappings);
    bool Exists(string sql, params object[] parameters);        
}
public interface ITransactionDbContext : ITransactionContext
{
    int SaveChanges();
}

我使用EF,但我们有一个旧的数据库,我们需要写SQL,这看起来和操作很像EF DbContext。注意接口ITransactionDbContext,它添加了SaveChanges()——这是ORM唯一需要的。但如果你不做ORM,你就需要其他的。

这是实现。注意,它完全是基于接口的。你可以通过工厂方法给出具体的数据库连接。

public class TransactionContext : ITransactionContext
{
    protected IDbTransaction Transaction;
    protected IDbConnection Connection;
    protected readonly Func<IDbConnection> CreateConnection;
    public TransactionContext(Func<IDbConnection> createConnection)
    {
        this.CreateConnection = createConnection;
    }
    public virtual IDbConnection Open()
    {
        if (this.Connection == null)
        {
            this.Connection = this.CreateConnection();
        }
        if (this.Connection.State == ConnectionState.Closed)
        {
            this.Connection.Open();
        }
        return this.Connection;
    }

    public virtual IDbTransaction BeginTransaction(IsolationLevel isolationLevel)
    {
        Open();
        return this.Transaction ?? (this.Transaction = this.Connection.BeginTransaction(isolationLevel));
    }
    public virtual void CommitTransaction()
    {
        if (this.Transaction != null)
        {
            this.Transaction.Commit();
        }
        this.Transaction = null;
    }
    public virtual void RollbackTransaction()
    {
        if (this.Transaction != null)
        {
            this.Transaction.Rollback();
        }
        this.Transaction = null;
    }
    public virtual int ExecuteSqlCommand(string sql, params object[] parameters)
    {
        Open();
        using (var cmd = CreateCommand(sql, parameters))
        {
            return cmd.ExecuteNonQuery();
        }
    }
    public virtual IEnumerable<T> SqlQuery<T>(string sql, object[] parameters ) 
    {
        return SqlQuery<T>(sql, parameters, null);
    }
    public IEnumerable<T> SqlQuery<T>(string sql, object[] parameters, IDictionary<string, string> mappings) 
    {
        var list = new List<T>();
        var converter = new DataConverter();
        Open();
        using (var cmd = CreateCommand(sql, parameters))
        {
            var reader = cmd.ExecuteReader();
            if (reader == null)
            {
                return list;
            }
            var schemaTable = reader.GetSchemaTable();
            while (reader.Read())
            {
                var values = new object[reader.FieldCount];
                reader.GetValues(values);
                var item = converter.GetObject<T>(schemaTable, values, mappings);
                list.Add(item);
            }
        }
        return list;        }
    public virtual bool Exists(string sql, params object[] parameters)
    {
        return SqlQuery<object>(sql, parameters).Any();
    }
    protected virtual IDbCommand CreateCommand(string commandText = null, params object[] parameters)
    {
        var command = this.Connection.CreateCommand();
        if (this.Transaction != null)
        {
            command.Transaction = this.Transaction;
        }
        if (!string.IsNullOrEmpty(commandText))
        {
            command.CommandText = commandText;
        }
        if (parameters != null && parameters.Any())
        {
            foreach (var parameter in parameters)
            {
                command.Parameters.Add(parameter);
            }
        }
        return command;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected void Dispose(bool disposing)
    {
        if (this.Connection != null)
        {
            this.Connection.Dispose();
        }
        this.Connection = null;
        this.Transaction = null;
    }
}

3.2。然后需要基于命令实现更新。下面是我的(简化):

public class UpdateHelper
{
    private readonly ITransactionContext transactionContext;
    public UpdateHelper(ITransactionContext transactionContext)
    {
        this.transactionContext = transactionContext;
    }
    public UpdateResponse Update(UpdateRequest request)
    {
        this.transactionContext.BeginTransaction(IsolationLevel.RepeatableRead);
        var response = new UpdateResponse();
        foreach (var command in request.Commands)
        {
            try
            {
                response = command.PerformAction(transactionContext);
                if (response.Status != UpdateStatus.Success)
                {
                    this.transactionContext.RollbackTransaction();
                    return response;
                }
            }
            catch (Exception ex)
            {
                this.transactionContext.RollbackTransaction();
                return HandleException(command, ex);
            }
        }
        this.transactionContext.CommitTransaction();
        return response;
    }
    private UpdateResponse HandleException(Command command, Exception exception)
    {
        Logger.Log(exception);
        return new UpdateResponse { Status = UpdateStatus.Error, Message = exception.Message, LastCommand = command };
    }
}
如您所见,这需要一个Command来执行操作(这是Command模式)。基本命令实现:
public class Command
{
    private readonly UpdateCommandType type;
    private readonly object data;
    private readonly IDbMapping mapping;
    public Command(UpdateCommandType type, object data, IDbMapping mapping)
    {
        this.type = type;
        this.data = data;
        this.mapping = mapping;
    }
    public UpdateResponse PerformAction(ITransactionContext context)
    {
        var commandBuilder = new CommandBuilder(mapping);
        var result = 0;
        switch (type)
        {
            case UpdateCommandType.Insert:
                result  = context.ExecuteSqlCommand(commandBuilder.InsertSql, commandBuilder.InsertParameters(data));
                break;
            case UpdateCommandType.Update:
                result = context.ExecuteSqlCommand(commandBuilder.UpdateSql, commandBuilder.UpdateParameters(data));
                break;
            case UpdateCommandType.Delete:
                result = context.ExecuteSqlCommand(commandBuilder.DeleteSql, commandBuilder.DeleteParameters(data));
                break;
        }
        return result == 0 ? new UpdateResponse { Status = UpdateStatus.Success } : new UpdateResponse { Status = UpdateStatus.Fail };
    }
}
3.3需要用对象来映射数据库。这是由更新方法使用的。在本例中,如果提交了映射,则假定EntityType的属性对应于数据库列。您需要每个表的映射。
public interface IDbMapping
{
    string TableName { get; }
    IEnumerable<string> Keys { get; }
    Dictionary<string, string> Mappings { get; }
    Type EntityType { get; }
    bool AutoGenerateIds { get; }
}
public class EmployeeMapping : IDbMapping
{
    public string TableName { get { return "Employee"; } }
    public IEnumerable<string> Keys { get { return new []{"EmployeeID"};} }
    public Dictionary<string, string> Mappings { get { return null; } } // indicates default mapping based on entity type } }
    public Type EntityType { get { return typeof (Employee); } }
    public bool AutoGenerateIds { get { return true; } }
}

3.4。您需要一个查询构建器对象。这将基于用户在sql中的输入构建查询。例如,您可能希望按姓、名、部门和加入日期搜索员工。您可以实现这样的查询接口:

 public interface IEmployeeQuery {
     IEmployeeQuery ByLastName(string lastName);
     IEmployeeQuery ByFirstName(string firstName);
     IEmployeeQuery ByDepartment(string department);
     IEmployeeQuery ByJoinDate(Datetime joinDate);
 }

这可以通过构建sql查询或linq查询的类来具体实现。如果您使用sql,请实现string Statementobject[] Parameters。然后你的逻辑层可以写这样的代码:

   public IEnumerable<Employee> QueryEmployees(EmployeeCriteria criteria) {
        var query = new EmployeeQuery(); 
        query.ByLastName(criteria.LastName);
        query.ByFirstName(criteria.FirstName); 
        //etc.
        using(var dbContext = new TransactionContext()){
            return dbContext.SqlQuery<Employee>(query.Statement, query.Parameters);
        }
   }

3.5。您的对象需要一个命令构建器。我建议您使用通用的命令构建器。您可以使用SqlCommandBuilder类,也可以编写自己的SQL生成器。我不建议您为每个表和每次更新编写sql。这部分很难维护。(从我的经验来说。我们有一个,但我们无法维护它,最后我写了一个SQL生成器。

注意:如果你没有太多的更新(例如你的应用程序主要是面向显示的),你可以省略这个,在你需要的时候手工编写更新。

这是一个通用的构建器(此代码未经过测试,您需要在需要时使用它):

public interface ICommandBuilder
{
    string InsertSql { get; }
    string UpdateSql { get; }
    string DeleteSql { get; }
    Dictionary<string, object> InsertParameters(object data);
    Dictionary<string, object> UpdateParameters(object data);
    Dictionary<string, object> DeleteParameters(object data);
}
public class CommandBuilder: ICommandBuilder
{
    private readonly IDbMapping mapping;
    private readonly Dictionary<string, object> fieldParameters;
    private readonly Dictionary<string, object> keyParameters; 
    public CommandBuilder(IDbMapping mapping)
    {
        this.mapping = mapping;
        fieldParameters = new Dictionary<string, object>();
        keyParameters = new Dictionary<string, object>();
        GenerateBaseSqlAndParams();
    }
    private void GenerateBaseSqlAndParams()
    {
        var updateSb = new StringBuilder();
        var insertSb = new StringBuilder();
        var whereClause = new StringBuilder(" WHERE ");
        updateSb.Append("Update " + mapping.TableName + " SET ");
        insertSb.Append("Insert Into " + mapping.TableName + " VALUES (");
        var properties = mapping.EntityType.GetProperties(); // if you have mappings, work that in
        foreach (var propertyInfo in properties)
        {
            var paramName = propertyInfo.Name;
            if (mapping.Keys.Contains(propertyInfo.Name, StringComparer.OrdinalIgnoreCase))
            {
                keyParameters.Add(paramName, null);
                if (!mapping.AutoGenerateIds)
                {
                    insertSb.Append(paramName + ", ");
                }
                whereClause.Append(paramName + " = @" + paramName);
            }
            updateSb.Append(propertyInfo.Name + " = @" + paramName + ", ");
            fieldParameters.Add(paramName, null);
        }
        updateSb.Remove(updateSb.Length - 2, 2); // remove the last ","
        insertSb.Remove(insertSb.Length - 2, 2);
        insertSb.Append(" )");
        this.InsertSql = insertSb.ToString();
        this.UpdateSql = updateSb.ToString() + whereClause;
        this.DeleteSql = "DELETE FROM " + mapping.TableName + whereClause;
    }
    public string InsertSql { get; private set; }
    public string UpdateSql { get; private set; }
    public string DeleteSql { get; private set; }
    public Dictionary<string, object> InsertParameters(object data)
    {
        PopulateParamValues(data);
        return mapping.AutoGenerateIds ? fieldParameters : keyParameters.Union(fieldParameters).ToDictionary(pair => pair.Key, pair => pair.Value);
    }
    public Dictionary<string, object> UpdateParameters(object data)
    {
        PopulateParamValues(data);
        return fieldParameters.Union(keyParameters).ToDictionary(pair => pair.Key, pair => pair.Value);
    }
    public Dictionary<string, object> DeleteParameters(object data)
    {
        PopulateParamValues(data);
        return keyParameters;
    }
    public void PopulateParamValues(object data)
    {
        var properties = mapping.EntityType.GetProperties(); // if you have mappings, work that in
        foreach (var propertyInfo in properties)
        {
            var paramName = propertyInfo.Name;
            if (keyParameters.ContainsKey(paramName))
            {
                keyParameters[paramName] = propertyInfo.GetValue(data);
            }
            if (fieldParameters.ContainsKey(paramName))
            {
                fieldParameters[paramName] = propertyInfo.GetValue(data);
            }
        }
    }
}

使用逻辑层中的更新帮助器和命令构建器进行更新的示例:

public class Logic
{
    private readonly Func<ITransactionContext> createContext;
    private readonly Func<ITransactionContext, UpdateHelper> createHelper; 
    public Logic(Func<ITransactionContext> createContext, 
        Func<ITransactionContext, UpdateHelper> createHelper)
    {
        this.createContext = createContext;
        this.createHelper = createHelper;
    }
    public int UpdateEmployee(Employee employeeData)
    {
        using (var context = createContext())
        {
            var request = new UpdateRequest();
            request.Commands.Add(new Command(UpdateCommandType.Update, employeeData, new EmployeeMapping()));
            var helper = createHelper(context);
            var response = helper.Update(request);
            return response.TransactionId ?? 0;
        }
    }
}

ORM真的可以帮助你:

  • 数据映射
  • 命令构建(你不需要这样做)
  • 查询构建-您可以使用内置的Linq-to-Sql。

总的来说,该方法使用来自Repository模式的工作单元,但是它使用UpdateHelper类来基于命令模式执行更新,而不是使用Repository对象及其Add、Update和Delete方法。这允许直接编写SQL,而不需要ORM映射器。

好吧,这很长,但显然没有所有的细节,我的回答被认为是不值得的。

是的,您可以轻松地基于通用的、恒定的存储库接口编写一个优雅的DAL层。

但是,它很可能会有一个非常糟糕的性能。

在一个完美的世界中,任何信息都可以从数据库中检索而不需要任何成本,一个简单而通用的存储库就足够了。不幸的是,情况并非如此——对于我们知道数据库可以处理的每个查询操作,最好有特定的查询方法,而不是有一个通用的存储库,其中包含通用的查询方法,允许来自业务层的各种疯狂查询。

编辑

我相信你在一个特定的点上似乎是错误的:避免使用通用的ORM映射库意味着你没有做ORM。这并不一定正确。

除非向UI公开一般的类数组对象(这也会使关于存储库模式的讨论完全无用),否则就是将关系数据转换为域对象。这正是ORM的意义所在:你不使用NHibernate、EF或LINQ to SQL,这意味着你将有更多的工作要做。: -)

所以,不,使用存储库模式仍然是有意义的,不管是否使用自动ORM工具。

当然,还有其他选项,比如活动记录。这是一个更简单的模式,它混合了域对象和数据访问逻辑(这里使用ORM工具也是可选的)。