10

我正在使用业务对象(员工、产品)创建一个新项目。由于限制,我没有使用 LINQ to SQL 或任何 ORM 映射器。

我必须手动编码数据访问层。我对使用“存储库模式”感兴趣。

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

令我困惑的是,不同的业务对象有不同的要求。例如:

产品资料库

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

员工资料库

 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 ();
4

4 回答 4

12

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

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

在此之后,如果您有任何特定于某些实体的特定方法,您都可以从存储库继承并覆盖或添加方法和属性作为 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 类中实现),并且仅当您调用 .Save() IUnitOfWork 的方法,您将把内容保存到数据库中。

我已经使用 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);
}

或者您可以直接调用(无需创建方法)

于 2013-05-15T20:42:03.883 回答
6

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

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

  1. 您的业​​务接口 - 您的客户端程序员需要调用的方法,例如 GetAllEmployees(criteria)、UpdateEmployee(Employee employee) 等。如果您有客户端/服务器架构,这些将对应于带有数据合同的服务调用。

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

如您所见,这需要一个将执行操作的命令(即命令模式)。基本命令实现:

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。

总体而言,此方法使用存储库模式中的工作单元,但它使用 UpdateHelper 类基于命令模式进行更新,而不是存储库对象及其添加、更新和删除方法。这允许直接编写 SQL,而无需 ORM 映射器。

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

于 2013-05-15T20:58:05.293 回答
6

一般来说,在我看来,通用存储库“基础”接口并不能真正解决那么多问题。有人提到,理论上它可以提供一个获取整数并返回记录的属性。是的,这很好也很方便 - 根据您的用例,甚至可能是可取的。

我个人划线的地方是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

我们现在正在定义我们数据的处理方式。通用的插入、更新和删除方法不会推断我们存储库的使用情况,并且可能会导致开发人员误用,他们不了解我们实际去做某事时还需要发生什么其他辅助事情

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

this[int]当我们需要回答我们将要返回的内容时,即使是默认属性也有复杂问题。如果它是一个有很多引用的大对象,我们是要返回整个事物及其所有部分,还是要返回一个非常简单的 POCO,需要进一步查询来填补空白。泛型this[int]不能回答这个问题,但是:

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

在我看来定义得很好。在智能感知的这些日子里,针对您的存储库进行编码的开发人员将知道他/她需要调用什么来获取他们想要的东西。您如何确定这些方法中有多少种存在?好吧,你看看你实际开发的产品。您有哪些获取数据的案例……谁在获取数据,他们为什么要获取数据?大多数时候,这些都是很容易回答的问题。

然而,一个常见的问题是,当我们希望允许用户以表格形式“浏览”数据时。“给我'x'条记录,按'x'字段排序,以分页方式......哦,我可能会或可能不会在某些列上包含某种搜索”。这种代码是您真的不想为每个存储库实现的东西。因此,在假设的IRepositoryThatSupportsPagination. 我相信你能想到一个更好的名字。

显然,可能还有更多的案例。但我永远不会将默认操作放入基本存储库接口/类中,因为除了不重要、微不足道的情况外,CRUD它没有任何意义。

于 2013-05-15T20:38:03.563 回答
0

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

不过,它很可能会有一个可笑的糟糕表现。

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

编辑

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

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

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

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

于 2013-05-15T19:51:29.400 回答