在设计n层应用程序时,我倾向于使用从Lhotka的CSLA框架中采用和改编的模式。简而言之,Repository层填充SqlDataReader并将数据读取器,要映射的实例和映射信息传递给Mapper层,然后Mapper层填充实例。
这种模式在我参与的许多项目中一次又一次地证明了这一点:
以下是一些示例存储库代码:
internal static List<CompositeEntities.ContactReportRpa> RetrieveByReportId(int reportId)
{
CompositeEntities.ContactReportRpa report = null;
List<CompositeEntities.ContactReportRpa> reports = new List<CompositeEntities.ContactReportRpa>();
using (SqlConnection conn = DBConnection.GetConnection())
{
cmd.Connection = conn;
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.CommandText = "ContactReportRpa_SEL_ByIdReport";
cmd.Parameters.Add("@IdReport", System.Data.SqlDbType.Int).Value = reportId;
conn.Open();
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
report = new CompositeEntities.ContactReportRpa();
ContactReportRpaMapper.Map("IdReportRpa", "IdReport", "IdRecommendation", "IsDisplayed", "Comments", report.Rpa, reader);
RpaRecommendationMapper.Map("IdRecommendation", "IdDepartment", "TitleRecommendation", "Description", "DisplayOrderRecommendation", report.Recommendation, reader);
RpaDepartmentMapper.Map("IdDepartment", "TitleDepartment", "DisplayOrderDepartment", report.Department, reader);
reports.Add(report);
}
}
}
return reports;
}
以下是一些Mapper Code示例。它非常简单:映射器知道哪个类属性被映射到数据读取器中的每个字段。每个字段的名称都会传递给映射器,因此无论分配给sproc中每个字段的名称如何,都可以使用相同的映射器。
internal static void Map(string fieldId, string fieldName, string fieldDisplayOrder, RpaDepartment entity, SqlDataReader reader)
{
entity.Id = reader.GetInt32(reader.GetOrdinal(fieldId));
entity.Title = reader.GetString(reader.GetOrdinal(fieldName));
entity.DisplayOrder = reader.GetInt32(reader.GetOrdinal(fieldDisplayOrder));
}
所以我的问题是:当数据源是文本文件时,我应该如何实现这种模式?我想坚持使用这种模式,因为最终数据源将迁移到数据库。
有什么建议吗?
编辑:ini文件已存在,此时我没有继续更改它们。所以我暂时坚持使用它们。
答案 0 :(得分:3)
在ini文件中创建良好的数据层次结构有点困难。这是MS似乎主要迁移到XML文件的部分原因。请参阅此问题的答案:Reading/writing an INI file
如果你使用XML选项,我会跳过这个映射的东西,并在使用XPath之后直接序列化你的对象以找到适当的XML。那你就不需要映射器了。
您也可以使用an in-memory or file-based DB, like SqLite。 Perf会很棒,而且你的部署空间非常小。
另外,我建议避免尝试抽象这个映射器的东西,因为我认为它不会在DB和ini文件之间很好地转换。如果你看一下那里的许多ORM库的复杂性,你会发现这个映射真的很难。映射级别的大多数概念都不能很好地转换为ini文件。它是映射(存储库)的更高级别概念,这就是我发布原始答案的原因(见下文)。
但是如果你想保留你正在使用的模式,你的ini文件看起来像这样:
[Report.3]
IdReport = 3
IdReportRpas = 7,13
[ReportRpa.7]
IdReportRpa = 7
IdReport = 3
IdRecommendation = 12
IsDisplayed = true
Comments = I'm not sure what an RPA is...
[ReportRpa.13]
IdReportRpa = 13
IdReport = 3
; ... and rest of properties here
[Recommendation.12]
IdRecommendation = 12
IdDepartment = 33
TitleRecommendation = Some Recommendation
Description = Some Recommendation Description
DisplayOrderRecommendation = 0
[Department.33]
IdDepartment = 33
TitleDepartment = Bureau of DBs and ini files
DisplayOrderDepartment = 0
...然后您可以简单地编写存储库以从ini部分中获取数据,并编写映射器以查看每个ini值,就像您当前查看结果集中的列一样。
using(var iniFileReader = new IniFileReader())
{
string reportSectionName = string.Format("Report.{0}", contactId);
var reportSection = iniFileReader.GetSection(reportSectionName);
// Todo: Abstract this sort of procedure/enumeration stuff out.
// Similar to the existing code's stored procedure call
int[] idReportRpas = reportSection.GetValue(IdReportRpas)
.Split(',')
.Select(s => int.Parse(s);
foreach(string idReportRpa in idReportRpas)
{
report = new CompositeEntities.ContactReportRpa();
string rpaSectionName = string.Format("ReportRpa.{0}", idReportRpa);
var rpaSection = iniFileReader.GetSection(rpaSectionName);
ContactReportRpaMapper.Map("IdReportRpa", "IdReport", "IdRecommendation",
"IsDisplayed", "Comments", report.Rpa, rpaSection);
// ...
}
}
您当前的映射器代码绑定到您的存储类型,因此您需要提供更通用的映射器接口。或者使最后一个读者参数更通用以支持两种映射类型(在您的情况下,reader
,在我的情况下,每个ini部分实例)。 E.g:
public interface IDataValueReader
{
// Signature is one that might be able to support ini files:
// string -> string; then cast
//
// As well as a DB reader:
// string -> strongly typed object
T ReadValue<T>(string valueName);
}
public class DbDataReader : IDataValueReader
{
private readonly SqlDataReader reader;
public DbDataReader(SqlDataReader reader)
{
this.reader = reader;
}
object ReadValue<T>(string fieldId)
{
return (T)reader.GetObject(reader.GetOrdinal(fieldId));
}
}
public class IniDataSectionReader : IDataValueReader
{
private readonly IniFileSection fileSection;
public IniDataSectionReader(IniFileSection fileSection)
{
this.fileSection = fileSection;
}
object ReadValue<T>(string valueName)
{
return (T)Convert.ChangeType(fileSection.GetValue(fieldId), typeof(T));
}
}
请注意,这是所有自定义代码 - 没有官方的ini文件阅读器,我还没有尝试过,所以我无法建议使用哪个第三方库。 That question I linked at the top有一些建议。
(部分内容可能仍然有用)
为repository制作interface
,并确保代码的更高级别层只通过此界面与您的数据存储区通信。
示例界面(您的界面可能不同):
public interface IReportRepository
{
void Create(Report report);
Report Read(int id);
void Update(Report report);
void Delete(Report report);
}
如果您愿意,也可以将此界面设为通用。
要确保更高级别的层只知道存储库,您可以构建用于在IReportRepository
的实现中与文件/ DB进行通信的类,或使用Dependency Injection来填充它。但无论你做什么,除了IRepository
和你的个人数据实体(Report
)之外,不要让你的更高级代码知道任何事情。
您可能还想查看the Unit of Work pattern,并在那里包装实际的数据访问权限。这样您就可以轻松支持transactional semantics和缓冲/延迟存储访问(即使使用文件)。
对于您的示例实现,SqlConnection
和SqlDataReader
将存在于您的工作单元类中,并且映射代码和特定存储过程名称可能存在于每个存储库类中。
让这个结构完全独立地工作可能有点棘手,但是如果你看一下Microsoft Entity Framework生成的代码,他们实际上让他们的工作单元实例化每个存储库,你只需要访问它就像一个属性。大致像:
public interface IUnitOfWork : IDisposable
{
void CommitChanges();
void RollbackChanges();
}
public class MyDataModel : IUnitOfWork
{
private bool isDisposed;
private readonly SqlConnection sqlConnection;
public MyDataModel()
{
sqlConnection = DBConnection.GetConnection();
}
// Todo: Implement IUnitOfWork here
public void Dispose()
{
sqlConnection.Dispose();
isDisposed = true;
}
public IRepository<Report> Reports
{
get
{
return new ReportDbRepository(sqlConnection);
}
}
}
public class ReportDbRepository : IRepository<Report>
{
private readonly SqlConnection sqlConnection;
public ReportDbRepository(SqlConnection sqlConnection)
{
this.sqlConnection = sqlConnection;
}
// Todo: Implement IRepository<Report> here using sqlConnection
}
有用的阅读:
答案 1 :(得分:0)
您可以为INI文件实现相同的模式,尽管使用P / Invoke进行调用会有点麻烦。基本思想是调用GetPrivateProfileSectionNames以获取INI文件中的节名称列表。然后为每个部分名称调用GetPrivateProfileSection以获取该部分的键和值的列表。从那里你可以解析键和值并填充你的列表。
请参阅对Reading/writing an INI file的响应,以获取将读取INI文件的代码的指针。