仓储模式和数据映射器模式

44
在阅读了大量有关Repository和Data Mapper的文章后,我决定在一个测试项目中实现这些模式。由于我对此还不熟悉,我想知道您对我如何在简单项目中实现这些模式的看法。Jeremy Miller说:

进行某种非平凡、个人编码项目,在其中可以自由地尝试设计模式。

但我不知道我是否做得正确。

以下是我的项目结构:

enter image description here

正如您所看到的,有许多文件夹,我将在下面详细描述它们。

  • Domain : Project Domain Entities go here I've a simple Personnel class which is inherited from EntityBase class, EntityBase class has a single property named Id.

    public int Id { get; set; }
    
  • Infrustructure : Here is a simple Data Access Layer with two classes. SqlDataLayer is a simple class which is inherited from an abstract class named DataLayer. Here I provide some functionality like following code :

    public SQLDataLayer() {
        const string connString = "ConnectionString goes here";
        _connection = new SqlConnection(connString);
        _command = _connection.CreateCommand();
    }
    
在命令参数集合中添加参数:
    public override void AddParameter(string key, string value) {
        var parameter = _command.CreateParameter();
        parameter.Value = value;
        parameter.ParameterName = key;

        _command.Parameters.Add(parameter);
    }

执行DataReader:

    public override IDataReader ExecuteReader() {
        if (_connection.State == ConnectionState.Closed)
            _connection.Open();

        return _command.ExecuteReader();
    }
  • 存储库:我在这里尝试实现存储库模式。IRepository是一个通用接口。

IRepository.cs:

等等。

public interface IRepository<TEntity> where TEntity : EntityBase
{
    DataLayer Context { get; }

    TEntity FindOne(int id);
    ICollection<TEntity> FindAll();

    void Delete(TEntity entity);
    void Insert(TEntity entity);
    void Update(TEntity entity);
}

Repository.cs :

public class Repository<TEntity> : IRepository<TEntity> where TEntity : EntityBase, new() {
    private readonly DataLayer _domainContext;
    private readonly DataMapper<TEntity> _dataMapper;
    public Repository(DataLayer domainContext, DataMapper<TEntity> dataMapper) {
        _domainContext = domainContext;
        _dataMapper = dataMapper;
    }
    public DataLayer Context {
        get { return _domainContext; }
    }
    public TEntity FindOne(int id)
    {
        var commandText = AutoCommand.CommandTextBuilder<TEntity>(CommandType.StoredProcedure, MethodType.FindOne);

        // Initialize parameter and their types
        Context.AddParameter("Id", id.ToString(CultureInfo.InvariantCulture));
        Context.SetCommandType(CommandType.StoredProcedure);
        Context.SetCommandText(commandText);

        var dbReader = Context.ExecuteReader();
        return dbReader.Read() ? _dataMapper.Map(dbReader) : null;
    }

我没有暴露未实现的IRepository方法。 在这里,泛型仓储类中,我期望构造函数有两个参数,第一个是对我的SqlDataLayer类的引用,第二个是对Entity DataMapper的引用。这些参数由每个继承自Repository类的实体仓库类发送。例如:
public class PersonnelRepository : Repository<Personnel>, IPersonnelRepository {
    public PersonnelRepository(DataLayer domainContext, PersonnelDataMapper dataMapper)
        : base(domainContext, dataMapper) {

    }
}

如您在FindOne方法中所见,我试图自动化一些操作,例如创建CommandText,然后利用我的DataLayer类配置命令,最终执行命令以获取IDataReader。我将IDataReader传递给我的DataMapper类以映射到实体。

  • DomainMapper : Finally here I map result of IDataReader to Entities, bellow is a sample of how I map Personnel entity :

    public class PersonnelDataMapper : DataMapper<Personnel> {
    public override Personnel Map(IDataRecord record) {
        return new Personnel {
            FirstName = record["FirstName"].ToString(),
            LastName = record["LastName"].ToString(),
            Address = record["Address"].ToString(),
            Id = Convert.ToInt32(record["Id"])
        };
    }}
    

用法:

