类型化数据集 LINQ 实体
将类型化数据集转换为 LINQ 实体,以及将 LINQ 实体转换为类型化数据集。
引言
在我之前的一篇 博文 中,我讨论了 LINQ 实体如何不适合无法持续访问数据源的应用程序。我得出的结论是,如果有一种方法可以将 LINQ 实体和类型化数据集连接起来,那么 Web 应用程序和 N 层应用程序的领域就可以由相同的业务对象模型和基于 LINQ 的数据访问层来支持。
假设 - 先决条件
实体和数据表命名
在我继续之前,必须牢记一个基本假设。业务对象模型和类型化数据集必须由 Visual Studio 中的相应设计器创建,方法是将表拖入每个设计器。主要原因是,我开发的转换器假定 LINQ 中的相应实体和 DataSet
中的表具有相同的名称。
关系和外键约束
实体之间的每个关系名称必须与 DataSet
中表之间的关系名称相同。通过使用设计器,以上内容会自动(巧合的是)保留。
尚未测试循环关系和所有组合,因此我不知道我的代码是否支持它们。
从 LINQ 构建数据库
如果您希望从 LINQ 设计器构建数据库架构,请这样做,但在创建类型化数据集之前,必须创建数据库。要做到这一点,只需调用
LinqTestDataContext ltdc = new LinqTestDataContext(connectionString);
if (!ltdc.DatabaseExists())
{
ltdc.CreateDatabase();
}
其中 LinqTestDataContext
是设计器创建的 DataContext
。
列先决条件
每个实体必须有一个 version
属性。这是因为
Attach(entity,true)
仅当存在此类属性时才有效。
用于测试的数据库架构
LINQ 架构命名为 LinqTest
,其 DataSet
表示形式为 DsLinqTest
。
如下图所示,有一个 RootElement
,其唯一键为 ID
,版本属性为 TimeStamp
,以及两个字符串属性。
RootElement
有一个 SubRootElement
实体的子关系,它们也有一个唯一的键 ID
,版本属性 TimeStamp
,一个字符串属性和一个指向其所属 RootElement
的 RootID
外键。
即使上面图像中未显示,关系名称也是相同的。
每个业务对象都位于单独的程序集中。
DataSetEntityConvertion
这是执行 LINQ 业务对象与类型化数据集之间转换的程序集名称,假设满足上述先决条件。该程序集大量使用反射和泛型,因此对它们的理解至少应比较好。请记住,由于 DataSet
是类型化的,DataSet
中的每个类型都经过特殊命名,以便可以用来发现它所关联的实体。
ToDataRow
这是使用实体填充 DataSet
中相应表的*.* 的部分。入口点是 Entity2DataSet
类,其中 TEntity
是实体类型,TDataSet
是 DataSet
类型。在我们的例子中,分别是 RootElement
和 DsLinqTest
。
基本上,Entity2DataSet
类会发现与实体对应的表,然后调用 Entity2DataRow
类,后者还会接受发现的 DataTable
类型。
有一些辅助函数,通过反射,从实体填充行,并查找实体的子关系(如果存在)。如果存在,将再次调用 Entity2DataSet
类,但这次 TEntity
应该是 SubRootElement
(在我们的例子中)。
转换的这一侧相当容易。
ToEntity
此案例涉及将整个 DataSet
转换为其实体。入口类是 DataSet2Entity
,其中 TDataContext
是我们的 DataContext
类型,TDataSet
是源 Dataset
的类型。在我们的例子中,分别是 LinqTestBigDataContext
和 DsLinqTest
。
DataSet2Entity
首先查找没有父级关系(parent relations)的表。对于这些表中的每一个,都使用 DataTable2Entity
,其中 TDataTable
和 TDataRow
分别是表及其行的类型。
DataTable2Entity
会发现必须为每一行创建的实体类型,并通过使用 DataRow2Entity
来实现,后者会知道它是否是子行。这一点至关重要,因为如果是子行,它必须添加到其父实体的相关 EntitySet
中,而不是添加到数据上下文的实体 Table
中。
这里的技巧是知道原始行是添加、修改、删除还是未更改,这通过 RowState
可以轻松实现。难点在于如何处理它。
Added
此案例很简单。只需构造实体并将其添加到表或实体集中,然后调用 InsertOnSubmit
。
修改或未修改
问题从这里开始。首先,我们必须获取我们将应用值的实体。根据行是子行还是非子行,必须构造一个谓词函数或表达式。这一部分是最困难的。
如果行未修改,则不会应用任何值。
Deleted
与修改类似,必须从数据上下文的实体表中检索实体,以便调用 DeleteOnSubmit
。
跟踪更改
当行被插入或修改时,各种列值需要由数据库的自动生成值更新。因此,对于每个实体,都会捕获 PropertyChanged
。在那里,借助字典,将新值应用于原始行。这发生在数据上下文的 SubmitChanges
被使用之后。
DataRow2Entity
的其余部分会为每个数据关系查找行的子行,并调用其另一个泛型版本。
创建谓词函数和表达式
这是最难的部分,而且我仍然有一些地方不理解。
当尝试从数据上下文的表实体中获取实体时,一个简单的委托函数就足够了。经过多次尝试,我设法使创建完全动态,基于实体的(主)键。
这是通过这两个函数完成的
private System.Func<TEntity, bool> CreatePredicateFunction(TDataRow row)
{
return p => (IsEqual(p, row));
}
private bool IsEqual(TEntity entity, TDataRow row)
{
for (int i = 0; i < Cache.EntityPrimaryKeys<TEntity>.Names.Count; i++)
{
object columnValue = null;
if (row.RowState == DataRowState.Deleted)
{
columnValue = row[Cache.EntityPrimaryKeys<TEntity>.Names[i],
DataRowVersion.Original];
}
else
{
columnValue = row[Cache.EntityPrimaryKeys<TEntity>.Names[i]];
}
if ((bool)Cache.EntityPrimaryKeys<TEntity>.EqualMethods[i].Invoke(
this.entityType.GetProperty(Cache.EntityPrimaryKeys<TEntity>.Names[i]).GetValue(
entity, null), new object[] { columnValue }) == false)
{
return false;
}
}
return true;
}
我很高兴能够将上述内容转换为 Expression<System.Func<TEntity, bool>>
,但我发现在运行时会引发一个异常,告诉我 IsEqual
无法转换或其他信息。
我假设 Expression
比委托复杂得多。因此,为了使此方法有效,必须在 DataSet
的每个 DataRow
中提供一个 CreatePredicateExpression
。我这样做了
public static class DsLinqTestPredicators
{
public static Expression<System.Func<RootElement,
bool>> CreatePredicateExpression(DsLinqTest.RootElementRow row)
{
int idValue = row.RowState == System.Data.DataRowState.Deleted ? (int)row["ID",
System.Data.DataRowVersion.Original] : row.ID;
return (Expression<System.Func<RootElement, bool>>)(p => p.ID.Equals(idValue));
}
public static Expression<System.Func<SubRootElement,
bool>> CreatePredicateExpression(DsLinqTest.SubRootElementRow row)
{
int idValue = row.RowState == System.Data.DataRowState.Deleted ? (int)row["ID",
System.Data.DataRowVersion.Original] : row.ID;
return (Expression<System.Func<SubRootElement, bool>>)(p => p.ID.Equals(idValue));
}
}
关于转换器的最后的话
扩展方法被大量使用,以帮助使转换尽可能在编程上透明。
Using the Code
Extenders
public static classDsLinqTestExtenders{
public static voidInsert(thisDsLinqTest extented, objectentity)
{
((DataSet)extented).Insert(entity);
}
public static voidInsert(thisDsLinqTest extented, object[] entities)
{
((DataSet)extented).Insert(entities);
}
public static voidToEntities(thisDsLinqTest extented, DataContext dataContext)
{
((DataSet)extented).ToEntities(dataContext);
}
}
Entity2Dataset
public DsLinqTest GetDsFromID(int id)
{
LinqTestDataContext ltdc = new LinqTestDataContext(connectionString);
RootElement re = ltdc.RootElements.Single(p => p.ID.Equals(id));
DsLinqTest ds = new DsLinqTest();
ds.Insert(re);
ds.AcceptChanges();
return ds;
}
DataSet2Entity
public void SaveGeneralDs(DsLinqTest dsLinqTest)
{
LinqTestDataContext ltdc = new LinqTestDataContext(connectionString);
dsLinqTest.ToEntities(ltdc);
ltdc.SubmitChanges();
}