使用自定义属性将数据库表和列映射到类和字段





5.00/5 (2投票s)
构建 O/R 映射器:第 2 步。
引言
在我关于《从任何数据库加载任何对象》的文章中,我列出了该代码的相当多的不足之处。在这篇文章中,我将通过使用自定义属性来修复其中的一些问题。到本文结束时,我们将已经修复或能够修复以下问题:
- 被迫使用性能低下的
SELECT *
- 无法映射不是有效变量名或类名的表名和列名
- 使用性能缓慢的属性
- 无法保护数据
背景
在 .NET 中,您可以“装饰”您的代码,即提供有关代码的信息,您可以通过反射来访问这些信息,从而了解更多关于您的代码。这听起来有点奇怪,但如果您曾经使用过 XML 序列化,您就已经见过代码中的装饰,例如(这是从 MSDN 网站 此处 借用的):
public class Address
{
// The XmlAttribute instructs the XmlSerializer to serialize the Name
// field as an XML attribute instead of an XML element (the default
// behavior).
[XmlAttribute]
public string Name;
public string Line1;
// Setting the IsNullable property to false instructs the
// XmlSerializer that the XML attribute will not appear if
// the City field is set to a null reference.
[XmlElementAttribute(IsNullable = false)]
public string City;
public string State;
public string Zip;
}
当然,重用这些装饰实际上并没有用;我们需要创建自己的。XML 序列化代码正是我们在第 1 步的文章中所做的。它使用变量名和类名作为节点,只需要为异常情况使用属性。我想使用我的自定义属性来限制使用的字段。默认的字段将导致我以后丢失信息。
对于类,我们需要一个属性,它至少提供两条信息:
- 架构信息(如果存在)
- 表名
对于列,我们至少需要两条信息:
- 表中列的名称
- 列的
DbType
(用于创建参数,尽管我不会在这篇文章中讨论它)
由于本文不是关于创建或读取自定义属性的,我将让您自己研究。您可以在此处和此处找到一些教程。
我选择对我的 TableAttribute
类强制执行一些规则,包括只允许该属性应用于类,而不是属性或字段,并且只允许类有一个该属性的实例。所以,这是代码:
/// <summary>
/// Attribute for decorating classes, so they can be matched to DB tables.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class TableAttribute : Attribute
{
public string Schema { get; set; }
public string Name { get; set; }
}
为了避免使用属性带来的固有的速度问题,并且因为我想使用属性来跟踪更改,我选择使列属性仅对字段(实例变量)有效。这使我能够保护我的数据免受意外更改,因为我可以为不允许更改的列省略属性的 'set' 部分。数据保护是一件好事!
/// <summary>
/// Attribute for decorating fields,
/// so they can be matched to DB columns
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class ColumnAttribute : Attribute
{
/// <summary>
/// Gets or sets the System.Data.DbType
/// that will be used for the object for Parameters
/// </summary>
public DbType DbType
{
get;
set;
}
/// <summary>
/// Gets or sets the Name of the object
/// </summary>
public String Name
{
get;
set;
}
}
注意:对于不知道创建自己的属性有点奇怪的人来说,您应该始终用后缀 Attribute 来命名它们,因此我的两个属性是 TableAttribute
和 ColumnAttribute
。奇怪的是,当您开始使用它们来装饰代码时,“Attribute”部分将被删除,所以它将是 Table
和 Column
。
如果查看我关于密码或关于SQL Server 索引视图的文章,您将无法将这些表与我第 1 步文章中的代码一起使用。但是,如果我们添加新属性,问题表 Vehicle 的对象可以如下所示:
[Table(Schema = "dbo", Name = "Vehicle")]
public class Vehicle
{
//private bool _isDirty;
[Column(DbType = DbType.Guid, Name = "ID")]
private Guid _id;
[Column(Name = "Vehicle Identification Number",
DbType = DbType.String)]
private string _vehicleIndentificationNumber = string.Empty;
public Guid ID
{
get { return _id; }
set
{
if (value != _id)
{
//_isDirty = true;
_id = value;
}
}
}
public string VIN
{
get { return _vehicleIndentificationNumber; }
set
{
value = (value ?? string.Empty).Trim();
if (string.Compare(value, _vehicleIndentificationNumber) != 0)
{
//_isDirty = true;
_vehicleIndentificationNumber = value;
}
}
}
//public bool IsDirty
//{
// get { return _isDirty; }
//}
}
如您所见,我提示了如何实现脏对象跟踪。我还为列使用了一个更短的名称,更熟悉的 VIN,而不是 Vehicle Identification Number。
现在,为了只从我们的表中提取我们需要的列,而不是使用 SELECT *
。为此,我将执行以下操作:
- 查询所有字段,检查它们是否具有我的
ColumnAttribute
,如果具有,则获取列名。 - 使用
CommandBuilder
函数QuoteIdentifier
包装列名。 - 从
TableAttribute
获取架构和表名,并将它们也包装起来。
所以,让我们构建一个函数来完成这项工作。
private static BindingList<T> LoadObjectsFromDatabase<T>
(string connectionString, string providerName) where T : new()
{
//portion 1, get the table name and wrap it
var factory = DbProviderFactories.GetFactory(providerName);
var commandBuilder = factory.CreateCommandBuilder();
Type type = typeof(T);
string name = string.Empty;
{
var attributes = type.GetCustomAttributes(false);
foreach (Attribute attribute in attributes)
{
TableAttribute ta = attribute as TableAttribute;
if (ta != null)
{
if (!string.IsNullOrEmpty(ta.Schema))
{
name = string.Format("{0}{1}",
commandBuilder.QuoteIdentifier(ta.Schema),
commandBuilder.SchemaSeparator
);
}
name += commandBuilder.QuoteIdentifier(ta.Name);
break;
}
}
}
if (string.IsNullOrEmpty(name))
{
//could fall back to just the class name, but not for this example
throw new
Exception("Unable to build SQL because " +
"custom attribute doesn't exist.");
}
//portion 2 - get the fields and their columns, append
//the column names into the SQL statement as well as
//create the field map, so we don't have to look up the field
//every time.
var fieldMap = new Dictionary<string, FieldInfo>();
string comma = string.Empty;
var sb = new StringBuilder("SELECT ");
var fields = type.GetFields(BindingFlags.NonPublic| BindingFlags.Instance);
foreach (var f in fields)
{
var attributes = f.GetCustomAttributes(false);
foreach (Attribute attribute in attributes)
{
ColumnAttribute ca = attribute as ColumnAttribute;
if (ca != null)
{
//create our fieldmap
fieldMap.Add(ca.Name, f);
//append a comma, if this is the first column, it will be empty.
sb.Append(comma);
//make sure additional columns will have comma seperators
comma = ", ";
sb.Append(commandBuilder.QuoteIdentifier(ca.Name));
}
}
}
if (fieldMap.Count < 1)
{
throw new Exception("No columns in query");
}
sb.AppendLine();
sb.AppendLine("FROM");
sb.Append(name);
//portion 3 - create the connection, build execute reader, and pass
//the new reader, ad its related field map to the LoadObjectsFromDatabase
//function
using (var connection = factory.CreateConnection())
{
connection.ConnectionString = connectionString;
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = sb.ToString();
command.CommandType = CommandType.Text;
using (var dr = command.ExecuteReader(CommandBehavior.CloseConnection))
{
return LoadObjectsFromDataReader<T>(dr, fieldMap);
}
}
}
}
这段代码分为三个基本部分,如果我构建一个完整的解决方案,这些部分将更好地组织起来以便重用。
第一部分
这一部分用于从类中获取自定义属性,以便我们知道表名。
第二部分
这部分用于从对象中获取字段列表,并从中获取自定义属性。我使用这些信息来构建一个 Select
语句,该语句仅选择我们实际有字段来存储信息的列。这比 SELECT *
有巨大的优势,因为它执行速度更快,列重新排序不会影响它,新列也不会影响。我还存储了列名和字段信息以备后用。这一部分最后附加 FROM
子句到 SQL 语句。注意:使用 SELECT *
会迫使您的数据库引擎将 *
扩展为列名列表。因此,每次需要时重新构建此 SQL 语句都会导致您失去数据库引擎因不使用 SELECT *
而获得的性能提升。您应该缓存您新构建的语句,但我现在还不打算讨论这一点。
第三部分
这应该简化为几行。首先,一个执行 SQL 语句的调用。然后,将 DbDataReader
传递给修改后的 LoadObjectsFromDataReader
函数。目前,它使用提供程序字符串和连接字符串来构建与数据库的新连接,确保将 CommandBehavior
设置为 CloseConnection
;这样,当数据读取器不再有记录或被处置时,连接会自动关闭。
现在,我们需要修改第 1 步文章中现有的 LoadObjectsFromDataReader
函数。
private static BindingList<T> LoadObjectsFromDataReader<T>
(IDataReader dr, Dictionary<string,
FieldInfo> fieldMap) where T : new()
{
//get the type of the object, without having to create one
BindingList<T> retval = new BindingList<T>();
//data block
object[] oo = new object[fieldMap.Count];
while (dr.Read())
{
dr.GetValues(oo);
//could be threaded block
T item = new T();
int fieldIndex = -1;
foreach (var kvp in fieldMap)
{
FieldInfo fi = kvp.Value;
object o = oo[++fieldIndex];
if (DBNull.Value.Equals(o))
{
o = null;
}
try
{
fi.SetValue(item, o);
}
catch
{
//eat data errors quietly
}
}
retval.Add(item);
//end of could be threaded block
}
return retval;
如果将其与原始函数进行比较,您会发现我不再查询对象的属性列表。相反,我正在使用列名到字段的映射,并且因为我知道我的 DbDataReader
和我的 fieldMap
是同步的,所以我可以直接迭代它们,在过程中填充字段。
Using the Code
使用这段代码很容易。正如我在第 1 步的文章中所述,Linq2Objects 是从不同数据源中提取数据的绝佳方法,所以虽然这个例子只使用了来自单个数据库的数据,但没关系;一旦它们被加载,我们就可以随意查询它们。
var vehicles = LoadObjectsFromDatabase<Vehicle>
(connectionString,
providerName);
var vehicleLicensePlates =
LoadObjectsFromDatabase<Vehicle_LicensePlate_Issuer>
(connectionString,
providerName);
var licenseIssuers = LoadObjectsFromDatabase<LicensePlate_Issuer>
(connectionString,
providerName);
var licenses = LoadObjectsFromDatabase<LicensePlate>
(connectionString,
providerName);
//query the current licenseplate for the car with VIN "asdf"
//because "Current" is a boolean so a comparison isn't required
var query = from license in licenses
join lincensePlateIssuer in licenseIssuers
on license.ID equals lincensePlateIssuer.LicensePlateID
join vehicleLicensePlate in vehicleLicensePlates
on lincensePlateIssuer.ID equals vehicleLicensePlate.LicensePlate_Issuer_ID
join vehicle in vehicles
on vehicleLicensePlate.Vehicle_ID equals vehicle.ID
where vehicle.VIN == "asdf" && vehicleLicensePlate.Current
select license;
这个例子只是从我的示例数据库(该文章中的示例数据库包含数据)加载了许多对象,并返回了一个对该查询有意义的记录。
关注点
这篇文章解决了上面提到的所有不足之处,但我还没有涉及实际的查询缓存,或者使用自定义属性创建 Insert
或 Update
语句(尽管您可以使用 CommandBuilder
来帮助实现这一点)。
这段代码最明显的一个主要不足之处是,没有办法将数据限制为行的一个子集,换句话说,我没有“WHERE
”子句。
在乐队开始调音之前,让我们看看这段代码的一些问题。DbDataReader
和 fieldMap
可能会不同步,应该进行检查但没有检查。LoadObjectsFromDatabase
函数可能应该只接受连接名称,而不是传递多个值。CommandBuilder.QuotePrefix
和 QuoteSuffix
并非总是填充的,因此在使用 QuoteIdentifier
之前必须确保它们是正确的。并非所有数据库都支持架构,或者即使支持,也可能不支持得不好。在某些情况下,忽略它比较容易;然而,有时这并非一个选项。例如,如果您正在使用一个设计类似于微软新的 AdventureWorks 演示数据库的数据库,则绝对需要架构。
尽管这段代码仍有重大不足之处,但现在它可以从任何数据库中拉取任何对象,包括 SQL Server 2008 的新类型。显然,如果您使用那些特定于数据库的类型,您将无法在代码下方交换数据库服务器。但是,只需更改连接字符串(可能还有架构),就可以将数据表从一个数据库迁移到另一个数据库。
另一件很棒的事情是,假设您正确地使用了 Name
字符串的大小写,这段代码允许您将列映射到任何变量名,它甚至不需要有意义。像空格和 SQL 关键字这样的字符不会破坏您的代码。
历史
- 2010-02-18:修复了
LoadObjectsFromDataReader
中的 bug(将字段索引从后增量改为前增量)。 - 2010-02-17:原始文章。