C# LINQ to SQL:重构通用的GetByID方法

21
我写了以下方法。
public T GetByID(int id)
{
    var dbcontext = DB;
    var table = dbcontext.GetTable<T>();
    return table.ToList().SingleOrDefault(e => Convert.ToInt16(e.GetType().GetProperties().First().GetValue(e, null)) == id);
}

基本上,这是一个泛型类中的方法,其中T是DataContext中的一个类。

该方法通过类型T(GetTable)获取表,然后检查第一个属性(始终为ID)是否符合输入参数。

问题在于,我必须先将元素表转换为列表,才能对属性执行GetType,但这并不方便,因为必须枚举和转换表中的所有元素为List

如何重构此方法,以避免在整个表上使用ToList

[更新]

我无法直接在表格上执行Where的原因是我收到了以下异常:

方法“System.Reflection.PropertyInfo [] GetProperties()”没有受支持的 SQL 翻译。

因为GetProperties无法被翻译成SQL。

[更新]

有些人建议为T使用接口,但问题是T参数将是在[DataContextName].designer.cs中自动生成的类,因此我不能使其实现接口(并且为LINQ的所有这些“数据库类”实现接口不可行;而且一旦我向DataContext添加新表,则文件将重新生成,从而丢失所有编写的数据)。

因此,必须有更好的方法来解决这个问题...

[更新]

我现在已经按照Neil Williams的建议实现了我的代码,但仍然存在问题。以下是代码摘录:
接口:
public interface IHasID
{
    int ID { get; set; }
}

DataContext [查看代码]:

namespace MusicRepo_DataContext
{
    partial class Artist : IHasID
    {
        public int ID
        {
            get { return ArtistID; }
            set { throw new System.NotImplementedException(); }
        }
    }
}

通用方法:

public class DBAccess<T> where T :  class, IHasID,new()
{
    public T GetByID(int id)
    {
        var dbcontext = DB;
        var table = dbcontext.GetTable<T>();

        return table.SingleOrDefault(e => e.ID.Equals(id));
    }
}

这个异常是在这一行代码抛出的:return table.SingleOrDefault(e => e.ID.Equals(id)); 异常信息如下:

System.NotSupportedException: 该成员 'MusicRepo_DataContext.IHasID.ID' 没有受支持的 SQL 翻译。

[更新] 解决方案:

Denis Troller发布的回答和Code Rant blog上的链接的帮助下,我最终找到了解决方案:

public static PropertyInfo GetPrimaryKey(this Type entityType)
{
    foreach (PropertyInfo property in entityType.GetProperties())
    {
        ColumnAttribute[] attributes = (ColumnAttribute[])property.GetCustomAttributes(typeof(ColumnAttribute), true);
        if (attributes.Length == 1)
        {
            ColumnAttribute columnAttribute = attributes[0];
            if (columnAttribute.IsPrimaryKey)
            {
                if (property.PropertyType != typeof(int))
                {
                    throw new ApplicationException(string.Format("Primary key, '{0}', of type '{1}' is not int",
                                property.Name, entityType));
                }
                return property;
            }
        }
    }
    throw new ApplicationException(string.Format("No primary key defined for type {0}", entityType.Name));
}

public T GetByID(int id)
{
    var dbcontext = DB;

    var itemParameter = Expression.Parameter(typeof (T), "item");
    var whereExpression = Expression.Lambda<Func<T, bool>>
        (
        Expression.Equal(
            Expression.Property(
                 itemParameter,
                 typeof (T).GetPrimaryKey().Name
                 ),
            Expression.Constant(id)
            ),
        new[] {itemParameter}
        );
    return dbcontext.GetTable<T>().Where(whereExpression).Single();
}

您不需要担心设计师生成的文件或edmx设计师覆盖它们..您不会在设计师文件中实现接口..您将为实体编写实现接口的部分类。 - meandmycode
GetPrimaryKey方法有点靠不住,linq to sql并不总是使用属性来解释映射,您可以完全使用dbml..无论您使用什么,都将与Denis Troller提供的Mappings定义示例相同。 - meandmycode
是的,最好像我一样使用映射,因为它可以在您映射的任何方式(属性或XML文件)中工作。此外,它应该会快得多(反射很慢)。无论如何,出于性能考虑,您真的应该缓存GetPrimaryKey()的结果。 - Denis Troller
抱歉让你走了一条错误的路,兄弟。我不能自己投票否定自己,所以需要有人帮我这么做。 - Jason Punyon
没问题,伙计,我们都从中学到了东西,谢谢你的贡献。;-) - Andreas Grech
显示剩余2条评论
6个回答

