65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2010 年 2 月 16 日

CPOL

7分钟阅读

viewsIcon

34533

构建 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 来命名它们,因此我的两个属性是 TableAttributeColumnAttribute。奇怪的是,当您开始使用它们来装饰代码时,“Attribute”部分将被删除,所以它将是 TableColumn

如果查看我关于密码或关于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;

这个例子只是从我的示例数据库(该文章中的示例数据库包含数据)加载了许多对象,并返回了一个对该查询有意义的记录。

关注点

这篇文章解决了上面提到的所有不足之处,但我还没有涉及实际的查询缓存,或者使用自定义属性创建 InsertUpdate 语句(尽管您可以使用 CommandBuilder 来帮助实现这一点)。

这段代码最明显的一个主要不足之处是,没有办法将数据限制为行的一个子集,换句话说,我没有“WHERE”子句。

在乐队开始调音之前,让我们看看这段代码的一些问题。DbDataReaderfieldMap 可能会不同步,应该进行检查但没有检查。LoadObjectsFromDatabase 函数可能应该接受连接名称,而不是传递多个值。CommandBuilder.QuotePrefixQuoteSuffix 并非总是填充的,因此在使用 QuoteIdentifier 之前必须确保它们是正确的。并非所有数据库都支持架构,或者即使支持,也可能不支持得不好。在某些情况下,忽略它比较容易;然而,有时这并非一个选项。例如,如果您正在使用一个设计类似于微软新的 AdventureWorks 演示数据库的数据库,则绝对需要架构。

尽管这段代码仍有重大不足之处,但现在它可以从任何数据库中拉取任何对象,包括 SQL Server 2008 的新类型。显然,如果您使用那些特定于数据库的类型,您将无法在代码下方交换数据库服务器。但是,只需更改连接字符串(可能还有架构),就可以将数据表从一个数据库迁移到另一个数据库。

另一件很棒的事情是,假设您正确地使用了 Name 字符串的大小写,这段代码允许您将列映射到任何变量名,它甚至不需要有意义。像空格和 SQL 关键字这样的字符不会破坏您的代码。

历史

  • 2010-02-18:修复了 LoadObjectsFromDataReader 中的 bug(将字段索引从后增量改为前增量)。
  • 2010-02-17:原始文章。
© . All rights reserved.