Comments 53
Наколько я понял по исходникам, есть поддержка TPH и TPC, но не TPT.
Попробуйте ещё его релизовать.
Попробовал. Оказалось чуть сложнее, чем ожидалось.
// Mapped to Table A
public class A {
public string MappingName {get; set;}
}
//Mapped to Table B
public class B: A {
public long OrderIndex {get; set;}
}
работаем с «B»
Delete from {x} where {x.MappingName} = 'Custom'
И тут выходит, что потребуется фактически парсить Sql, т.к. от конкретного оператора
зависит преобразование.
в этот delete придётся инжектить еще операторы:
delete t1
from B t1
inner join A t2
on A.Id = B.Id
where t2.MappingName = 'Custom'
delete
from A
where MappingName = 'Custom'
Для общего случая с TPT, имхо, не применимо.
В моей реализации кинет ошибку, мол, "такой колонки нет в этой таблице". К этому решению приходишь весьма естественным путем.
private static void FillParameters(this DbCommand cmd, FormattableString sql)
{
var substitutions = new object[sql.ArgumentCount];
for (var i = 0; i < sql.ArgumentCount; i++)
{
var name = string.Concat("p", i.ToString());
var parameter = cmd.CreateParameter();
parameter.ParameterName = name;
parameter.Value = sql.GetArgument(i);
cmd.Parameters.Add(parameter);
substitutions[i] = string.Concat("@", name);
}
cmd.CommandText = sql.ArgumentCount > 0 ? string.Format(sql.Format, substitutions) : sql.Format;
}
До такого он не упростится никак — потому что параметры там "виртуальные", в их роли могут выступать имена таблиц и колонок.
Более того, его даже в форме Expression<Func<T, FormattableString>>
использовать не получится — потому что компилятор создает наследника для этого типа с заранее неизвестным конструктором, из которого непонятно как вытаскивать аргументы.
Попробовал все же реализовать. Пусть не весь функционал, но справляется с:
dc.FormatSql<Order>(x => $"DELETE FROM {x} WHERE {x.Subtotal} = 0");
public static string FormatSql<T>(this DbContext context, Expression<Func<T, FormattableString>> expression)
{
var body = (MethodCallExpression) expression.Body;
var sql = (string) ((ConstantExpression) body.Arguments[0]).Value;
var args = ((NewArrayExpression) body.Arguments[1]).Expressions;
var parameters = new object[args.Count];
for (var i = 0; i < args.Count; i++)
{
var arg = args[i];
if (arg.NodeType == ExpressionType.Parameter) // table
{
var tableName = context.GetTableName(arg.Type);
parameters[i] = $"[{tableName}]";
}
else
{
var operand = ((UnaryExpression) arg).Operand;
if (operand is MemberExpression me) // column
{
var tableName = context.GetTableName(me.Expression.Type);
var columnName = context.GetColumnName(me.Expression.Type, me.Member.Name);
parameters[i] = $"[{tableName}].{columnName}";
}
else // parameters
{
// TODO:
}
}
}
return string.Format(sql, parameters);
}
Вы какбы учитывайте, что некоторые параметры надо передать именно объектами, дабы EF сам обернул их в SqlParameter
чтобы избежать, например SQL-инъекций и передачи дат в неправильном формате.
Мне вот эта часть жутко не нравится:
// собираем бесконтекстное лямбда-выражение
var lex = Expression.Lambda(cArg);
// компилим
var compiled = lex.Compile();
// вычисляем
var result = compiled.DynamicInvoke();
Неужели никто не знает способа ускорить это дело?
Мне она тоже не нравится. Написал из того, что было под рукой. Там вон ниже человек предложил FastExpressionCompiler, но сдается мне, что если вы самостоятельно вычислите параметры в замыканиях и будете подставлять в запрос готовые переменные, то существенного прироста в скорости от изменения способа компиляции не будет (сугубо ИМХО).
Ну и да. По сравнению с проходом метаданных EF, .Compile/.DynamicInvoke — это быстро :)
Лямбду можно скомпилировать в конкретный делегат. По скорости будет как если бы вы написали эту лямбду в коде. Замыкания стоит вытащить в аргументы, что-бы не плодить объекты.
Взгляните на GenerateGetHashCode, он по-понятней будет.
Этот человек я =) Нет, вы получаете настоящую взаправдовскую лямбду.
Вызов выглядит так
var x = getHashCodeLambda(obj);
Эм… Вы точно статью читали? Мне не нужна лямбда.
Вы исползуете DynamicInvoke, сначит вы используете лямбду. Но ваша композиция не позволяет эффективно использовать кодогенерацию.
К примеру, вы вызываете Compile для каждого параметра, что 1, очень медленно (Сначала вы компилите в IL, потом в машинный код), 2, при длительном использовании программа свалится, когда не будет места для выделения памяти для нового куска кода.
Я бы создал лямбду для каждой связки строка + аргументы и дергал бы их.
У нас задача: вычислить каждый аргумент и сложить их в массив. Тут максимум что можно сделать — собрать лямбду а-ля NewArrayInit и единожды скомпилировать/посчитать. Никак не возьму в толк что вы предлагаете.
class Program
{
class MyClass
{
public string Data { get; set; }
private static Dictionary<Expression, Dictionary<Expression, Func<object, string>>> Cache = new Dictionary<Expression, Dictionary<Expression, Func<object, string>>>();
public string ParseInterpolation(Expression<Func<MyClass, string>> expression)
{
var extrapolationCallExpr = (MethodCallExpression)expression.Body;
var args = extrapolationCallExpr
.Arguments
.Skip(1)
.Select(x => x is UnaryExpression e ? e.Operand : x)
.ToArray();
if (Cache.ContainsKey(expression) == false)
{
var cache = new Dictionary<Expression, Func<object, string>>();
Cache.Add(expression, cache);
var frmtStr = (string)((ConstantExpression)extrapolationCallExpr.Arguments.First()).Value;
var converters = new List<Func<object, string>>();
for (int i = 0; i < args.Count(); i += 1)
{
switch (unwrapClosure(args[i]))
{
case ConstantExpression e:
{
var param = Expression.Parameter(typeof(object));
var access = Expression.MakeMemberAccess(
Expression.Convert(
param,
((MemberExpression)args[i]).Expression.Type),
((MemberExpression)args[i]).Member);
cache.Add(
e,
Expression.Lambda<Func<object, string>>(
Expression.Call(
access,
getMemberType(((MemberExpression)args[i]).Member).GetMethod("ToString", new Type[0])
),
param).Compile()
);
}
break;
case MemberExpression e:
{
if (e.Expression.Type == typeof(MyClass))
{
var memberType = getMemberType(e.Member);
var param = Expression.Parameter(typeof(object), e.Member.Name + "_param");
var access = Expression.MakeMemberAccess(
Expression.Convert(
param,
typeof(MyClass)),
e.Member);
var call =
Expression.Call(
((Func<string, string, string>)convertMember).Method,
Expression.Constant(e.Member.Name, typeof(string)),
Expression.Call(
access,
memberType.GetMethod("ToString", new Type[0])
));
cache.Add(e, Expression.Lambda<Func<object, string>>(call, param).Compile());
}
}
break;
default:
throw new NotImplementedException();
}
}
}
var arr =
args
.Select(arg =>
{
var cache = Cache[expression];
switch (unwrapClosure(arg))
{
case ConstantExpression e:
return cache[e](e.Value);
case MemberExpression e:
{
if (e.Expression.Type == typeof(MyClass))
{
if (e.Expression is ParameterExpression)
return cache[e](this);
}
else
{
}
}
break;
default:
throw new NotImplementedException();
}
throw new NotImplementedException();
})
.ToArray(); ;
return string.Format((string)((ConstantExpression)extrapolationCallExpr.Arguments[0]).Value, arr);
Expression unwrapClosure(Expression e)
{
if (e is MemberExpression me)
if (me.Expression is ConstantExpression ce)
if (ce.Type.Name.StartsWith("<>"))
return me.Expression;
return e;
}
Type getMemberType(MemberInfo nfo)
{
switch (nfo)
{
case FieldInfo i:
return i.FieldType;
case PropertyInfo i:
return i.PropertyType;
default:
throw new Exception("Wrong member type.");
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string convertMember(string memberName, string value)
{
return $"member name: {memberName}, member value: {value}.";
}
}
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var obj = new MyClass { Data = "MyClassData" };
var i = 1;
Console.WriteLine(obj.ParseInterpolation(x => $"Static data: {i}, Dynamic data: {x.Data}."));
Console.WriteLine(obj.ParseInterpolation(x => $"Static data: {i}, Dynamic data: {x.Data}."));
var obj2 = new MyClass { Data = "some other data" };
var j = 3;
Console.WriteLine(obj2.ParseInterpolation(x => $"Static data: {j}, Dynamic data: {x.Data}."));
Console.ReadLine();
}
}
Поскольку данные замыкания меняются, закешировать все выражение будет сложно. Можно передавать в нее само дерево, но это реально много работы. Второй вариант закешировать конвертеры. Для каждого аргумента, для каждого схожего случая вызова. Замкнутые переменные можно выдернуть из дерева. Самый большой оверхед тут, это сама генерация дерева.
Однако есть небольшая проблема. Поскольку для каждого вызова метода генерируется новое выражение, для кеширования нужно писать свой IEqualityComparer. Без кеша, как я сказал выше, программа вылетит когда кончится память (выражения компилируются как часть AppDomain'а, а .net не умеет выгружать его частями.)
Теоретически, возможно получится кешировать конвертеры для общего случая — сравнивать маленькие деревья проще и быстрее.
В доках ничего не сказано о выгрузке кода. И метода для этого я не нашел.
А метод для этого и не нужен. Динамические методы собираются сборщиком мусора же.
Defines and represents a dynamic method that can be compiled, executed, and discarded. Discarded methods are available for garbage collection.
— https://msdn.microsoft.com/en-us/library/system.reflection.emit.dynamicmethod(v=vs.110).aspx
Для проблемы, которой передо мной не стоит вы предоставили решение, о котором вас не просили.
Вдобавок ещё и неправильное. Cache.ContainsKey(expression)
всегда будет давать false, как вы верно заметили. EqualityComparer для лямбда-выражения, пожалуйста, в студию.
Поймите, наконец, что мне не нужно кэшировать аргументы. Мне их надо высчитывать каждый раз, ибо как оные могут быть разные.
Ознакомьтесь, пожалуйста, со спецификой задачи ещё раз.
Если бы вы еще предложили эффективный способ сравнения элементов — все было бы вообще замечательно!
Вы понимаете что перед каждым вызовом Stoke внешний код генерирует новое AST, не содержащее ничего от старого?
Вот вам пример. Допустим, у нас есть вот такой запрос:
var baz = ...;
db.Stroke<Foo>(x => $"UPDATE {x} SET {x.Bar}={x.Bar+1} WHERE {x.Baz} = {baz}");
Компилятор константу baz передает примерно вот так:
var scope = new { baz = ... };
Expression.Field( Expression.Constant(scope), "baz" );
Что из этого вы будете кешировать? И как вы вообще поймете что тут можно хоть что-то закешировать?
Ах да, еще требуется чтобы этот способ не развалился на новых версиях компилятора или в нестандартной ситуации (например, когда переменная baz захвачена в замыкание или является полем какого-нибудь класса)
Может лучше так сделать?
var emptyOrders = from o in db.Orders where o.Subtotal == 0 select o;
emptyOrders.Delete();
var allOrders = from o in db.Orders select o;
allOrders.Delete();
var old = DateTime.Today.AddDays(-30);
var oldCustomers = from c in db.Customers where c.RegisterDate < old select с;
oldCustomers.Update(c => new { IsActive = 0});
var items = from i in db.Items where i.Order.Subtotal == 0 select i;
items.Update(i => new { Name = '[FREE] ' + i.Name });
Ну вот попробуйте и сделайте :)
Спойлер: не вы первый додумались до такого крутого интерфейса использования. Проблема в том, что для него требуется писать свой конструктор запросов, что равноценно переписыванию EF с нуля.
Возможно уже все сделано за нас: http://entityframework-plus.net/. Использовать не пробовал.
Зато я пробовал. Z.EntityFramework.Extensions. 800 баксов стоит эта радость.
А это не их исходники? https://github.com/zzzprojects/EntityFramework-Plus
// и этот метод - точно String.Format?
if (bdy.Method.DeclaringType != typeof(String) && bdy.Method.Name != "Format")
{
throw new Exception(err);
}
Наверное здесь все-таки задумывалась более жесткая проверка условий через «ИЛИ» (||)?
копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться.
Совсем недавно столкнулся с использованием службы SQL как прокси-доступа к базе SQL для среды NET с утечками памяти (кто-то посоветовал кешировать метадату), и ужасает, что подобные статьи не содержат примеров правильного использования IDisposable контекстов в using (для наглядности новичкам)
Сомневаюсь, что вы горите желанием писать хранимку для каждого однострочного запроса.
Если бы я это сделал на текущем рабочем месте — у нас бы из воздуха появилось ~200 хранимок.
Плюс хранимки не решают проблемы устойчивости к переименованию и рефакторингу.
Прямой SQL в EntityFramework. Теперь со строгой типизацией