LINQ to SQL:在一个基类中实现所有常用操作(插入、更新、删除、获取)






4.53/5 (39投票s)
一个用于执行所有常用 LINQ to SQL 操作的基类。
引言
我一直在使用 LINQ to SQL。它非常棒,而且与 VS.NET 2008 附带的 Designer 一起使用非常方便。我想创建一个与 LINQ to SQL 类集成的 Facade 层。传统的做法是创建一个公共类(Manager
),然后该类调用你的数据库映射器来获取/设置所需的信息。现在,LINQ to SQL 取代了这些映射器类,执行操作的方式是打开 DataContext
并开始定义查询来执行这些操作。
因此,我将这些查询和操作组合到另一个单独的层,称为 Processes/Operations 层,Facade 层最终会调用它。这样做的好处是 Facade 层保持不变,而 Processes/Operations 层取代了 mapper 层,同时封装了数据库操作的全部逻辑和复杂性。
话虽如此,我创建了一个基类,它封装了 Processes/Operations 层中最常用的数据库操作逻辑,而不是在每个类中重复相同的代码。
注意:本文档面向熟悉 LINQ 和 LINQ to SQL 的读者。
使用代码
类定义
下面是该类的定义
internal class DataBaseProcessBase<T, DC> where T :
class, new() where DC : DataContext, new()
正如你所注意到的,这是一个泛型类,接受两个类型:第一个是你的实体类型,第二个是你的 DataContext
。接下来,这里是该类中列出的操作
- Add
- Get
- 更新
- 删除
你只需创建一个继承自它的类,如下所示
internal class MyProcess : DataBaseProcessBase<MyEntity,MyDataContext>
Add 操作
/// <summary>
/// Adds a new record to the DB
/// </summary>
/// <param name="entity">Current Object</param>
/// <param name="IdPropertyName">Name of the property
/// containing identity Column or the ID returned by
/// the DB</param>
/// <returns><see cref="System.Object"/> </returns>
protected virtual object Add(T entity, string IdPropertyName)
{
using (DC db = new DC())
{
db.GetTable<T>().InsertOnSubmit(entity);
db.SubmitChanges();
}
return entity.GetType().GetProperty(IdPropertyName).GetValue(entity, null);
}
Add 操作非常简单直接。它只是将实体插入数据库并返回新记录的 ID。请注意,第二个参数是 ID 属性的名称,因为我使用反射来获取已插入实体 ID 属性的值。
如何使用
base.Add(MyEntity, "ID");
注意:属性名称区分大小写。
Get 操作
/// <summary>
/// Select From DB on the defined query
/// </summary>
/// <param name="options"><see
/// cref="System.Data.Linq.DataLoadOptions"/></param>
/// <param name="query">Select Query</param>
/// <param name="from">for pagination Purposes, starting Index</param>
/// <param name="to">for pagination Purposes, End Index</param>
/// <returns>collection of the current type,
/// <see cref="System.System.Collections.Generic.IList<T>"/></returns>
/// <remarks>if "to" parameter was passed as 0,
/// it will be defaulted to 100, you can replace it by
/// a valued defined in the config, and another point
/// of interest, if from > to, from will be
/// reseted to 0.
///
/// if there is no query defined, all results will be
/// returned, and also if there is no load data options
/// defined, the results will contain only the entity specified
/// with no nested data (objects) within that entity.
/// </remarks>
protected virtual IList<T> Get(DataLoadOptions options,
Expression<Func<T, bool>> query, int from, int to)
{
IList<T> list = null;
if (to == 0)
to = 100;
if (from > to)
from = 0;
using (DC db = new DC())
{
if (null != options)
db.LoadOptions = options;
if (null == query)
list = db.GetTable<T>().Skip(from).Take(to - from).ToList();
else
list =
db.GetTable<T>().Where(query).Skip(from).Take(to - from).ToList();
}
return list;
}
get
方法包含四个参数
DataLoadOptions
:如果定义了,它将被分配给数据库。Expression<Func<T,bool>> query
:这是将返回结果的查询或 Lambda 表达式。int from
:起始索引,用于分页。int to
:结束索引,也用于分页。
如何使用
DataLoadOptions options = new DataLoadOptions();
//Load my object along with nested object with it,
//as an example MyObject = Customer, MyNestObject=Orders
options.LoadWith<MyObject>(m => m.MyNestedEntity);
base.Get(options,m => m.CategoryID == 1,0,100);
假设你有一个名为 MyObject
的类,该类有两个属性:CategoryId
和 MyNestedObject
。场景是,我们想获取 MyObject
的 100 个结果,其中它的 CategoryID
等于 1,并包含其嵌套对象。
此方法有两个重载,如下所示
/// <summary>
/// Select From DB on the defined query
/// </summary>
/// <param name="query">Select Query</param>
/// <param name="from">for pagination Purposes, starting Index</param>
/// <param name="to">for pagination Purposes, End Index</param>
/// <returns>collection of the current type,
///<see cref="System.System.Collections.Generic.IList<T>"/></returns>
/// <remarks>if "to" parameter was passed as 0, it will be defaulted to 100,
///you can replace it by a valued defined in the config, and another point of
/// interest, if from > to, from will be reseted to 0</remarks>
protected virtual IList<T> Get(Expression<Func<T, bool>> query, int from, int to)
{
return Get(null, query, from, to);
}
/// <summary>
/// Select All From DB
/// </summary>
/// <param name="from">for pagination Purposes, starting Index</param>
/// <param name="to">for pagination Purposes, End Index</param>
/// <returns>collection of the current type,
///<see cref="System.System.Collections.Generic.IList<T>"/></returns>
/// <remarks>if "to" parameter was passed as 0, it will be defaulted to 100,
///you can replace it by a valued defined in the config, and another point of
/// interest, if from > to, from will be reseted to 0</remarks>
protected virtual IList<T> Get(int from, int to)
{
return Get(null, null, from, to);
}
关注点
get
方法中定义的每个参数都可以是 null
。从第一个参数开始,如果 DataLoadOptions
为 null
,则不会将其分配给数据库上下文。如果 query
为 null
,则将根据指定的分页索引返回所有结果。最后但同样重要的是,如果 'to
' 参数为 0,则默认为 100(可以通过配置文件值进行更改),如果 'from
' 大于 'to
' 参数,则默认为 0(如果这种行为你不喜欢,也可以进行更改)。
当你进行搜索时,这些方法非常有用;基于条件,构建你的查询,然后调用其中一个方法。
请记住,这些方法将根据提供的查询获取任何数据。如果你想在客户端代码中过滤这些 get 方法,只需在 Facade 和 Process 类中创建你自己的方法,如下所示
public static class FacadeExample
{
public static MyEntity GetMyEntityByID(int id)
{
return (new MyProcess()).GetByID(id);
}
}
现在,在 MyProcess
类(本文顶部定义的那个)中,添加相同的方法,但添加按 ID 获取的查询逻辑,类似于这样
public MyEntity GetByID(int id)
{
return base.Get(m => m.ID == id,0,1)[0];
}
在 Facade 类中,请注意我们实例化了 MyProcess
类的一个新实例(new MyProcess()
),所以每次调用此方法时,都会创建一个新实例。这不是一个好习惯,但我在这里添加它以便快速引用该类,因为它与本文主题无关。因此,你应该考虑使用单例模式或工厂类来获取你的 Process 实例,而不是每次调用方法时都创建一个新实例。
注意:如果结果计数为零,上面列出的代码将抛出 IndexOutOfRangeException
,因此在返回结果之前,你应该始终进行 null 和 count 检查。
Update 操作
/// <summary>
/// Updates Entity
/// </summary>
/// <param name="entity">Entity which hold the updated information</param>
/// <param name="query">query to get the same
/// entity from db and perform the update operation</param>
/// <remarks>this method will do dynamic property mapping between the passed entity
/// and the entity retrieved from DB upon the query defined,
/// ONLY ValueTypes and strings are
/// mapped between both entities, NO nested objects will be mapped, you have to do
/// the objects mapping nested in your entity before calling this method</remarks>
protected virtual void Update(T entity, Expression<Func<T, bool>> query)
{
using (DC db = new DC())
{
object propertyValue = null;
T entityFromDB = db.GetTable<T>().Where(query).SingleOrDefault();
if (null == entityFromDB)
throw new NullReferenceException("Query Supplied to " +
"Get entity from DB is invalid, NULL value returned");
PropertyInfo[] properties = entityFromDB.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
propertyValue = null;
if (null != property.GetSetMethod())
{
PropertyInfo entityProperty =
entity.GetType().GetProperty(property.Name);
if (entityProperty.PropertyType.BaseType ==
Type.GetType("System.ValueType")||
entityProperty.PropertyType ==
Type.GetType("System.String"))
propertyValue =
entity.GetType().GetProperty(property.Name).GetValue(entity, null);
if (null != propertyValue)
property.SetValue(entityFromDB, propertyValue, null);
}
}
db.SubmitChanges();
}
}
Update 操作有点棘手,因为传递的实体与数据库中的实体之间的属性映射是动态进行的,所以任何嵌套对象都不会被映射。唯一会进行的属性映射是对值类型和字符串值的。
如何使用
base.Update(MyEntity,e => e.ID == MyEntity.ID);
工作原理
根据查询表达式,此方法将从数据库中获取实体,然后对传递的实体和检索到的实体进行动态属性映射,然后提交更改。因此,在上面的示例中,我正在更新数据库实体,其中 ID = 传递的实体 ID。
关注点
我们之所以从数据库中获取实体并进行映射,是因为正如你所注意到的,我们正在使用 Facade 来获取实体。所以,一旦数据库上下文关闭,我们就无法使用传递给此方法的实体提交更改。否则,你将遇到 Object Disposed 异常。另一种方法是获取数据库中的实体,进行映射,然后提交更改。
更新时间
我一直在思考 Update 方法中属性之间的映射,但对设计不满意,因此我重新设计了整个映射机制,并为每种支持的类型插入了一个映射提供程序,你也可以创建自己的提供程序并将其插入使用。
首先,我创建了一个接口,如下所示
/// <summary>
/// Common interface for all Property Mapping Providers
/// </summary>
public interface IPropertyMappingProvider
{
/// <summary>
/// Responsible for mapping the two properties
/// </summary>
/// <param name="entity">Entity received from
/// the client code, <see cref="System.Object"/></param>
/// <param name="LINQEntity">Entity retrieved from DB</param>
/// <param name="LINQProperty">
/// <see cref="System.Reflection.PropertyInfo"/> from LINQ entity
/// retrieved from DB to be mapped</param>
void MapProperties(object entity, object LINQEntity, PropertyInfo LINQProperty);
}
还有一个实现了上述接口的基属性映射器类
internal class PropertyMappingProviderBase : IPropertyMappingProvider
{
#region IPropertyMappingProvider Members
/// <summary>
/// Encapsulates the common functionality of mapping two
///properties using <see cref="System.Reflection"/>
/// </summary>
/// <param name="entity">Entity received from the client code,
///<see cref="System.Object"/></param>
/// <param name="LINQEntity">Entity retrieved from DB</param>
/// <param name="LINQProperty">
/// <see cref="System.Reflection.PropertyInfo"/>
///from LINQ entity
/// retrieved from DB to be mapped</param>
/// <remarks>If you want to create a new Provider, just inherit from this class
/// and have MappingPropertyTypeNameAttribute set to the type you are providing
///the mapping
/// against</remarks>
public virtual void MapProperties(object entity,
object LINQEntity, PropertyInfo LINQProperty)
{
object propertyValue = null;
//Get Property from entity
PropertyInfo entityProperty = entity.GetType().
GetProperty(LINQProperty.Name);
//Get Value from the property
if (null != entityProperty)
propertyValue = entityProperty.GetValue(entity, null);
//Set LinqEntity to the value retrieved from the entity
if (null != propertyValue)
LINQProperty.SetValue(LINQEntity, propertyValue, null);
}
#endregion
}
所以,正如你所看到的,基类实现了 IPropertyMappingProvider
接口,该接口只有一个方法 MapProperties
。MapProperties
方法接受三个参数:第一个是你从客户端代码接收到的实体,下一个是你从数据库检索到的实体,最后一个是你进行映射的属性。
现在,这是创建提供程序的部分。但在粘贴每个提供程序的代码之前,应该动态初始化每种类型的提供程序。不要使用 switch
语句来初始化它们,也不要在负责初始化正确提供程序的类中使用硬编码的值。那么,最好的方法是什么呢??
我发现创建自定义属性并用它来装饰我们的提供程序类是最好的方法。这是它的定义
/// <summary>
/// Attribute specified on a <see cref="IPropertyMappingProvider"/>
/// indicating the type of property that it maps
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class MappingPropertyTypeNameAttribute : System.Attribute
{
private string _propertyTypeName;
public MappingPropertyTypeNameAttribute(string propertyTypeName)
{
_propertyTypeName = propertyTypeName;
}
public string PropertyTypeName
{
get
{
return _propertyTypeName;
}
}
简单来说,这个属性被分配给每个提供程序类(除了基类),它将持有它将处理的类型的完全限定名(例如:System.String
)。所以现在,我可以列出我创建的两个提供程序:一个用于字符串值,另一个用于值类型
字符串映射提供程序
/// <summary>
/// Responsible for mapping String Values between both properties
/// </summary>
[MappingPropertyTypeName("System.String")]
internal class StringPropertyMappingProvider : PropertyMappingProviderBase
{
/// <summary>
/// Map String Values
/// </summary>
/// <param name="entity">Entity received from the client code,
/// <see cref="System.Object"/></param>
/// <param name="LINQEntity">Entity retrieved from DB</param>
/// <param name="LINQProperty">
/// <see cref="System.Reflection.PropertyInfo"/> from LINQ entity
/// retrieved from DB to be mapped</param>
public override void MapProperties(object entity,
object LINQEntity, PropertyInfo LINQProperty)
{
base.MapProperties(entity, LINQEntity, LINQProperty);
}
}
值类型映射提供程序
/// <summary>
/// Responsible for mapping Value types between both properties
/// </summary>
[MappingPropertyTypeName("System.ValueType")]
internal class ValueTypePropertyMappingProvider : PropertyMappingProviderBase
{
/// <summary>
/// Map Value types
/// </summary>
/// <param name="entity">Entity received from the
/// client code, <see cref="System.Object"/></param>
/// <param name="LINQEntity">Entity retrieved from DB</param>
/// <param name="LINQProperty">
/// <see cref="System.Reflection.PropertyInfo"/> from LINQ entity
/// retrieved from DB to be mapped</param>
public override void MapProperties(object entity,
object LINQEntity, PropertyInfo LINQProperty)
{
base.MapProperties(entity, LINQEntity, LINQProperty);
}
}
注意 MappingPropertyTypeName
设置为与类型名称完全相同。这两个类都没有什么特别之处,只是调用了基类方法。但为每种类型创建一个提供程序是一个好习惯,以防你需要进行更多操作,或者如果你想更改其中一个的行为而不影响其他。
现在,一切都设置好了,唯一缺少的是我们的 Facade 类来动态初始化正确的提供程序。所以,这是该类的定义
/// <summary>
/// Entry Point for the Client code to map the properties
/// </summary>
public static class MappingProvider
{
/// <summary>
/// Map Properties between two objects
/// </summary>
/// <param name="entity">Entity received from the client code,
///<see cref="System.Object"/></param>
/// <param name="LINQEntity">Entity retrieved from DB</param>
/// <param name="LINQProperty">
/// <see cref="System.Reflection.PropertyInfo"/>
///from LINQ entity
/// retrieved from DB to be mapped</param>
/// <remarks>This class will get the provider dynamically and will map
///the properties
/// using that provider, so if you want
/// to implement your own provider, you dont
/// have to modify anything in the code</remarks>
public static void MapProperties(object entity,
object LINQEntity, PropertyInfo LINQProperty)
{
IPropertyMappingProvider provider = null;
//Get All Types in the current assembly which have
//MappingPropertyTypeNameAttribute defined
Type[] currentProviders = Assembly.GetExecutingAssembly().GetTypes().Where(
t => t.GetCustomAttributes(typeof(MappingPropertyTypeNameAttribute),
false).ToArray().Length > 0).ToArray();
if (null != currentProviders && currentProviders.Length > 0)
{
//Get the provider type,first try to get from its type
//the mechanism used is to get the MappingPropertyTypeNameAttribute
//and compare the string defined there with the LINQProperty type
Type providerType = currentProviders.Where(p =>
(p.GetCustomAttributes(typeof(MappingPropertyTypeNameAttribute),
false).ToArray()[0] as MappingPropertyTypeNameAttribute).PropertyTypeName ==
LINQProperty.PropertyType.ToString()).SingleOrDefault();
//if no provider found,Try to get it from comparing LINQproperty
//base type with MappingPropertyTypeNameAttribute
if(null == providerType)
providerType = currentProviders.Where(p =>
(p.GetCustomAttributes(typeof(MappingPropertyTypeNameAttribute),
false).ToArray()[0] as MappingPropertyTypeNameAttribute).PropertyTypeName ==
LINQProperty.PropertyType.BaseType.ToString()).SingleOrDefault();
if (null != providerType)
{
//Call the provider factory to get our instance
provider = ProviderFactory.CreatePropertyMappingProvider(providerType);
//Map Properties
provider.MapProperties(entity, LINQEntity, LINQProperty);
}
}
}
}
说实话,编写这个类很有趣,因为它使用反射动态地完成了所有事情。首先,它获取程序集中所有带有 MappingPropertyTypeNameAttribute
定义的类型,这意味着它将获取我们所有的映射提供程序。如果找到任何类型,它将尝试通过将传递的属性类型与我们每个提供程序类上定义的属性值进行字符串比较来获取正确的提供程序。如果未找到,它将尝试比较属性的基类型与我们的属性值(原因是所有值类型(例如:Int32
)都将其类型设置为 System.Int32
并且它们的基类型是值类型;此外,扩展比较的可能性也很好)。
所以,在找到我们的提供程序类型之后,我们需要初始化它。我创建了一个工厂类来完成这项工作,如下所示
/// <summary>
/// Responsible of instantiating each provider
/// and cahing it into a Dictionary
/// </summary>
internal static class ProviderFactory
{
//Static providers cache
static IDictionary<string, IPropertyMappingProvider> providers =
new Dictionary<string, IPropertyMappingProvider>();
public static IPropertyMappingProvider
CreatePropertyMappingProvider(Type providerType)
{
IPropertyMappingProvider provider = null;
//Check if the provider already exists in the cahce
if (providers.ContainsKey(providerType.ToString()))
provider = providers[providerType.ToString()]
as IPropertyMappingProvider;
else
{
//Instaniate a new provider and add it to the cache
provider= AppDomain.CurrentDomain.CreateInstanceAndUnwrap(
Assembly.GetExecutingAssembly().ToString(),
providerType.ToString()) as IPropertyMappingProvider;
providers.Add(provider.GetType().ToString(),
provider as IPropertyMappingProvider);
}
return provider;
}
}
那里没有什么特别的,只是初始化了提供程序并将其插入自定义的 Dictionary
进行缓存,因为没有必要反复创建提供程序,单例模式就足够了。
最后,它调用相应提供程序的 MapProperties
方法,所以我们基类中的 Update
方法在修改后将如下所示
protected virtual void Update(T entity, Expression<Func<T, bool>> query)
{
using (DC db = new DC())
{
T entityFromDB = db.GetTable<T>().Where(query).SingleOrDefault();
if (null == entityFromDB)
throw new NullReferenceException("Query Supplied to Get" +
" entity from DB is invalid, NULL value returned");
PropertyInfo[] properties = entityFromDB.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
if (null != property.GetSetMethod())
{
//Just one Line Of Code to Do the mapping
MappingProvider.MapProperties(entity, entityFromDB, property);
}
}
db.SubmitChanges();
}
}
最后,关于 Update 操作,旧的 DatabaseProcessBase
类仍然可以在下载文件中找到,并且还添加了所有提供程序的新设计和类的修改。
Delete 操作
/// <summary>
/// Deletes the entity upon the defined query
/// </summary>
/// <param name="query">Delete Query</param>
protected virtual void Delete(Expression<Func<T, bool>> query)
{
using (DC db = new DC())
{
db.GetTable<T>().DeleteOnSubmit(
db.GetTable<T>().Where(query).Single());
db.SubmitChanges();
}
}
Delete 操作只接受一个参数,即用于获取将被删除实体的查询。你可以扩展此方法以删除所有内容,如果未定义查询。
如何使用
base.Delete(e => e.ID == 1);
结论
希望大家都能从这个类中受益。感谢大家的阅读。
历史
- 2008 年 5 月 9 日 - 本文的初始版本。
- 2008 年 5 月 31 日 - 更新,加入映射提供程序,用于映射对象之间的属性。