Pull to refresh

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, имхо, не применимо.

В моей реализации кинет ошибку, мол, "такой колонки нет в этой таблице". К этому решению приходишь весьма естественным путем.

ИМХО, имеет смысл упомянуть в статье об этом ограничении. TPT не такая уж редко используемая конфигурация.

Да я в гитхаб лучше докоммичу решение и там в readme упомяну


К слову: TPT как раз встретился в том проекте, в рамках которого я изобрел этот подход. Добрался до него в ходе тестирования.

Есть такая замечательная штука, как FormattableString. Код заполнения параметров команды упрощается примерно до такого:
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>> использовать не получится — потому что компилятор создает наследника для этого типа с заранее неизвестным конструктором, из которого непонятно как вытаскивать аргументы.

Конструктор известен, это FormattableStringFactory.Create(string, params object[]).
Попробовал все же реализовать. Пусть не весь функционал, но справляется с:
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-инъекций и передачи дат в неправильном формате.

Как раз для этого там и стоит TODO :)

Тогда мне неочевидно в чем профит использования FormattableString :( Возможно я глупенький.

Как минимум обходится проверка на число параметров у string.Format(...)
Вам этого мало? :)
Но вообще да, больше плюсов от использования FormattableString нет, остальной код копирует ваш один в один.

Мне вот эта часть жутко не нравится:


            // собираем бесконтекстное лямбда-выражение
            var lex = Expression.Lambda(cArg);
            // компилим
            var compiled = lex.Compile();
            // вычисляем
            var result = compiled.DynamicInvoke();

Неужели никто не знает способа ускорить это дело?

UFO just landed and posted this here

Мне она тоже не нравится. Написал из того, что было под рукой. Там вон ниже человек предложил FastExpressionCompiler, но сдается мне, что если вы самостоятельно вычислите параметры в замыканиях и будете подставлять в запрос готовые переменные, то существенного прироста в скорости от изменения способа компиляции не будет (сугубо ИМХО).
Ну и да. По сравнению с проходом метаданных EF, .Compile/.DynamicInvoke — это быстро :)

Лямбду можно скомпилировать в конкретный делегат. По скорости будет как если бы вы написали эту лямбду в коде. Замыкания стоит вытащить в аргументы, что-бы не плодить объекты.


Тут есть примеры


Взгляните на GenerateGetHashCode, он по-понятней будет.

Там человек так же применяет .Compile и, скорее всего, последующий .DynamicInvoke, что полностью эквивалентно решению, изложенному в статье, которым не доволен mayorovp.

Этот человек я =) Нет, вы получаете настоящую взаправдовскую лямбду.
Вызов выглядит так


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 не умеет выгружать его частями.)


Теоретически, возможно получится кешировать конвертеры для общего случая — сравнивать маленькие деревья проще и быстрее.

Выражения компилируются как Dynamic Method, а их .net выгружать умеет.

В доках ничего не сказано о выгрузке кода. И метода для этого я не нашел.

Для проблемы, которой передо мной не стоит вы предоставили решение, о котором вас не просили.

Вдобавок ещё и неправильное. 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 захвачена в замыкание или является полем какого-нибудь класса)

Э… вы уверены что это именно я не понял? Или это вы о том что я случайно {x.Bar+1} вместо {x.Bar}+1 написал? :-)

Пардон, невнимателен. Конечно же не вы. Прошу прощения :)

Есть еще такая вещь, как linq2db. И одни и те же классы для маппинга таблиц базы можно размечать двумя типами атрибутов сразу — и EF, и linq2db.
Прощу обучить EF понимать атрибуты linq2db.

Может лучше так сделать?


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 с нуля.

Зато я пробовал. Z.EntityFramework.Extensions. 800 баксов стоит эта радость.

Их, но там далеко не всё, что нужно.

Там полные исходники насколько я вижу. Правда там есть только реализация удаления и обновления но нет вставки, что странно. Реализация вставки должна быть чуть проще чем обновления.

    // и этот метод - точно String.Format?
    if (bdy.Method.DeclaringType != typeof(String) && bdy.Method.Name != "Format")
    {
        throw new Exception(err);
    }

Наверное здесь все-таки задумывалась более жесткая проверка условий через «ИЛИ» (||)?
Хотелось бы увидеть зависимости от версий, сейчас MS предлагает NET Standart 2.0 и для него EF 2.0, вот только код из старых версий плохо переносится (особенно касается части EF Core, аналогично под SQlite).
копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться.

Совсем недавно столкнулся с использованием службы SQL как прокси-доступа к базе SQL для среды NET с утечками памяти (кто-то посоветовал кешировать метадату), и ужасает, что подобные статьи не содержат примеров правильного использования IDisposable контекстов в using (для наглядности новичкам), зато технологично.

Эм… в нормальных системах контексты запихиваются в IoC-контейнер и диспозятся сами, используя настройки лайфтайма контейнера. У меня, кстати, контекст обернут в юзинг. Так что нече.


Про EF Core — сказал же — упражнение читателю

UFO just landed and posted this here

Сомневаюсь, что вы горите желанием писать хранимку для каждого однострочного запроса.
Если бы я это сделал на текущем рабочем месте — у нас бы из воздуха появилось ~200 хранимок.
Плюс хранимки не решают проблемы устойчивости к переименованию и рефакторингу.

UFO just landed and posted this here

Одинаковых запросов.
Нужда возникла из необходимости удалять тысячи сущностей из БД, не создавая аналогичное число объектов в памяти, когда можно просто указать критерий удаления. Проект об управлении складом.

Для того, чтобы избежать переименования параметров, можно использовать nameof.
Нельзя: имя свойства в классе и атрибута в базе могут отличаться.
Sign up to leave a comment.

Articles