    using (var context = new SQLDataLayer()) {
        _personnelRepository = new PersonnelRepository(context, new PersonnelDataMapper());
            var personnel  = _personnelRepository.FindOne(1);
    }

我知道我在这里犯了许多错误,这就是我来这里的原因。我需要您的建议,以了解我做错了什么或这个简单测试项目的优点是什么。

提前致谢。


你能否多讲一些关于抽象的 DataLayer 类的内容(可能附带一些代码)? - smartcaveman
1
请问你有引用的Jeremy Miller文章链接吗? - Curtis
@Curt,找到了 - Saeb Amini
1
@saber 你好!你在Github或其他仓库里有这个示例吗?那会非常有帮助! - Marshall
1个回答

36
一些要点:
  1. 我认为整体上来说,你的设计很不错。这部分可以通过以下事实证明:在更改其内容时,对于除更改的类外的其他类影响很小(低耦合度)。话虽如此,它与Entity Framework非常接近,因此虽然它是一个很好的个人项目,但我建议在生产项目中使用EF而不是实现它。

  2. 通过使用反射,您的DataMapper类可以变成通用的(例如:GenericDataMapper<T>),并使用反射迭代类型T的属性,动态地从数据行中获取它们。

  3. 假设您确实创建了通用数据映射器,那么您可以考虑在DataLayer上创建一个CreateRepository<T>()方法,以便用户无需担心选择哪种类型的Mapper的详细信息。

  4. 一个小缺陷是,您假定所有实体都将具有名为“Id”的单个整数ID,并且将设置存储过程以按此方式检索它们。您可能可以通过允许不同类型的主键来改进设计,也许可以使用泛型。

  5. 您可能不想像您所做的那样重复使用Connection和Command对象。那不是线程安全的,即使是线程安全的,也会在DB事务周围产生一些令人惊讶且难以调试的竞争条件。您可以为每个函数调用创建新的Connection和Command对象(确保在完成后处理它们),或者在访问数据库的方法周围实现一些同步。

例如,我建议使用这个ExecuteReader的替代版本:
public override IDataReader ExecuteReader(Command command) {
    var connection = new SqlConnection(connString);
    command.Connection = connection;
    return command.ExecuteReader();
}

你的旧代码重复使用了命令对象,这可能会导致多线程调用者之间出现竞争条件。你还想创建一个新连接,因为旧连接可能正在由不同调用者启动的事务中使用。如果你想要重用事务,应该创建一个连接,开始一个事务,并重用该事务,直到你执行完想要与事务关联的所有命令。例如,你可以像这样创建ExecuteXXX方法的重载:
public override IDataReader ExecuteReader(Command command, ref SqlTransaction transaction) {
    SqlConnection connection = null;
    if (transaction == null) {
        connection = new SqlConnection(connString);
        transaction = connection.BeginTransaction();
    } else {
        connection = transaction.Connection;
    }
    command.Connection = connection;
    return command.ExecuteReader();
}    

// When you call this, you can pass along a transaction by reference.  If it is null, a new one will be created for you, and returned via the ref parameter for re-use in your next call:

SqlTransaction transaction = null;

// This line sets up the transaction and executes the first command
var myFirstReader = mySqlDataLayer.ExecuteReader(someCommandObject, ref transaction);

// This next line gets executed on the same transaction as the previous one.
var myOtherReader = mySqlDataLayer.ExecuteReader(someOtherCommandObject, ref transaction);

// Be sure to commit the transaction afterward!
transaction.Commit();

// Be a good kid and clean up after yourself
transaction.Connection.Dispose();
transaction.Dispose();

最后但同样重要的是,和Jeremy一起工作后,我敢说他会建议你为所有这些类编写单元测试!

+1 很棒的技巧 Chris,但我对第五点很好奇。请详细解释一下。你告诉我为每个函数调用创建连接,这没问题。但我应该如何处理 DbTransaction? - Saber Amani
添加了一个事务处理的基本示例。 - Chris Shain
+1,但是看起来你忽略了对象的处理。例如,SqlConnection是IDisposable。 - TrueWill
Arvand:如果你指的是解决方案容器“DataMapper”,我敢打赌它只是项目启动时的产物(也许Saber最初专注于DataMapper)。+1 给Saber和Chris! - samus

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