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

时间:2013-05-15 18:44:53

标签: c# oop design-patterns

我正在使用业务对象(Employee,Product)创建一个新项目。由于约束,我没有使用LINQ to SQL或任何ORM Mapper。

我必须手动编码数据访问层。我有兴趣使用'存储库模式'。

根据我的理解,我必须创建一个通用存储库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

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

我已经阅读了很多关于Repository Pattern的理论,但我真的很欣赏一个有效的例子。

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

interface IRepository
{
    ....
}

ProductRepository : IRepository
{
    ....
}

EmployeeRepository : IRepository
{
    ....
}

然后我们可以有效地使用工厂模式:

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

repository = new EmployeeRepository ();
repository.Call_Employee_Methods ();

4 个答案:

答案 0 :(得分:11)

存储库模式是一个很好用的模式,但如果它没有正确完成,而不是让你的生活更轻松,那将是一个巨大的痛苦!

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

在此之后,如果您有某些特定于某些实体的特定方法,则可以从Repository继承并覆盖或添加方法和属性为nedded。

如果您想使用存储库模式,我还建议您使用IUnitOfWork模式,并将其与存储库分开。

两个界面应如下所示:

非常简单的IUnitOfWork:

Public interface IUnitOfWork
{
    bool Save();
}

他们是Repository接口,使用泛型:

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);
        }
    } ...

这样你就不需要构建一个GetEmployeeByAge,你只需写:

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

或者您可以直接致电(无需创建方法)

答案 1 :(得分:6)

一般而言,在我看来,通用存储库&#34; base&#34;界面,并没有真正解决这个问题。有人提到它理论上可以提供一个获取整数并返回记录的get属性。是的,这很方便 - 根据您的使用情况,甚至可能是理想的。

我个人画线的方法是InsertUpdateDelete方法。除了最简单的情况之外,我们应该确定我们正在做什么。是的,创建新的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

我们现在正在定义我们用数据的内容。通用的插入,更新和删除方法不会推断我们的存储库的使用情况,并且可能会导致开发人员误用,因为他们不了解当我们真正去某事时需要发生的其他辅助事情。 / p>

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

当我们需要回答我们要返回的时,即使this[int]默认属性也会出现复杂问题。如果它是一个有很多引用的大对象,我们是否会将所有部分返回整个事物,或者我们将返回一个非常简单的POCO,需要进一步查询以填补空白。通用this[int]没有回答这个问题,但是:

GetBareBonesClient(int id);
GetClientAndProductDetail(int id);
GetClientAndContracts(int id);

在我看来,定义相当明确。在intellisense的这些日子里,编写针对您的存储库的开发人员将知道他/她需要调用什么来获得他们想要的东西。您如何确定存在多少这些方法?那么,你看看你实际开发的产品。你有什么案例可以获取数据......谁来获取数据,以及他们为什么要获取数据?大多数时候,这些都是容易回答的问题。

然而,一个常见的问题是,当我们想要允许用户浏览&#34;表格形式的数据。 &#34;给我&#39; x&#39;记录数量,按&#39; x&#39;排序字段,以分页的方式...哦,我可能会或可能不会在某些列上包含某种搜索&#34;。这种代码是您真正不想为每个存储库实现的。因此,在假设IRepositoryThatSupportsPagination中可能存在一些锅炉板查询构造的情况。我相信你能想到一个更好的名字。

显然,可能会有更多的案例。但是我永远不会将默认的CRUD操作抛到基础存储库接口/类中,因为除了不重要的,无关紧要的情况之外,它不会意味着任何事情。

答案 2 :(得分:6)

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

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

  1. 您的业务接口 - 您的客户端程序员需要调用的方法,例如GetAllEmployees(条件),UpdateEmployee(员工雇员)等。如果您有客户端/服务器体系结构,这些将对应于服务调用与数据合同。

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

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

  4. 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。注意interace 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模式中的Unit of Work,但是它使用UpdateHelper类来代替存储库对象及其Add,Update和Delete方法,根据命令模式进行更新。这允许直接编写SQL,而无需ORM映射器。

    嗯,这很长,但显然没有所有细节我的回答被认为是不值得的。我希望这会有所帮助。

答案 3 :(得分:0)

是的,您可以根据通用的常量存储库接口轻松编写优雅的DAL层。

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

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

修改

我认为你似乎错误地指出了一个具体问题:避免使用通用的ORM Mapping库意味着你没有做ORM。这不一定是真的。

除非您将类似通用数组的对象暴露给UI(这也会使关于存储库模式的讨论完全失效),否则 将关系数据转换为域对象。这正是ORM的意思:你没有使用NHibernate,EF和LINQ to SQL只是意味着你将有更多的工作。 : - )

所以,不,使用Repository Pattern仍然有意义,无论是否使用自动ORM工具。

当然,还有其他选项,例如Active Record。这是一个更简单的模式,它将域对象与数据访问逻辑混合在一起(这里使用ORM工具也是可选的)。