LINQ-to-SQL 的动态表映射





5.00/5 (17投票s)
LINQ-to-SQL 动态表映射,适用于数据水平分区(分片)。
引言
在实际应用中,大型数据集的水平分区非常流行。这种分区,有时也称为“分片”,可以帮助减少查询在获得结果前需要处理的数据量。参与此类分区的表之间仅名称不同。它们应具有相同的字段结构。
使用 LINQ-to-SQL 访问这些表会带来一个问题。LINQ-to-SQL 要求一个类映射一个表。这意味着对于分区中的每个可能的表,都必须定义一个类。有人可能会争辩说,动态定义类并在 DataContext.GetTable<TEntity>()
中使用它们不是问题。但这种方法的 C# 结果并不令人满意。对于这些动态定义的类,只能在代码中显式使用 ITable()
接口,因此 Intelli-sense 无法提供帮助来识别元素类型,编译器也无法执行语法检查,漂亮的 LINQ 语句也无法使用。即使这些类继承自某个基类,在这种情况下也没有帮助。事实是 ITable<Derived>()
可以赋值给 ITable<Base>()
。
一种尝试解决此 LINQ-to-SQL 问题的失败尝试是尝试动态地将表名与类定义关联起来,以便在需要映射信息时,返回关联的表名,而不是 TableAttribute()
指定的名称。这是一种死胡同。存储在 Type()
实例中的类型信息对所有类定义都是单例的。并且由于 LINQ 的延迟执行特性,无法唯一地识别事先指定的表名。
因此出现了两种解决方案,其中一种仅在 .NET 4.0 上可用。
方法 1:使用等效查询,适用于 3.5+。
该方法是使用一个包装类,并将对其执行的操作转换为对底层对象的 C# 操作。也就是说,将新类视为一个表,并将 CRUD 操作下放到底层表中。
包装类使用等效查询进行读取操作。要最终读取数据,需要动态定义一个具有必要映射的新类,并将其用作 DataContext.GetTable<TEntity>()
中的 TEntity
。由于 LINQ-to-SQL 的读取操作仅使用 IQueryable<TEntity>()
接口,并且不显式区分表或查询。因此,包装器返回等效查询:
from r in DataContext.GetTable<DynamicEntity>() select (Base)r
每当需要表时,并且进一步的查询组合都不是问题。
通过等效查询解决了读取操作,更新和删除操作也得到了解决。等效查询返回的对象实际上直接来自底层表。对这些对象的任何更改都将被数据上下文跟踪,并在提交时保存到数据库。删除也是如此。
创建新记录或插入对象有所不同。因为代表新记录的对象不属于底层表的行类型。但是,这两种类型都可以看作是 Base()
类型,并且可以通过简单的克隆将所有必要的值复制到要插入到底层表中的最终对象中。有一点需要特别注意。由于值是从输入对象复制到表对象中的,因此稍后对表对象的任何更改都不会反映回输入对象。下面的部分提供了一个方法。然而,水平分区表并不总是具有数据库生成的 C# 值,并且插入可以与其他操作分开,因此可以避免这种 C# 差异问题。
IQueryable<TEntity>()
和 ITable()
接口尚未完全为该项目实现。实现其他接口应该是微不足道的,因为方法的核心内容已经呈现。
主要实现如下所示。
public static ATable<TEntity> GetTable<TEntity>(this DataContext context, string name)
where TEntity : class
{
// Create the entity type
Type type = DefineEntityType(typeof(TEntity), name);
// Create the underlying table
ITable refer = context.GetTable(type);
// New instance of the wrapper
return new ATable<TEntity>(refer, name);
}
public class ATable<TEntity> : IQueryable<TEntity>, ITable
where TEntity : class
{
/// <summary>
/// Equivalent query
/// </summary>
private IQueryable _equivalent;
/// <summary>
/// Supporting table
/// </summary>
private ITable _table;
public ATable(ITable inner, string name)
{
// Supporting table
_table = inner;
// Get the "Select" method
MethodInfo select = GetGenericSelect();
MethodInfo invokable = select.MakeGenericMethod(_table.ElementType, typeof(TEntity));
// Prepare a conversion lambda
ParameterExpression param = Expression.Parameter(_table.ElementType, "r");
Expression body = Expression.Convert(param, typeof(TEntity));
LambdaExpression lambda = Expression.Lambda(body, param);
// Invoke the select and get the equivalent query from the supporting table
_equivalent = (IQueryable)invokable.Invoke(null, new object[] { _table, lambda });
}
/// <summary>
/// Retrieve the right "Select" method from the Queryable
/// </summary>
/// <returns></returns>
private static MethodInfo GetGenericSelect()
{
foreach (var method in typeof(Queryable)
.GetMethods(BindingFlags.Static | BindingFlags.Public))
{
if ((method.Name == "Select") &&
(method.GetParameters()[1].ParameterType.GetGenericArguments()[0]
.GetGenericArguments().Length == 2))
return method;
}
throw new Exception();
}
#region IQueryable interface implementation (all from equivalent)
public IEnumerator<TEntity> GetEnumerator()
{
return (IEnumerator<TEntity>)_equivalent.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _equivalent.GetEnumerator();
}
public Type ElementType
{
get { return _equivalent.ElementType; }
}
public Expression Expression
{
get { return _equivalent.Expression; }
}
public IQueryProvider Provider
{
get { return _equivalent.Provider; }
}
#endregion
#region ITable interface implementation
public void InsertOnSubmit(object entity)
{
// Input validation
if ((entity == null) || (!typeof(TEntity).IsAssignableFrom(entity.GetType())))
return;
// Create a new table row
TEntity instance = (TEntity)Activator.CreateInstance(_table.ElementType);
// Transfer values into the table row
foreach (var prop in typeof(TEntity).GetProperties())
prop.SetValue(instance, prop.GetValue(entity, null), null);
// Submission
_table.InsertOnSubmit(instance);
if (entity is Wrapper<TEntity>)
((Wrapper<TEntity>)entity).Inner = instance;
}
public void DeleteOnSubmit(object entity)
{
_table.DeleteOnSubmit(entity);
}
#endregion
方法 2:适用于 4.0+
不确定如何命名此解决方案。简而言之,该解决方案是使用一个包装 ITable<TEntity>()
的类,该类包装一个 ITable()
对象。事实上,内部的 ITable()
对象实际上是 ITable<DynamicClass>()
,它保存着表名。动态类是 TEntity()
的派生类,没有额外的属性和字段。因此,动态类可以被视为 TEntity()
,这将满足 .NET 运行时在从数据库返回 C# 结果时的检查。另一方面,动态类实例可以转换为 TEntity()
而不丢失任何重要信息。
这是一个偶然的发现,因为在 ITable<TEntity>()
实现中,TEntity()
由 Intelli-sense 引用并由编译器使用,而 LINQ-to-SQL 使用属性表达式和提供程序。这种信息的解耦允许进行这种技巧。
在此解决方案中,有两个实现块比其他块更重要。一个是创建带有 TableAttribute()
和 ColumnAttribute()
的动态类。另一个是可执行的委托,用于从 TEntity()
实例克隆动态类实例,这仅在执行插入时需要。这两个代码块的实现都没有 C# 技巧,并且易于在附加代码中理解。
对象数据同步问题在此方法中也会发生。请检查以下 C# 代码以了解解决方案。
以下是此表包装器的实现。
/// <summary>
/// Retrieve a table from the data context which implements
/// ITable<TEntity> uses specific backing table
/// </summary>
/// <typeparam name="TEntity">Entity Type</typeparam>
/// <param name="context">Data context</param>
/// <param name="name">Table name</param>
/// <returns></returns>
public static ATable<TEntity> GetTable<TEntity>(this DataContext context, string name)
where TEntity : class
{
// Create/Retrieve a type definition for the table using the TEntity type
var type = DefineEntityType(typeof(TEntity), name);
// Create the backup table using the new type
var refer = context.GetTable(type);
// Prepare the cloning method
var cloneFrom = CompileCloning(typeof(TEntity), type);
// Construct the table wrapper
return new ATable<TEntity>(refer, cloneFrom);
}
/// <summary>
/// A table wrapper implements ITable<TEntity> backed by other ITable object
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ATable<TEntity> : ITable<TEntity> where TEntity : class
{
/// <summary>
/// Backing table
/// </summary>
private readonly ITable _internal;
/// <summary>
/// Cloning method
/// </summary>
private readonly Delegate _clone;
/// <summary>
/// Construct from backing table
/// </summary>
/// <param name="inter"></param>
/// <param name="from"></param>
public ATable(ITable inter, Delegate from)
{
_internal = inter;
_clone = from;
}
public void Attach(TEntity entity)
{
throw new NotImplementedException();
}
public void DeleteOnSubmit(TEntity entity)
{
// Directly invoke the backing table
_internal.DeleteOnSubmit(entity);
}
public void InsertOnSubmit(TEntity entity)
{
// Input entity must be changed to backing type
var v = _clone.DynamicInvoke(entity);
// Invoke the backing table
_internal.InsertOnSubmit(v);
}
public IEnumerator<TEntity> GetEnumerator()
{
throw new NotImplementedException();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
public Type ElementType { get { return _internal.ElementType; } }
public System.Linq.Expressions.Expression Expression { get { return _internal.Expression; } }
public IQueryProvider Provider { get { return _internal.Provider; } }
}
对于普通的 CRUD 操作,不必实现所有 ITable<TEntity>()
接口方法。
如何使用以接口定义的通用结构。
这些表的通用结构可以在接口中定义,并直接附加 ColumnAttribute()
。动态类将具有附加到相应属性的所有 ColumnAttribute()
的副本。为了执行插入,需要一个实现该接口的类,并附加 ColumnAttribute()
。
以下是使用接口的示例。
/// <summary>
/// Result table interface
/// </summary>
public interface IResult
{
[Column(IsPrimaryKey = true)]
int Id { get; set; }
[Column]
string Name { get; set; }
[Column]
double Value { get; set; }
}
/// <summary>
/// A implementation of the "result"
/// </summary>
public class ResultImp : IResult
{
public int Id { get; set; }
public string Name { get; set; }
public double Value { get; set; }
}
// Sample reading
var context = new DataContext(SQLTest);
var table = context.GetTable<iresult>("result2012");
var query = from r in table where r.Id == 108 select r;
var list = query.ToList();
// Insertion
table.InsertOnSubmit(
new ResultImp { Id = NewId, Name = "Newly added", Value = 230.4595 });
context.SubmitChanges();</iresult>
如何使用以类定义的通用结构。
这些表的通用结构也可以在类中定义,并具有虚拟属性。但是,在 4.0+ 方法中不能将 ColumnAttribute()
用于属性,因为它会混淆 LINQ-to-SQL 运行时,因为动态派生的类将具有另一个具有相同属性名称和列名称定义的属性。因此,使用 ColumnAttribute()
的克隆来携带映射信息。3.5+ 方法仍然可以使用 ColumnAttribute()
。
以下是在 4.0+ 中使用基类的示例。3.5+ 基类仅在列属性方面有所不同。
/// <summary>
/// Result table class
/// </summary>
public class AResult
{
[AlterColumn(IsPrimaryKey = true)]
public virtual int Id { get; set; }
[AlterColumn]
public virtual string Name { get; set; }
[AlterColumn]
public virtual double Value { get; set; }
}
// Read from database
var context = new DataContext(SQLTest);
var table = context.GetTable<AResult>("result2012");
var query = from r in table where r.Id == 108 select r;
var list = query.ToList();
// Insertion
table.InsertOnSubmit(
new AResult { Id = NewId, Name = "Newly added", Value = 230.4595 });
context.SubmitChanges();
如何同步插入的对象。
这是通过使用基数据对象的轻量级包装器对象来完成的。在插入时实例化的基数据对象使用其自身的字段来存储值。在将其传递给 ATable<TEntity>InsertOnSubmit()
之后,实际的表对象将被附加。对虚拟属性的所有读取或写入都会同步到下面的表对象。
以下是同步后的基对象的外观。
public class VResult : Utility.Wrapper<VResult>
{
private int _id;
private string _name;
private double _value;
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public virtual int Id
{
get { return Inner == null ? _id : Inner.Id; }
set { if (Inner == null) _id = value; else Inner.Id = value; }
}
[Column]
public virtual string Name
{
get { return Inner == null ? _name : Inner.Name; }
set { if (Inner == null) _name = value; else Inner.Name = value; }
}
[Column]
public virtual double Value
{
get { return Inner == null ? _value : Inner.Value; }
set { if (Inner == null) _value = value; else Inner.Value = value; }
}
}
同步发生在 ATable<TEntity>.InsertOnSubmit()
的以下 C# 代码行中。
if (entity is Wrapper<TEntity>)
((Wrapper<TEntity>)entity).Inner = instance;
运行示例。
3.5+ 示例是用 Visual Studio 2008 和 .NET 3.5 编写的。它在 .NET 4 和 .NET 4.5 上运行(已在 Visual Studio 2011 Beta 中测试)。只需更改配置即可使其在新框架下运行。
4.0+ 示例是用 Visual Studio 2010 和 .NET 4 编写的。即使重新配置项目,它也不能在 .NET 3.5 上运行,因为 .NET 4 中的 LINQ-to-SQL 具有不同的类结构。
为了运行所有测试,请确保已安装 SQL Express,并且上面有一个名为“test”的数据库。首先运行“准备”中的测试用例以准备数据。
额外思考。
有一种解决方案,如果提供,可以大大降低本文引入的复杂性。那就是泛型类的继承。可以理解的是,List<Base>()
可以从 List<Derived>()
赋值,因为子列表中的所有元素确实都具有父对象的 C# 特征,无论对 List<Base>()
执行什么操作都适用于 List<Derived>()
。同样的 C# 逻辑也适用于 ITable<Base>()
和 ITable<Derived>()
。如果真是这样,那么甚至不需要这篇文章和上述方法。使用派生类检索表,然后将其转换为父类表,即可完成。不幸的是,在 .NET 4.0 中,这些类仍然被视为完全不同的类。同样,使用子类消耗的委托与使用父类消耗的委托之间没有任何关系。希望 .NET 团队能够尽快解决此问题。
最后
感谢阅读。欢迎所有评论和问题。