18
您需要构建一个表达式树,使得LINQ to SQL能够理解。假设您的"id"属性总是命名为"id":
public virtual T GetById<T>(short id)
{
    var itemParameter = Expression.Parameter(typeof(T), "item");
    var whereExpression = Expression.Lambda<Func<T, bool>>
        (
        Expression.Equal(
            Expression.Property(
                itemParameter,
                "id"
                ),
            Expression.Constant(id)
            ),
        new[] { itemParameter }
        );
    var table = DB.GetTable<T>();
    return table.Where(whereExpression).Single();
}

这个应该能解决问题。它无耻地借鉴了这篇博客的内容。 当你写类似下面的LINQ查询时,这基本上就是LINQ to SQL所做的:
var Q = from t in Context.GetTable<T)()
        where t.id == id
        select t;

由于编译器无法为您创建LTS,因此您需要自己完成这项工作,因为没有任何东西可以强制T具有“id”属性,并且您不能将接口中的任意“id”属性映射到数据库。

==== 更新 ====

好的,这里是查找主键名称的简单实现,假设只有一个主键(不是复合主键),并且假设类型方面一切正常(也就是说,您在GetById函数中使用的“short”类型与您的主键兼容):

public virtual T GetById<T>(short id)
{
    var itemParameter = Expression.Parameter(typeof(T), "item");
    var whereExpression = Expression.Lambda<Func<T, bool>>
        (
        Expression.Equal(
            Expression.Property(
                itemParameter,
                GetPrimaryKeyName<T>()
                ),
            Expression.Constant(id)
            ),
        new[] { itemParameter }
        );
    var table = DB.GetTable<T>();
    return table.Where(whereExpression).Single();
}


public string GetPrimaryKeyName<T>()
{
    var type = Mapping.GetMetaType(typeof(T));

    var PK = (from m in type.DataMembers
              where m.IsPrimaryKey
              select m).Single();
    return PK.Name;
}

如何解决不同字段名称的问题? - Andreas Grech
此外,您真的应该在其中构建一些缓存,这样您就不必每次调用时都查询PK,但这留给读者作为练习(或出于简洁起见而被省略,查看您最喜欢的免责声明)。 - Denis Troller
现在我有很多工作要做,以完全理解他的方法,特别是关于表达式树和函数,因为我还没有真正深入了解它们。 - Andreas Grech
有关信息,直到.NET 4.0发布之前,使用Single(pred)而不是Where(pred).Single()有非常好的理由(使用LINQ-to-SQL)-它避免了为主键获取执行往返服务器操作(在此之前已经查看了对象)。 - Marc Gravell
谢谢提供信息。我的VB编程习惯很明显,因为在VB中使用查询表单比方法表单更容易 :) - Denis Troller
显示剩余5条评论

1
如果你重新使用GetTable().Where(...)进行重构并在那里进行过滤,会怎样呢?
这将更加高效,因为Where扩展方法应该比将整个表格提取到列表中更好地处理你的过滤。

1
一些想法...
只需删除 ToList() 调用,SingleOrDefault 与我认为 table 是的 IEnumerably 兼容。
将对 e.GetType().GetProperties().First() 的调用缓存起来,以获取返回的 PropertyInfo。
您不能只添加一个约束到 T 上强制它们实现公开 Id 属性的接口吗?

0

也许执行一个查询是个好主意。

public static T GetByID(int id)
    {
        Type type = typeof(T);
        //get table name
        var att = type.GetCustomAttributes(typeof(TableAttribute), false).FirstOrDefault();
        string tablename = att == null ? "" : ((TableAttribute)att).Name;
        //make a query
        if (string.IsNullOrEmpty(tablename))
            return null;
        else
        {
            string query = string.Format("Select * from {0} where {1} = {2}", new object[] { tablename, "ID", id });

            //and execute
            return dbcontext.ExecuteQuery<T>(query).FirstOrDefault();
        }
    }

列名因表而异 - Andreas Grech
好的,我还没有明白主键列名可能会变化的情况。我看到已经有解决方案可以获取主键列名。谢谢。 - Misha N.

0

关于:

System.NotSupportedException:成员'MusicRepo_DataContext.IHasID.ID'没有支持的SQL翻译。

解决您最初问题的简单方法是指定一个表达式。请参见下面的示例,它对我非常有效。

public interface IHasID
{
    int ID { get; set; }
}
DataContext [View Code]:

