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

Cod*_*uru 10 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... (...);
Run Code Online (Sandbox Code Playgroud)

EmployeeRepository

 GetEmployeeByAge ();
 GetEmployeeByJob (string description);
 GetEmployeeBySalary (double salary);
 Get... (...); //and so on
Run Code Online (Sandbox Code Playgroud)

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

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

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

interface IRepository
{
    ....
}

ProductRepository : IRepository
{
    ....
}

EmployeeRepository : IRepository
{
    ....
}
Run Code Online (Sandbox Code Playgroud)

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

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

repository = new EmployeeRepository ();
repository.Call_Employee_Methods ();
Run Code Online (Sandbox Code Playgroud)

Gab*_*pes 11

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

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

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

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

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

非常简单的IUnitOfWork:

Public interface IUnitOfWork
{
    bool Save();
}
Run Code Online (Sandbox Code Playgroud)

而他们,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);
}
Run Code Online (Sandbox Code Playgroud)

方法.Add(),. Delale()不应该向数据库发送任何内容,但是它们应该始终将更改发送到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();

        {
    {
}
Run Code Online (Sandbox Code Playgroud)

就像我说的,使用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);
        }
    } ...
Run Code Online (Sandbox Code Playgroud)

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

IEnumerable<Employee> GetEmployee(int age)
{
 return  rep.GetAll<Employee>(e=> e.Age == age);
}
Run Code Online (Sandbox Code Playgroud)

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


Moo*_*ice 6

一般而言,在我看来,通用存储库"基础"接口并没有真正解决这么多问题.有人提到它理论上可以提供一个获取整数并返回记录的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
Run Code Online (Sandbox Code Playgroud)

我们现在定义我们是什么的数据.通用插入,更新和删除方法不推断我们的代码库的使用,并通过开发谁不明白其他附属事情需要发生当我们真正去可能会导致误用什么.

那么什么是基础存储库的一个很好的例子呢?那么,实现缓存的存储库呢?基本存储库可以具有某种缓存,并且我们的派生存储库可以使用该缓存来返回陈旧数据(如果有人希望的话).

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

GetBareBonesClient(int id);
GetClientAndProductDetail(int id);
GetClientAndContracts(int id);
Run Code Online (Sandbox Code Playgroud)

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

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

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


gab*_*aim 6

[根据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();
}
Run Code Online (Sandbox Code Playgroud)

我使用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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 };
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,这需要一个将执行操作的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 };
    }
}
Run Code Online (Sandbox Code Playgroud)

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; } }
}
Run Code Online (Sandbox Code Playgroud)

3.4.您需要一个查询构建器对象.这将根据sql中的用户输入构建您的查询.例如,您可能希望按姓氏,名字,部门和加入日期搜索员工.您可以实现这样的查询界面:

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

 }
Run Code Online (Sandbox Code Playgroud)

这可以通过构建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);
        }
   }
Run Code Online (Sandbox Code Playgroud)

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

ORM真的会帮助你:

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

总的来说,这种方法使用Repository模式中的Unit of Work,但是它使用UpdateHelper类代替存储库对象及其Add,Update和Delete方法,根据命令模式进行更新.这允许直接编写SQL,而无需ORM映射器.

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


rse*_*nna 0

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

不过,它的表现很可能会非常糟糕。

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

编辑

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

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

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

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