如何使用Dapper实现通用仓储设计模式?

28
我正在使用Dapper作为MicroORM,用于检索和保存数据到SQL Server 2014。我有DTO类在DTO Proj中,它们表示从数据库检索的数据或保存到数据库中的数据。
我正在使用存储库模式,因此在我的服务层中,如果需要存储库,则使用构造函数DI来注入该依赖项,然后调用存储库上的方法来完成工作。
假设我有两个名为CustomerService和CarService的服务。
然后我有两个存储库,一个是CustomerRepository,另一个是CarRepository。
我有一个接口定义了每个存储库中的所有方法,然后是具体实现。
下面显示了一个示例方法(调用存储过程执行DB INSERT(请注意,存储过程的实际字符串变量在类的顶部定义为私有字符串):
    public void SaveCustomer(CustomerDTO custDTO)
    {
        using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
        {
            db.Execute(saveCustSp, custDTO, commandType: CommandType.StoredProcedure);
        }
    }

这一切都很好,但我发现在每个存储库的每个方法中都要重复使用using块。我有以下两个真正的问题。
1. 是否有更好的方法可以使用,可能是通过使用每个其他存储库都继承自的BaseRepository类,并且Base将实现DB连接的实例化?
2. 对于系统上的多个并发用户,这是否仍然有效?
基于Silas的答案,我创建了以下内容:
public interface IBaseRepository
{
    void Execute(Action<IDbConnection> query);
}

public class BaseRepository: IBaseRepository
{
        public void Execute(Action<IDbConnection> query)
        {
            using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
            {
                query.Invoke(db);
            }
        }
}

然而,在我的代码库中,我还有其他方法,如下所示:

    public bool IsOnlyCarInStock(int carId, int year)
    {
        using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
        {
            var car = db.ExecuteScalar<int>(anotherStoredSp, new { CarID = carId, Year = year },
                                commandType: CommandType.StoredProcedure);

            return car > 0 ? true : false;
        }
    }

并且

    public IEnumerable<EmployeeDTO> GetEmployeeDetails(int employeeId)
    {
        using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
        {
            return db.Query<EmployeeDTO>(anotherSp, new { EmployeeID = employeeId },
                                commandType: CommandType.StoredProcedure);
        }
    }

使用泛型类型T将它们添加到我的基础存储库的正确方法是什么,这样我就可以返回任何类型的DTO或任何C#本机类型。


这是实现的方式,您需要使您的 BaseRepository 可被处理以处理 IDbConnection。您可以查看 Microsoft 文档中有关使用存储库模式和工作单元模式的信息 https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application - OrcusZ
1
“using”块是必要的恶,因为您正在打开需要关闭的数据库连接。所以重复是必要的。我只建议不要陷入整个存储库设计模式的东西…… - Callum Linington
@Callum - 你能提出其他的设计模式吗?或者你可以举个例子来说明。我曾考虑过使用CQRS,但基于KISS原则,我觉得上述的repository对我来说已经足够了。 - Ctrl_Alt_Defeat
2
虽然有点跑题,但是CustomerDTO应该改成CustomerDto。类名需要使用PascalCase(Microsoft推荐的命名规范)。由于DTO的缩写大于2个字符,需要将其改为Dto。 - David Klempfner
3个回答

22
当然,创建和释放连接的函数将非常有效。
protected void Execute(Action<IDbConnection> query)
{
    using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
    {
        query.Invoke(db);
    }
}

而你的简化调用站点:

public void SaveCustomer(CustomerDTO custDTO)
{
    Execute(db => db.Execute(saveCustSp, custDTO, CommandType.StoredProcedure));
}

有返回值的函数:

public T Get<T>(Func<IDbConnection, T> query)
{
    using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
    {
        return query.Invoke(db); 
    }
}

在您的调用位置,只需编写您希望使用的逻辑。
public IEnumerable<EmployeeDTO> GetEmployeeDetails(int employeeId)
{
    return Get<IEnumerable<EmployeeDTO>(db => 
        db.Query<EmployeeDTO>(anotherSp, new { EmployeeID = employeeId }, CommandType.StoredProcedure));
}

1
如果您预计会有许多需要此功能的类,则基础存储库是一个很好的选择。 - Silas Reinagel
1
调用查询/操作将适用于您希望使用IDbConnection执行的任何操作,包括Dapper方法和非Dapper方法。 - Silas Reinagel
2
我添加了代码,应该可以处理任何查询场景。 - Silas Reinagel
@SilasReinagel,每个查询都开启新连接好吗?我对Dapper不熟悉,不知道如何使用它来实现我的数据访问层。这样做会有性能问题吗? - Viacheslav Yankov
1
@syler 这取决于您的使用情况。如果您关心性能并且遇到了问题,那么运行分析器以查看手动管理连接是否更高效。一般来说,我会将决策推迟给Dapper。 - Silas Reinagel
显示剩余3条评论

15

这与你的问题不直接相关。但我建议你考虑使用DapperExtensions。

最初,我使用Dapper实现了存储库模式。缺点是我不得不在各个地方编写查询语句,这非常繁琐。由于硬编码的查询,几乎不可能编写通用存储库。

最近,我升级了我的代码以使用DapperExtensions。这解决了很多问题。

以下是通用存储库:

public abstract class BaseRepository<T> where T : BasePoco
{
    internal BaseRepository(IUnitOfWork unitOfWork)
    {
        dapperExtensionsProxy = new DapperExtensionsProxy(unitOfWork);
    }

    DapperExtensionsProxy dapperExtensionsProxy = null;

    protected bool Exists()
    {
        return (GetCount() == 0) ? false : true;
    }

    protected int GetCount()
    {
        var result = dapperExtensionsProxy.Count<T>(null);
        return result;
    }

    protected T GetById(Guid id)
    {
        var result = dapperExtensionsProxy.Get<T>(id);
        return result;
    }
    protected T GetById(string id)
    {
        var result = dapperExtensionsProxy.Get<T>(id);
        return result;
    }

    protected List<T> GetList()
    {
        var result = dapperExtensionsProxy.GetList<T>(null);
        return result.ToList();
    }

    protected void Insert(T poco)
    {
        var result = dapperExtensionsProxy.Insert(poco);
    }

    protected void Update(T poco)
    {
        var result = dapperExtensionsProxy.Update(poco);
    }

    protected void Delete(T poco)
    {
        var result = dapperExtensionsProxy.Delete(poco);
    }

    protected void DeleteById(Guid id)
    {
        T poco = (T)Activator.CreateInstance(typeof(T));
        poco.SetDbId(id);
        var result = dapperExtensionsProxy.Delete(poco);
    }
    protected void DeleteById(string id)
    {
        T poco = (T)Activator.CreateInstance(typeof(T));
        poco.SetDbId(id);
        var result = dapperExtensionsProxy.Delete(poco);
    }

    protected void DeleteAll()
    {
        var predicateGroup = new PredicateGroup { Operator = GroupOperator.And, Predicates = new List<IPredicate>() };
        var result = dapperExtensionsProxy.Delete<T>(predicateGroup);//Send empty predicateGroup to delete all records.
    }

如上所示,大部分方法只是对底层的 DapperExtensionsProxy 类进行了包装。 DapperExtensionsProxy 内部还管理着 UnitOfWork,可以在下面看到。这两个类可以无缝结合。我个人更喜欢将它们分开。
您还可以注意到其他一些方法,例如 ExistsDeleteByIdDeleteAll,这些方法不是 DapperExtensionsProxy 的一部分。
每个 POCO 类中都定义了 poco.SetDbId 方法来设置其标识符属性。在我的情况下,POCO 的标识符可能具有不同的数据类型和名称。
以下是 DapperExtensionsProxy
internal sealed class DapperExtensionsProxy
{
    internal DapperExtensionsProxy(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    IUnitOfWork unitOfWork = null;

    internal int Count<T>(object predicate) where T : BasePoco
    {
        var result = unitOfWork.Connection.Count<T>(predicate, unitOfWork.Transaction);
        return result;
    }

    internal T Get<T>(object id) where T : BasePoco
    {
        var result = unitOfWork.Connection.Get<T>(id, unitOfWork.Transaction);
        return result;
    }

    internal IEnumerable<T> GetList<T>(object predicate, IList<ISort> sort = null, bool buffered = false) where T : BasePoco
    {
        var result = unitOfWork.Connection.GetList<T>(predicate, sort, unitOfWork.Transaction, null, buffered);
        return result;
    }

    internal IEnumerable<T> GetPage<T>(object predicate, int page, int resultsPerPage, IList<ISort> sort = null, bool buffered = false) where T : BasePoco
    {
        var result = unitOfWork.Connection.GetPage<T>(predicate, sort, page, resultsPerPage, unitOfWork.Transaction, null, buffered);
        return result;
    }

    internal dynamic Insert<T>(T poco) where T : BasePoco
    {
        var result = unitOfWork.Connection.Insert<T>(poco, unitOfWork.Transaction);
        return result;
    }

    internal void Insert<T>(IEnumerable<T> listPoco) where T : BasePoco
    {
        unitOfWork.Connection.Insert<T>(listPoco, unitOfWork.Transaction);
    }

    internal bool Update<T>(T poco) where T : BasePoco
    {
        var result = unitOfWork.Connection.Update<T>(poco, unitOfWork.Transaction);
        return result;
    }

    internal bool Delete<T>(T poco) where T : BasePoco
    {
        var result = unitOfWork.Connection.Delete<T>(poco, unitOfWork.Transaction);
        return result;
    }

    internal bool Delete<T>(object predicate) where T : BasePoco
    {
        var result = unitOfWork.Connection.Delete<T>(predicate, unitOfWork.Transaction);
        return result;
    }
}

以下是上面使用的BasePoco
public abstract class BasePoco
{
    Guid pocoId = Guid.NewGuid();

    public Guid PocoId { get { return pocoId; } }

    public virtual void SetDbId(object id)
    {//Each POCO should override this method for specific implementation.
        throw new NotImplementedException("This method is not implemented by Poco.");
    }

    public override string ToString()
    {
        return PocoId + Environment.NewLine + base.ToString();
    }
}

这也使用了UnitOfWork,这在这里有解释。


很好,@Amit Joshi...如果我有多个数据库连接字符串,它会起作用吗?是的,请建议需要做哪些最小更改?请建议。 - Ajt
1
@LajithKumar:不需要进行任何更改即可使其与多个数据库实例一起使用。请注意,UnitOfWork是以这种方式注入Repository的BaseRepository(IUnitOfWork unitOfWork)。您可以为每个数据库实例创建新的UnitOfWork,并将其注入到同一类(Repository的新实例)中,而无需进行任何更改。 - Amit Joshi

1
我知道这是一个非常古老的问题,但我仍然想提出建议。 Dapper.SimpleRepository 是一个 NuGet 包,已经为您创建了一个基于 Dapper 的存储库。它为您提供了基本的 CRUD 方法以及使用过滤器、完整查询、存储过程等功能的能力。它支持异步和非异步。并且它将与 Framework、Standard 和 Core 一起工作。 它给你两个选项。假设 Foo 是一个反映数据库表的 C# 类... 选项 1: 通过注入连接字符串并定义类型来创建存储库。
Dapper.SimpleRepository.Repository<Foo> fooRepo = new Dapper.SimpleRepository.Repository<Foo>("your connection string");

然后,基本的CRUD操作就像这样简单:
fooRepo.Insert(foo);    // Add a record to the database
fooRepo.Get(55);             // Get a sinlge item from the database by Id
fooRepo.Update(foo);    // Update a record in the database
fooRepo.Delete(55);          // Delete a single object from the database by Id

选项2:通过注入连接字符串创建您的存储库,但不要定义类型。
Dapper.SimpleRepository.Repository repo = new Dapper.SimpleRepository.Repository("your connection string");

然后你的 CRUD 方法看起来像这样:

repo.Insert<Foo>(foo);    // Add a record to the database
repo.Get<Foo>(55);        // Get a sinlge item from the database by Id
repo.Update<Foo>(foo);    // Update a record in the database
repo.Delete<Foo>(55);     // Delete a single object from the database by Id

对于超出基本crud的所有方法(有很多),请参阅GitHub页面。

(完全透明...我创建了NuGet包。)


1
存储库模式的关键之一不是应该将存储库直接映射到单个数据库表,而是映射到域级别聚合根对象吗?我不知道SimpleDapper如何实现这一点。 - Neutrino
存储库模式的另一个关键方面不是客户端域对象与特定数据访问实现的细节隔离吗?但在这种情况下,客户端使用原始SQL字符串检索过滤后的域对象。也许我错了,但这对我来说似乎根本不是存储库模式的真正实现,只是一个简单的数据访问类。 - Neutrino
如果数据库中的ID是GUID,会怎样? - Bjørn
@Bjørn 好问题!我可能需要测试一下。但现在,您可以使用带有WHERE子句的SELECT语句获取记录。 - Casey Crookston
@Neutrino,在此之前不确定为什么错过了您的评论。您是正确的,这并不是经典意义上的真正存储库。但是,Dapper并不打算在那个领域中运作。这只是一个快速,轻便的工具,几乎像存储库一样运作,而无需复杂的工作单元使用。 - Casey Crookston
1
在我最近的一次尝试中,我创建了一个可以处理所有类型ID的存储库。我还不确定是否对此感到满意,但它使用通用身份对象,该对象使用数据层实现使用的任何内容,这非常方便。到目前为止,我还没有遇到任何问题,但我必须在应用程序代码中使用身份对象,而不知道ID是int、long、guid还是其他任何对象。 - Bjørn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接