namespace MusicRepo_DataContext
{
    partial class Artist : IHasID
    {
        [Column(Name = "ArtistID", Expression = "ArtistID")]
        public int ID
        {
            get { return ArtistID; }
            set { throw new System.NotImplementedException(); }
        }
    }
}

0

好的,请查看这个演示实现。它试图使用datacontext(Linq To Sql)获取通用的GetById。同时也兼容多键属性。

using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;

public static class Programm
{
    public const string ConnectionString = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=TestDb2;Persist Security Info=True;integrated Security=True";

    static void Main()
    {
        using (var dc = new DataContextDom(ConnectionString))
        {
            if (dc.DatabaseExists())
                dc.DeleteDatabase();
            dc.CreateDatabase();
            dc.GetTable<DataHelperDb1>().InsertOnSubmit(new DataHelperDb1() { Name = "DataHelperDb1Desc1", Id = 1 });
            dc.GetTable<DataHelperDb2>().InsertOnSubmit(new DataHelperDb2() { Name = "DataHelperDb2Desc1", Key1 = "A", Key2 = "1" });
            dc.SubmitChanges();

            Console.WriteLine("Name:" + GetByID(dc.GetTable<DataHelperDb1>(), 1).Name);
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Name:" + GetByID(dc.GetTable<DataHelperDb2>(), new PkClass { Key1 = "A", Key2 = "1" }).Name);
        }
    }

    //Datacontext definition
    [Database(Name = "TestDb2")]
    public class DataContextDom : DataContext
    {
        public DataContextDom(string connStr) : base(connStr) { }
        public Table<DataHelperDb1> DataHelperDb1;
        public Table<DataHelperDb2> DataHelperD2;
    }

    [Table(Name = "DataHelperDb1")]
    public class DataHelperDb1 : Entity<DataHelperDb1, int>
    {
        [Column(IsPrimaryKey = true)]
        public int Id { get; set; }
        [Column]
        public string Name { get; set; }
    }

    public class PkClass
    {
        public string Key1 { get; set; }
        public string Key2 { get; set; }
    }
    [Table(Name = "DataHelperDb2")]
    public class DataHelperDb2 : Entity<DataHelperDb2, PkClass>
    {
        [Column(IsPrimaryKey = true)]
        public string Key1 { get; set; }
        [Column(IsPrimaryKey = true)]
        public string Key2 { get; set; }
        [Column]
        public string Name { get; set; }
    }

    public class Entity<TEntity, TKey> where TEntity : new()
    {
        public static TEntity SearchObjInstance(TKey key)
        {
            var res = new TEntity();
            var targhetPropertyInfos = GetPrimaryKey<TEntity>().ToList();
            if (targhetPropertyInfos.Count == 1)
            {
                targhetPropertyInfos.First().SetValue(res, key, null);
            }
            else if (targhetPropertyInfos.Count > 1) 
            {
                var sourcePropertyInfos = key.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
                foreach (var sourcePi in sourcePropertyInfos)
                {
                    var destinationPi = targhetPropertyInfos.FirstOrDefault(x => x.Name == sourcePi.Name);
                    if (destinationPi == null || sourcePi.PropertyType != destinationPi.PropertyType)
                        continue;

                    object value = sourcePi.GetValue(key, null);
                    destinationPi.SetValue(res, value, null);
                }
            }
            return res;
        }
    }

    public static IEnumerable<PropertyInfo> GetPrimaryKey<T>()
    {
        foreach (var info in typeof(T).GetProperties().ToList())
        {
            if (info.GetCustomAttributes(false)
            .Where(x => x.GetType() == typeof(ColumnAttribute))
            .Where(x => ((ColumnAttribute)x).IsPrimaryKey)
            .Any())
                yield return info;
        }
    }
    //Move in repository pattern
    public static TEntity GetByID<TEntity, TKey>(Table<TEntity> source, TKey id) where TEntity : Entity<TEntity, TKey>, new()
    {
        var searchObj = Entity<TEntity, TKey>.SearchObjInstance(id);
        Console.WriteLine(source.Where(e => e.Equals(searchObj)).ToString());
        return source.Single(e => e.Equals(searchObj));
    }
}

结果:

SELECT [t0].[Id], [t0].[Name]
FROM [DataHelperDb1] AS [t0]
WHERE [t0].[Id] = @p0

Name:DataHelperDb1Desc1


SELECT [t0].[Key1], [t0].[Key2], [t0].[Name]
FROM [DataHelperDb2] AS [t0]
WHERE ([t0].[Key1] = @p0) AND ([t0].[Key2] = @p1)

Name:DataHelperDb2Desc1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接