С# LINQ to SQL: рефакторинг этого общего метода GetByID
Я написал следующий метод.
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);
}
В основном это метод в классе Generic, где T
- это класс в DataContext.
Метод получает таблицу из типа T (GetTable
) и проверяет для первого свойства (всегда являющегося идентификатором) введенный параметр.
Проблема с этим заключается в том, что мне пришлось сначала преобразовать таблицу элементов в список, чтобы выполнить 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 [View Code]:
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: The
member
'MusicRepo_DataContext.IHasID.ID' has
no supported translation to 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();
}
Ответы
Ответ 1
Вам нужно построить дерево выражений, которое 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 to SQL имеет значение, когда вы пишете запрос типа
var Q = from t in Context.GetTable<T)()
where t.id == id
select t;
Вы просто выполняете работу для LTS, потому что компилятор не может создать это для вас, поскольку ничто не может гарантировать, что T имеет свойство "id", и вы не можете сопоставить произвольное свойство "id" от интерфейса к базе данных.
==== UPDATE ====
ОК, здесь простая реализация для поиска имени первичного ключа, предполагая, что есть только один (а не составной первичный ключ) и предполагая, что все хорошо по типу (т.е. ваш первичный ключ совместим с "коротким" тип, который вы используете в функции GetById):
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;
}
Ответ 2
Что делать, если вы переработаете это, чтобы использовать GetTable(). Где (...) и помещаете ли вы свою фильтрацию?
Это будет более эффективно, так как метод расширения Where должен заботиться о вашей фильтрации лучше, чем выбор всей таблицы в список.
Ответ 3
Некоторые мысли...
Просто удалите вызов ToList(), SingleOrDefault работает с IEnumerably, который я предполагаю в таблице.
Сбросить вызов e.GetType(). GetProperties(). Сначала(), чтобы вернуть возвращаемое свойство.
Нельзя ли добавить ограничение на T, чтобы заставить их реализовать интерфейс, который предоставляет свойство Id?
Ответ 4
Возможно, выполнение запроса может быть хорошей идеей.
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();
}
}
Ответ 5
Относительно:
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(); }
}
}
}
Ответ 6
Хорошо, проверьте эту демонстрационную реализацию. Является попыткой получить общий GetById с datacontext (Linq To Sql). Также совместим с многозначным свойством.
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