存储库和数据映射器模式

时间:2012-01-12 23:55:27

标签: c# repository datamapper software-design

在大量关于Repository和Data Mapper的阅读之后,我决定在测试项目中实现这些模式。由于我是这些人的新手,我想了解一下如何在一个简单的项目中实现这些。

Jeremy Miller说:

  

做一些非常重要的个人编码项目,你可以自由地尝试设计模式。

但我不知道我做了所有这些事情是对的。

这是我的项目结构:

enter image description here

正如您所看到的,我将在下面详细介绍它们。

  • 域:项目域实体到这里我有一个简单的Personnel类,它继承自EntityBase类,EntityBase类有一个名为Id的属性。

    public int Id { get; set; }
    
  • Infrustructure:这是一个简单的数据访问层,有两个类。 SqlDataLayer是一个简单的类,它继承自名为DataLayer的抽象类。在这里,我提供了一些功能,如下面的代码:

    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中未实现的方法。

在Generic Repository类中,我希望构造函数中的两个参数首先是对我的SqlDataLayer类的引用,第二个是对Entity DataMapper的引用。 每个Entities Repository类发送的从Repository类继承的参数。例如:

public class PersonnelRepository : Repository<Personnel>, IPersonnelRepository {
    public PersonnelRepository(DataLayer domainContext, PersonnelDataMapper dataMapper)
        : base(domainContext, dataMapper) {

    }
}

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

  • DomainMapper:最后我在这里将IDataReader的结果映射到实体,下面是我如何映射Personnel实体的示例:

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

我知道我在这里犯了很多错误,这就是为什么我在这里。我需要你的建议才能知道我做错了什么或者这个简单的测试项目有什么好处。

提前致谢。

1 个答案:

答案 0 :(得分:32)

几点:

  1. 总体来说,你有一个很好的设计让我感到震惊。部分原因在于,您可以对其进行更改,而对变更之外的任何类别(低耦合)几乎没有影响。也就是说,它与实体框架的作用非常接近,所以虽然它是一个很好的个人项目,但我会考虑在生产项目中实施EF之前先使用EF。

  2. 您的DataMapper类可以使用反射制作通用(例如,GenericDataMapper<T>)。 Iterate over the properties of type T using reflection,并动态地从数据行中获取它们。

  3. 假设您确实创建了一个通用DataMapper,您可以考虑在DataLayer上创建一个CreateRepository<T>()方法,这样用户就不必担心要选择哪种Mapper的细节。

  4. 一个小批评 - 你假设所有实体都有一个名为“Id”的整数ID,并且存储过程将被设置为通过它来检索它们。您可以通过允许使用不同类型的主键来改进您的设计,也可以使用泛型。

  5. 您可能不希望以您的方式重复使用Connection和Command对象。这不是线程安全的,即使它是,你最终会在数据库事务周围遇到一些令人惊讶且难以调试的竞争条件。您应该为每个函数调用创建新的Connection和Command对象(确保在完成后处理它们),或者围绕访问数据库的方法实现一些同步。

  6. 例如,我建议使用这个替代版本的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();
    
    1. 最后但并非最不重要的是,与Jeremy合作我确定他会说你应该为所有这些课程进行单元测试!