Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Часто мне кажется, что в большой степени разочарование ORM связано с раздутыми ожиданиямиХотелось бы добавить к сказанному еще несколько моментов.
Как выяснилось, запрос, который строил EF, содержал 8 вложенных select-выражений!
Во-первых сам EF ничего не строит сам, он только интерпретирует ваш запросКонечно. Но только интерпретировать он его может по-разному. И получается, что помимо LINQ я должен понимать особенности интерпретации expression tree с запросом каждым конкретным провайдером. Т.е. абстракция уже не скрывает от меня детали реализации, а значит она «течет».
То есть проблема не в том, что EF плохойЯ не говорил, что EF плохой! EF отличный, просто нужно понимать, что он не избавляет от необходимости хорошо понимать его работу с СУБД. Вы, кстати, наглядно аргументировали это в своем комментарии.
И получается, что помимо LINQ я должен понимать особенности интерпретации expression tree с запросом каждым конкретным провайдером.
абстракция уже не скрывает от меня детали реализации, а значит она «течет»
… чтобы те, кто рассматривает возможность использования EF или других ORM в первый раз, понимали, с какими ограничениями они столкнутся в будущем, и учитывали это в своих проектных решениях
я говорю да Repository, ведь все описанные проблемы легко обойти с помощью этой абстракции которая обернет EF и поглотит проблемы совместимостиОно вообще похоже на то, что найдена «серебряная пуля». Но мне кажется, что здесь свое дело сделали эмоции автора, ведь и стиль изложения материала в статье несколько «импульсивный», имхо. Насчет Repository над EF не хочу писать обрывками: думаю, идея полезная и может решить часть проблем, но каких и как — нужно пояснить. Попробую сформулировать это чуть позже.
Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.
If you have a specific ORM in mind, then be explicit about it. Don't hide it behind an interface. It creates the illusion that you can replace one implementation with another. In practice, that's impossible.
я говорю да Repository, ведь все описанные проблемы легко обойти с помощью этой абстракции которая обернет EF и поглотит проблемы совместимости.
If you have a specific ORM in mindда это так, меня очень волнует как конкретная ОРМ обрабатывает мои запросы.
Don't hide it behind an interface. It creates the illusion that you can replace one implementation with another. In practice, that's impossible.«Не скрывайте ее за интерфейсом», да это то о чем пишу я, общий IQuariable это большая проблема. «Это создает иллюзию, что можно заменить одну реализацию другой» именно об этом я и пишу, нельзя один провайдер заменить другим и думать что это прокатит. Заметьте у меня в голове Repository который знает о проблемах конкретных провайдеров и борется только с ними а не с общими проблемами ОРМ.
используется ограниченный набор запросов
Доступно все, что описано в предметной области, это ограждает от невалидных и неэффективных запросов
можно делать стандартным для реляционных БД способом
что вы подразумеваете под «можно делать стандартным для реляционных БД способом»
LIKE '%term%'Совсем необязательно урезать доступную коду-потребителю функциональностья согласен
а для критичных запросов иметь отдельныеа как узнать какой запрос критический если у нас «доступную коду-потребителю функциональность» есть доступ и тут можно пилить что угодно и как угодно.
Ничего не напоминает? Это реализация Repository + UoW. [...] Основная идея EF ( как и любого Repository )...
я говорю да Repository, ведь все описанные проблемы легко обойти с помощью этой абстракции
Вот тут самое интересное, под разные провайдеры нужно писать свои запросы на LINQ. Еще интересный сценарий: написали некоторый функционал с кучей запросов к контексту и все прекрасно работает. Но приложение cross и нужно проверить, как это все будет работать на другом провайдере. Было обнаружено, что часть запросов стали не эффективные. Не беда – оптимизация. Все хорошо. Вопрос как оптимизация отразилась на работе при исходном провайдере?
Ограничения запроса
В EF разработчик может сформировать запросы, используя или LINQ to Entities, или Entity SQL или их комбинацию. В любом случае такой запрос преобразовывается механизмом EF в промежуточное представление как expression tree и отправляется в EF-провайдер, чтобы он сгенерировал SQL запрос. Так как EF была первоначально разработана для SQL Server, то иногда EF-провайдеры должны значительно преобразовать первоначальное expression tree, чтобы сгенерировать корректный SQL для определенной базы данных. Однако это не всегда возможно.
Здесь ясно сказано mission impossible. SQL SQLю рознь, провайдер провайдеру. Тут даже речь не о том, что запрос будет не эффективен, а о том, что он может покрошить выполнение приложения.
Repository replaces specialized finder methods on Data Mapper classes with a specification-based approach to object selection. Compare that with the direct use of Query Object, in which client code may construct a criteria object (a simple example of the specification pattern), add() that directly to the Query Object, and execute the query. With a Repository, client code constructs the criteria and then passes them to the Repository, asking it to select those of its object that match. From client code's perspective, there's no notion of query «execution»; rather there's the selection of appropriate objects through the «satisfaction» of the query's specification. This may seem an academic distinction, but it illustrates the declarative flavor of object interaction with Repository, which is a large part of its conceptual power. [PoEAA, 2003, pp. 323-324] A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer.И если рассматривать в качестве реализации data-mapper только часть EF, то я не вижу каких-то особенных противоречий в утверждении, что EF реализует в том числе и шаблон Repository. Ведь и в приведенной Вами цитате сказано, что:
...This may seem an academic distinction, but it illustrates the declarative flavor of object interaction with Repository, which is a large part of its conceptual power.Т.е. концептуально EF конечно не репозиторий, поскольку репозиторий более окрашен в «тона предметной области». Так же, как и Specification по сравнению с Query Object, как мне кажется. С другой стороны, формально фасад EF очень похож на описание фасада репозитория.
Т.е. концептуально EF конечно не репозиторий, поскольку репозиторий более окрашен в «тона предметной области». Так же, как и Specification по сравнению с Query Object, как мне кажется.
С другой стороны, формально фасад EF очень похож на описание фасада репозитория.
Что именно вы считаете фасадом EF?В данном случае я имею ввиду DbSet<T>, который, реализуя IQueryable<T> и добавляя Add и Remove, фактически и действует как
… in-memory domain object collection
SaveChanges. И вот тут вся эта конструкция превращается в UoW, и начинается путаница, которой лучше бы избегать.System.Transactions?А как вы гарантируете поддержку всеми репозиториями этой системной транзакции
System.Transactions (ну или вашу транзакцию, что, в принципе, одинаково). Как вы это гарантируете?SaveChanges в публичном интерфейсе.public class FooRepo {
private FooDbContext _dbContext;
public FooRepo() {
_dbContext = new FooDbContext();
}
public GetFooByParamSet1(paramSet1) { ... }
public GetFooByParamSet2(paramSet2) { ... }
...
public AddFoo() {}
public RemoveFoo() {}
...
public SaveChanges() {}
}
public interface IFooDbContext {
GetFooByParamSet1(paramSet1);
GetFooByParamSet2(paramSet2);
AddFoo();
RemoveFoo();
SaveChanges();
}
class FooDbContext: DbContext, IFooDbContext {
public GetFooByParamSet1(paramSet1) { ... }
public GetFooByParamSet2(paramSet2) { ... }
...
public AddFoo() {}
public RemoveFoo() {}
...
public SaveChanges() {}
}
class FooDbContext: DbContext
{
public DbSet<A> A {get;}
public DbSet<B> B {get;}
}
interface IFooDbContext
{
DbSet<A> A {get;}
DbSet<B> B {get;}
}
interface IFooDbContext
{
IQueryable<A> A {get;}
IQueryable<B> B {get;}
}
IQueryable на что угодно. При этом важно то, что для потребителей это как был интерфейс контекста данных, так и остался, просто он стал чуть более обобщенным. Мы не порождаем никаких новых сущностей и новых слоев.Ограничение доступа к DbContext-у очень условное: т.е. можно конечно коду-потребителю передавать IFooDbContext и там будет только то, что мы туда внесли. Но ведь пытливый разработчик может всегда привести его к типу FooDbContext и получить тот же DbSet и снова начать плодить «зверинец» разнообразных запросов. Конечно, в случае замены наследования композицией можно тоже взять Reflection и сделать все, что хочется, но это уже явный «хак», на который просто так никто не решится.
Что если мы захотим написать unit tests к методам GetFooByParamSet1 и т.д.? Как мы заменим сам DbContext каким-нибудь mock-объектом?
Set в DbContext — никак. Какой-то из уровней абстракции тестировать уже не нужно. Если вы хотите написать тесты на этот уровень, и при этом это реально оправданно, а не блажь, значит, у вас ошибка в разделении обязанностей.Если так, то как мы решаем проблему «зверинца» запросов от потребителей? Хочется ведь их свести вместе и ограничить-таки интерфейс DbContext-а.interface IFooDbContext { IQueryable<A> A {get;} IQueryable<B> B {get;} }
If you have a specific ORM in mind, then be explicit about it. Don't hide it behind an interface. It creates the illusion that you can replace one implementation with another. In practice, that's impossible. Imagine attempting to provide an implementation over an Event Store.Во-первых, статья называется «IQueryable is Tight Coupling» и весь ее посыл в том, чтобы не использовать этот интерфейс в своем API. Мне даже кажется, что Симан считает, что IQueryable вообще не стоило разрабатывать, поскольку абстракция «дырявая» до неприличия. Хотя непонятно, как тогда стоило бы поступить.
What do you think about the API outlined in this post: http://stackoverflow.com/a/9943907/572644И вот ответ Марка:
Looks good, Daniel. I'd love to hear your experience with this once you've tried it on for some timeПо ссылке на StackOverflow есть пример кода, где IQueriable скрыт за своим интерфейсом, который уже не предоставляет IQueryable.
Во-первых, статья называется «IQueryable is Tight Coupling» и весь ее посыл в том, чтобы не использовать этот интерфейс в своем API.
IQueryable — это текущая абстракция, которая не предоставляет изоляции. Если это осознавать, равно как и осознавать ограничения этого — все будет хорошо.Возникает вопрос: implementation of what? Мне кажется, что речь об интерфейсе IQueryable.
IQueryable, но это побочный эффект).В-третьих, в подтверждение этого есть комментарий к статье с просьбой к Марку оценить интерфейс репозитория над NHibernate:
IQueryable плоха (потому что его реализации не взаимозаменяемы). Если нет — то нет.IQueryable, а есть IQueryableEF, IQueryableL2S, IQueryableNH и так далее. Когда вы принимаете решение выставить такой интерфейс — вы принимаете решение «я не буду заменять EF на NH». Если это решение вас устраивает, то ничего плохого в этом интерфейсе нет.IQueryable, мы берем на себя обязанность предоставить замену. Банальный пример: как в предложенном DSL выразить запрос на (а) начатые и незавершенные (б) начатые или незавершенные? А проекции? А джойны? Ну и понеслась. А интеграция с UI и сервисной моделью навроде OData?)Нет, ее посыл в том, что IQueryable — это текущая абстракция, которая не предоставляет изоляции. Если это осознавать, равно как и осознавать ограничения этого — все будет хорошо.А эти слова разве я написал в начале статьи?
From time to time I encounter people who attempt to express an API in terms of IQueryable. That's almost always a bad idea. In this post, I'll explain why
весь ее посыл в том, чтобы не использовать этот интерфейс в своем API
Вам кажется. На самом деле, речь идет о замене ORM за абстрактным интерфейсомКогда мне говорят, что мне кажется, а собеседник знает, как «на самом деле», я очень настораживаюсь: возможно у человека сверхспособности. Боюсь что в данном случае у нас с Вами просто разный прошлый опыт и набор тех проблем, с которыми мы сталкивались. Поэтому Вы смотрите на это в своем контексте, а я в своем. А кто из нас «на самом деле» прав, может сказать только Марк Симан.
Если вы предполагаете заменять хранилище — тогда зависимость от IQueryable плоха (потому что его реализации не взаимозаменяемы). Если нет — то нет.Но заметьте, что автор ответа на SO (он же и автор вопроса) нигде не говорит о том, что собирается заменять NHibernate. И тем не менее, он зачем-то инкапсулирует IQueryable внутри репозитория.
как только мы отбираем IQueryable, мы берем на себя обязанность предоставить замену. Банальный пример: как в предложенном DSL выразить запрос на (а) начатые и незавершенные (б) начатые или незавершенные? А проекции? А джойны? Ну и понеслась. А интеграция с UI и сервисной моделью навроде OData?А вот тут я с Вами полностью согласен. Как только у нас появляется разнообразие запросов, мы будем изобретать велосипед, создавая нечто такое же гибкое, как IQueryable, с теми же проблемами. Вопрос только в том, всегда ли нужно такое разнообразие и те возможности, о которых Вы написали.
На всякий случай напомню, что я написал почти то же самое:
Но заметьте, что автор ответа на SO (он же и автор вопроса) нигде не говорит о том, что собирается заменять NHibernate.
Вопрос только в том, всегда ли нужно такое разнообразие и те возможности, о которых Вы написали.
Как только у нас появляется разнообразие запросов, мы будем изобретать велосипед,если запросов очень много, то лучше всего использховать habrahabr.ru/post/125720
Я работаю над проектом, в котором два боевых слоя хранилища и еще одно демонстрационное.
Допустим в некой задаче нет необходимости поддерживать разные хранилища/ORM, есть только EF и, к примеру, SQL Server. Но в то же время набор возможных запросов к БД сильно ограничен и расширяется очень предсказуемо. Стали бы Вы оборачивать EF в репозиторий с простым интерфейсом, или же просто использовали бы DbContext?
А каким был интерфейс репозитория? Универсальным, т.е. выставлялся IQueryable или его аналог, или просто содержал конечный набор методов, который и определял набор возможных запросов?
И еще интересно было бы узнать Ваше мнение. Допустим в некой задаче нет необходимости поддерживать разные хранилища/ORM, есть только EF и, к примеру, SQL Server. Но в то же время набор возможных запросов к БД сильно ограничен и расширяется очень предсказуемо. Стали бы Вы оборачивать EF в репозиторий с простым интерфейсом, или же просто использовали бы DbContext?
а что, не плохо. мы только что ограничили запросы, а вот теперь нам нужно получить элементы по paramSet2 и paramSet3, и вотGetFooByParamSet2(paramSet2); GetFooByParamSet3(paramSet3); //-------- GetFooByParamSetN(paramSetN);
GetFooByParamSetN+1(paramSetN+1);
100% рабочий вариант, спору нет, но чем он лучше? проще — да, но попытка комбинирования запросов приводит к разрастанию интерфейса — это нормально?И как же репозиторий позволяет обойти эти проблемы? Вероятно, вы имеете в виду, что вы напишете две разных реализации репозитория для двух разных провайдеров. Это здорово, но вы помните, что вы только что увеличили свои затраты вдвое?
Как же репозиторий ее решает в таком случае?
Однако это не всегда возможно.
В двое? Я реализую абстракцию, которая позволит переопределить обработку запросов отдельно, и в каждой реализации переопределю только не эффективные.
Как же репозиторий ее решает в таком случае?
написано причем не мной
Однако это не всегда возможно.
на это я ответил выше.
А где реализованы те, которые вы переопределяете.
Во-первых, вы передергиваете: нигде не сказано, что эта задача не имеет решения; сказано лишь что не всегда возможно сгенерировать корректный SQL для всех БД для любого AST. Но предположим на минуту, что вы правы, и эта задача действительно не имеет решения. Как же репозиторий ее решает в таком случае?
написано причем не мной
Однако это не всегда возможно.
и эта задача действительно не имеет решения. Как же репозиторий ее решает в таком случае?
А где реализованы те, которые вы переопределяете.
в базовом репозитории.
можно выполнить другой эквивалентный запрос внутри репозитория
Наверное, вами же реализованы? Вот и увеличение ресурсных затрат.
Ох. Давайте пойдем сначала. Вот есть проблема: не всякий запрос, переданный в DAL снаружи (и, как следствие, сформулированный в терминах объектов), можно преобразовать в запрос в терминах хранилища. Я правильно вас понял, или вы что-то другое имеете в виду?
хорошую архитектуру бесплатно не получить.
все что можно написать в LINQ запросах не всегда можно отразить в SQL запросе.
преимущества Repository бесплатныя ни где об этом не пишу, я пишу что это способ решить проблему совместимости
И как репозиторий решает эту проблему?
я ни где об этом не пишу, я пишу что это способ решить проблему совместимости
передав информацию о том что нужно, репозиторий переведет ее в как получить.
Вы пишете, что все описанные проблемы можно легко решить с помощью repository.
А разве (любой LINQ-based DAL) делает не то же самое? В чем преимущества репозитория в решении этой проблемы?то же самое, но один и тот же LINQ запрос может по разному обрабатываться разными провайдерами, а именно построенный запрос для конкретной базы мб не эффективным или даже покрашить систему.
Вот и увеличение ресурсных затрат.
repository.Get<Product>, а productRepository.Get. И это даже имеет смысл, потому что для каждой конкретной сущности наиболее эффективный способ получения/фильтрации/отображения может отличаться. И обычно эти классы все наследуются от базового репозитория, чтобы переиспользовать базовые операции. И вот тут-то и возникает занимательный конфликт (ну, занимательный он для языков без множественного наследования) — у нас есть общий код для (а) всех репозиториев (б) репозиториев под MS SQL (в) репозиториев продуктов — как бы нам теперь весь его переиспользовать в конкретном репозитории продуктов в MS SQL?нам нужен общий интерфейс для провайдеров к разным БДда
этот интерфейс мы запихиваем в базовый класс репозиторияда
нам нужны провайдеры, удовлетворяющие этому интерфейсуне понял юмора, зачем провайдер?
поверх этого интерфейса мы пишем расширения в каждой реализации репозитория для конкретного провайдера БДтолько необходимые
в написании этих расширений мы ограничены возможностями провайдеров из поза-предыдущего пунктатолько из за не совместимости провайдеров это нужно
как бы нам теперь весь его переиспользовать в конкретном репозитории продуктов в MS SQL?
Skip/Take, а второй — нет. Как паттерн Repository поможет вам решить эту проблему?Shift и TakeEntity? Можете привести пример кода одного (любого) из них?virtual IQuariable<T> Shift (IQuariable<T> set, long conut)
{
return set.Skip(count);
}
правда мы используем построитель запросов.
class GenericRepository
{
public IEnumerable GetPaged(int skip, int take)
{
return set.DefaultOrder().Shift(skip).TakeEntity(take).Map();
}
protected virtual IQueryable Shift(IQueryable source, long count)
{
return source.Skip(count);
}
}
class DbProviderARepository: GenericRepository
{
//не оверрайдим ничего, все работает "из коробки
}
class DbProviderBRepository: GenericRepository
{
protected virtual IQueryable Shift(IQueryable source, long count)
{
//например, так, детали не очень важны
return source.AsEnumerable().Skip(count).AsQueryable();
}
}
public IEnumerable GetPaged(int skip, int take)
public IEnumerable Get(Query)
Skip, другая — нет). Меня интересует структура задействованных классов и конкретные точки расширения, которые вы используете, чтобы обойти проблему.var query = new Query();
query.PagingOptions.SkipCount = 10;
query.PagingOptions.TakeCount = 10;
repository.Get(query);
public virtual IEnumerable<Entity> Get( Query query )
{
IQueryable<Entity> filtrate = _table;
//обработка запроса
//-----------
...
filtrate = Build( filtrate, query.PagingOptions );
return Map(filtrate);
}
private IQueryable<Entity> Build<Q>( IQueryable<Entity> filtrate, Q query ) where Q : struct
{
if ( !_builders.ContainsKey( typeof( Q ) ) )
return filtrate;
var builder = (IQueryBuilder<Entity, Q>)_builders[typeof( Q )];
return builder.Build( filtrate, query );
}
public class PagingQueryBuilder<T> : IQueryBuilder<T, PagingOptions>
{
public IQueryable<T> Build( IQueryable<T> queriable, PagingOptions query )
{
var buildQuery = queriable;
if ( query.SkipCount > 0 )
buildQuery = buildQuery.Skip( query.SkipCount );
if ( query.TakeCount > 0 )
buildQuery = buildQuery.Take(query.TakeCount);
return buildQuery;
}
}
protected Dictionary<Type, object> _builders;
Вы писали про 18 критериев фильтрации (правда походу не в этой теме, но отвечу тут, подходящие место), вам всеравно нужно будет их анализировать при построение Linq запроса, так почему же их не сгрупировать на критерии подзапросов, оформить в запрос и передать репозиторию. Или вы предпочитаете эти 18 параметров засунуть в 1 LINQ запрос? или еще хуже передать эти 18 параметров в функцию?
Так в каком же конкретно месте происходит выбор специфичного для конкретной БД способа пейджинга? Если в QueryBuilder, то, удивительным образом, репозиторий для этого не нужен.Нужен, конфигурация билдера происходит в нем, да это можно сделать и в других местах, репозиторий — фасад, скрывающий создание/удаление/запросы/маппинг и конкретную технологию доступа к данным. Я считаю что очень важно запрещать прямой доступ к контексту данных, тк мы получим описанные здесь проблемы.
конфигурация билдера
Я считаю что очень важно запрещать прямой доступ к контексту данных
Что именно вы под этим подразумеваете?связь реализации построителя с параметром запроса.
Для этого достаточно — всего лишь — убрать контекст за интерфейс. Не создать новую прослойку, а просто выделить интерфейс с нужными методами.сделаем Create Save Delete Query и получим тот же репозиторий.
Чем больше вы прячете, тем меньше возможностей вы оставляете пользовательскому коду, тем беднее он становится (если, конечно, вы не тратите неимоверные ресуры на написание полного DSL).Меньше возможностей пользовательскому коду — меньше ошибок, да это может доставить некоторые неудобства, но не зря появляется куча DSL, например XAML.
сделаем Create Save Delete Query и получим тот же репозиторий.
Меньше возможностей пользовательскому коду — меньше ошибок
Я предпочитаю взять 18 критериев фильтрации, пришедших с клиента в виде одного AST, сконвертировать их в другой AST и скормить в DAL. Просто и прямолинейно.да все этого хотят, но на практике приходиться плясать, и не только с бубном, что бы желания вписывались в реалии.
Или вы предпочитаете эти 18 параметров засунуть в 1 LINQ запрос? или еще хуже передать эти 18 параметров в функцию?
У linq запросов есть потрясающее свойство — композируемость. Тебе не надо 18 параметров передавать в функции. Ты можешь 18 раз применить функции (комбинаторы), при этом каждая функция будет тривиальной (IQueryable, param) => IQueryable.именно по этой схеме и работает QueryBuilder о котором я пишу, и нам все равно нужно анализировать каждый из параметров отдельно.
Если же тебе надо будет в каждом вызове передавать 18 параметров, да еще и править каждый раз код при добавлении нового, то ты быстро забьешь на это дело и будешь просто тянуть всю простыню данных на клиент, а потом фильтровать уже объекты. И под любой серьезной нагрузкой это ляжет.кто ж спорит.
именно по этой схеме и работает QueryBuilder о котором я пишу, и нам все равно нужно анализировать каждый из параметров отдельно.
именно по этой схеме и работает QueryBuilder о котором я пишу, и нам все равно нужно анализировать каждый из параметров отдельно.
public virtual IEnumerable<Entity> Get( Query query ) { IQueryable<Entity> filtrate = _table; //обработка запроса //----------- ... filtrate = Build( filtrate, query.PagingOptions ); return Map(filtrate); //Ахтунг }
Прочитал ваш код, у вас нет проекций, вы всегда тянете полные сущности. Это тормозит.это простейший пример, для оптимизации используются параметры прогрузки сущностный (аля Include ), так же в терминах предметной области.
Самое интересное, что метод Get у вас принимает один Query, то есть никакой декомпозиции, все параметры запроса надо выписать по месту вызова.декомпозиция — если не заполнено, значит нет,
У каждой сущности будет постраничная разбивка, у половины фильтры активных, еще у части фильтры доступа.плохо написано, возможно не разобрал, но для каждого репозитория свой query
То есть у вас в Query должны быть все возможные параметры для всех возможных запросов. На практике нежизнеспособное решение.правильно определив корни агрегации и хорошо зная предметную область разбив все крупные запросы на простые как правило получается что и вариантов та таких подзапросов на конкретную предметку не много.
Кстати вы применил зачем-то словарь, вместо полиморфизма. Почему Query не может накладывать фильтры на IQueryable сам?для того что бы решить проблему совместимости, переопределив нужные билдеры запросов
И в итоге все это делалось, чтобы вызвать разные методы Build для разных провайдеров. Только я так и не понял, почему нельзя нужный QueryBuilder заинжектить в БЛ и не использовать репозитории вообще? И проблем с проекциями не будет.потому что реп это не просто построитель запросов — он умеет создавать сохранять удалять еще и маппинг,
это простейший пример, для оптимизации используются параметры прогрузки сущностный (аля Include ), так же в терминах предметной области.
декомпозиция — если не заполнено, значит нет
для каждого репозитория свой query
как правило получается что и вариантов та таких подзапросов на конкретную предметку не много.
правильно определив корни агрегации и хорошо зная предметную область
потому что реп это не просто построитель запросов — он умеет создавать сохранять удалять еще и маппингТо же самое делает EF. То есть вы в репозитории объединили функции построения запросов и исполнения запросов, что нарушает SRP, да еще и пессимизироали программу.
Причем копипаста в итоге будет как в самом классе query, так и в QueryBuilder.о каком копипасте идет речь в билдере?
Но чтобы работало быстро — надо иметь много запросов, под каждый сценарий нужен свой специализированный запрос.поэтому и описние идет в терминах предметной области, а там где запрос выполняется идет анализ как лучше обработать данный запрос.
У меня примитивное приложение с 5 таблицами генерирует 150 разных запросов.на сколько элементарных подзапросов можно их разбить? или они все уникальны?
Берем простой интернет-магазин. Товары-Клиенты-Заказы-Позиции. 4 сущности всего.а для магеров таблички нет? все сущности являются корнями..
Функции: каталог товаров, создание заказа, список заказов для менеджера,
То же самое делает EF. То есть вы в репозитории объединили функции построения запросов и исполнения запросов,умеет ли он строить идентичные запросы которые будут одинаково адекватно выполняться на разных БД — нет. Безопасно ли заменять один провайдер другим — нет. Умеет ли EF адекватно сохранять корни агрегации — нет, умеет ли он правильно сохранять корни — нет.
что нарушает SRPSecure Remote Password?
да еще и пессимизироали программупессимизироали это противоположенное оптимизировали?
о каком копипасте идет речь в билдере?
поэтому и описние идет в терминах предметной области, а там где запрос выполняется идет анализ как лучше обработать данный запрос.
на сколько элементарных подзапросов можно их разбить? или они все уникальны?
а для магеров таблички нет? все сущности являются корнями..
умеет ли он строить идентичные запросы которые будут одинаково адекватно выполняться на разных БД — нет.
Безопасно ли заменять один провайдер другим — нет.
Умеет ли EF адекватно сохранять корни агрегации — нет, умеет ли он правильно сохранять корни — нет.
что нарушает SRP
Secure Remote Password?
пессимизироали это противоположенное оптимизировали?
У вас свой Query на каждый репозиторий, так?Build(filtrate,query.Paging) — много копи паста.
Лучше — сделать проекцию, но вы не можете так сделать, потому что хотите тянуть целые объекты. Все остальное быстродействию не поможет. Самый лучший запрос можно составить точно зная как его результаты будут обрабатываться. Увы ваш репозиторий лишает такой возможности.я привел самый простой случай, ни кто не мешает передать в билдер context и там вывернуть его на изнанку.
То есть запросов получится много даже для такой примитивнй модели. Что уж говорить о сложных моделях…да ладно, можно группировать запросы, например запрос зака, передовать с запросом пользователя в репозитория пользователей, обернуть билдеры в фабрику, а в нутри репозитория можно это все дело совместить и скомбинировать, сделать проекции и тд, не проблема
EF запросы не строит, их строит программиствот именно, это на его совести, к сожалению совесть вещь не предсказуемая.
Прости, но никто не умеет. Если у тебя в модели DateTimeOffset, то половина провайдеров тебя пошлют нафиг. Если у тебя нет DateTimeOffset в модели, то все провайдеры для EF спокойно прожуют твою модель. Лишние прослойки тут не помогут.речь не только о типах, но и о самих провайдерах, как они отработают на одних и тех же запросах заранее сказат ьне возможно
Build(filtrate,query.Paging) — много копи паста.
я привел самый простой случай, ни кто не мешает передать в билдер context и там вывернуть его на изнанку.Вот именно, самый простой случай работает сильно медленнее, чем мог бы. Боюсь представить что в сложном будет.
да ладно, можно группировать запросы, например запрос зака, передовать с запросом пользователя в репозитория пользователей, обернуть билдеры в фабрику, а в нутри репозитория можно это все дело совместить и скомбинировать, сделать проекции и тд, не проблема
вот именно, это на его совести, к сожалению совесть вещь не предсказуемая.
речь не только о типах, но и о самих провайдерах, как они отработают на одних и тех же запросах заранее сказат ьне возможно
Не у каждого query будет Paging.вот именно! каждый из репозиториев будет принимать свой тип запроса.
случай работает сильно медленнееа вы можите написать выражение которое будет делать скип и тэйк быстрее?
Тогда репозиторий превратится в бизнес-логику.то есть если query является составным query.UserOptions и query.OrderOptions то обработка этого запроса это бизнес уровень, а написать LINQ запрос который будет получать пользователей по определенному критерию заказов — это нет. А в чем разница?
Думаешь у программистов, которые пишут репозитории другая совесть?таже, но ограничив запросы предметной области идет естественное ограничение совести, тк нет универсального построителя в котором безсовестно можно писать все что угодно.
вот именно! каждый из репозиториев будет принимать свой тип запроса.
а вы можите написать выражение которое будет делать скип и тэйк быстрее?Конечно, нужно сделать проекцию (select).
А в чем разница?
но ограничив запросы предметной области идет естественное ограничение совести, тк нет универсального построителя в котором безсовестно можно писать все что угодно.
Build(filtrate,query.Paging) надо будет скопипастить как минимум в половине билдеров.тоже самое что и рассматривать за копипаст Skip(n).Take(k), для особого извращения это все можно сделать с помощью рефлексии ( крайне не рекомендую ) универсально.
А твой подход рождает изрядное количество копипасты.запросы изменяются для каждого репозитория отдельно, причем эти запросы специфический, такие запросф как Paging с точки зрения предметной области меняться не будут, может меняться только их построение.
А кто помешает программисту написать любой запрос внутри репозитария?ни кто, но так он будет находиться в конкретном месте.
кроме компилятора рассчитывать не на что.к сожалению компилятор не в силах распознать LINQ запросы которые приведут к краху, тк с точки зрения статического анализа они верны, но перевести их в SQL imposible.
тоже самое что и рассматривать за копипаст Skip(n).Take(k)
А кто помешает программисту написать любой запрос внутри репозитария?
ни кто, но так он будет находиться в конкретном месте.
5) Написать билдерэто делается 1 раз,
Кто помешает там написать плохой запрос?ни кто, но это будет не в 10 местах по коду, а в одном, и так же легко поправиться
это делается 1 раз,
ни кто, но это будет не в 10 местах по коду, а в одном, и так же легко поправиться
для оптимизации используются параметры прогрузки сущностный (аля Include ), так же в терминах предметной области.
IQueryable.Ответ лежит в совместимости конкретных провайдеров. Вот что о совместимости говорит один из разработчиков таких провайдеров Yevhen Shchyholyev www.infoq.com/articles/multiple-databases.
Проблема лежит в попытке универсализации доступа к совершенно разным системам, что на практике не получается.
Типы данных
Поддержка DateTimeOffset
Производительность
Ограничения запроса
Решается тем, что делаются разные генераторы запросов под разные базы, то есть разные наборы функций DbContext => IQueryable. Причем не для всех запросов, а только для тех где имеются значимые различия (обычно не более 5%).как мы в коде будем определять какие можно запросы использовать а какие нет?
В итоге я так и не понял зачем городить Repository. И это как-бы самый сложны случай, когда надо поддерживать несколько СУБД. По факту же 99% и более приложений работают с одной базой и у них вообще таких проблем нет.так и есть, но если бы вы действительно внимательно читали статью
Я работаю над проектом, в котором два боевых слоя хранилища и еще одно демонстрационное.
как мы в коде будем определять какие можно запросы использовать а какие нет?
А зачем это определять?
class SomeBL
{
SomeBL(private IPlatformSpecificQuery queryService, MyDbContext context) {....}
IQueryable<Entity> GetEntites()
{
// some code
return queryService.PlatfomSpecificQuery1(context, ...);
}
}
SomeBL(private IPlatformSpecificQuery queryService, MyDbContext context)то есть вы не управляете жизнью контекста? Предоставляя открытый доступ к MyDbContext, печально,
IQueryable GetEntites() а дальше мы выставляем общий интерфейс с проблемами о которых описано. Круто, чего сказать.
IQueryable, все критерии фильтрации применяются туда. Магия компонующихся функций.If you have a specific ORM in mind, then be explicit about it. Don't hide it behind an interface. It creates the illusion that you can replace one implementation with another. In practice, that's impossible.
Don't hide it behind an interface.речь про IQuariable, за которым прячется конкретная орм,
IQueryable в том, что между разными ORM его поведение отличается (может отличаться). Но если мы знаем, что с той стороны EF, то мы, по факту, имеем дело с одним и тем же IQueryable. До тех пор, пока мы не говорим «нет, у нас тут не EF, тут может быть любой QueryProvider» — мы ничего не прячем.Любое наложенное ограничение упрощает вашу жизнь, как разработчика DALда, а еще тестировщика, заказчика и начальства.
и усложняет жизнь программистада, но меньше вариантов получить по шее, за то что он по ( не знанию, лени и тд ) накосячил, за ним не до проверил тестировщик, из за этого заказчик пожаловался руководству.
Вот и весь компромис.какой там. жесткая диктатура. Но пусть она остается в коде.
да, а еще тестировщика, заказчика и начальства.
да, но меньше вариантов получить по шее, за то что он по ( не знанию, лени и тд ) накосячил, за ним не до проверил тестировщик, из за этого заказчик пожаловался руководству.
1) Запросы становится проще писать с репозитарием, по сравнению с IQueryable? Как то незаметно, скорее наоборот — формировать Query для репозитория гораздо более многословно, чем написать IQueryable запрос.всегда можно использовать запись в виде Query.With(q=>q.Param = value).With…
3) Быстродействия репозиторий не прибавит, а скорее наоборот.любая обертка ведет к потери производительности, это согласен, касательно производительности выполнения запросов, это будет зависеть только от того что написать в конкретной реализации
4) Количество изменений с репозиторием будет больше, так как репозиторий должен знать о прикладной логике приложения, чтобы хоть сколько нибудть быстро работать (нарушается принцип расслоения),это ему знать не нужно, он должен знать о запросе в терминах предметной области и о построителе который умеет перегонять запрос в запрос к хранилищу, тк это фасад и он ничего сам не делает, только делегирует и объединяет. Вот ваше расслоение и единственность ответственности. Что касается количества изменений, да их будет больше, но любая попытка пере использования к этому ведет.
а к IQueryable дописать предикат\пейджинг\сортировку\проекцию можно прямо по месту использования, не влезая в другие слои.да, быстро удобно, но только если у нас в приложении работа с 1 бд и только с ней, но если даже при наличии 1 бд нет ни какой гарантии что прикладной программист напишет адекватный запрос или хуже того начнет дублировать распространяя по коду (почему это плохо наверное понятно).
Эта тема больше недели существуетмне то же уже надоело,
всегда можно использовать запись в виде Query.With(q=>q.Param = value).With…
А кто-то рассказывал про вред свободных запросов,это не свободный запрос, посмотрите внимательно, это всего лишь запись
query.Param = value;
query.Param1 = value1;
что такие конструкции не дублируются по всему коду?
query.Paging.Skip = 10;
query.Paging.Take = 10;
запрос так и остался в терминах предметной области
Query.ActiveUsers
что в терминах хранилища на том же LINQ будетWhere(user=>user.isBanned == false && user.LastLogining < now.AddYear(-1) && user.isDeleted == false)
static очевидно просто подменить во время выполнения…
1. Создание объекта
2. Сохранение
3. Объекты бизнеса могут отличаться от хранимых — это маппинг
всегда можно использовать запись в виде Query.With(q=>q.Param = value).With…
4) Количество изменений с репозиторием будет больше, так как репозиторий должен знать о прикладной логике приложения, чтобы хоть сколько нибудть быстро работать (нарушается принцип расслоения),
это ему знать не нужно, он должен знать о запросе в терминах предметной области и о построителе который умеет перегонять запрос в запрос к хранилищу, тк это фасад и он ничего сам не делает, только делегирует и объединяет.
Что касается количества изменений, да их будет больше, но любая попытка пере использования к этому ведет.Не любая. IQueryable+методы-комбинаторы+generics не приводят к увеличению площади изменений.
нет ни какой гарантии что прикладной программист напишет адекватный запрос
хуже того начнет дублировать распространяя по коду
Тогда у вас получится тот же IQueryable.это не IQuariable а Linq подобный, передавать в реп все равно QueryObject
Не любая. IQueryable+методы-комбинаторы+generics не приводят к увеличению площади изменений.методы-комбинаторы+generics ну да, они бесплатны, и мы не как не можем оградитьмя от дублирования путем не использования всего это го
Репозиторий такую гарантию не дает. тем боле ты сам предлагаешь Linq-подобный интерфейс.QueryObject является ограничителем
Все преимущества исключительно надуманные или требуют высокой дисциплины разработчиков, но при высокой дисциплине и с IQueryable проблем нет.как раз на оборот, из за того что за всеми не уследишь, репозиторий повышает контроль, правда за счет ограниченности
это не IQuariable а Linq подобный, передавать в реп все равно QueryObject
QueryObject является ограничителем
как раз на оборот, из за того что за всеми не уследишь, репозиторий повышает контроль, правда за счет ограниченности
Если вы позволяете передавать Expression Tree в вашем Query, то у вас появляются те же «проблемы», которые есть у IQueryable.
public static T With<T>( T target, Action<T> action )
{
action( target );
return target;
}
Не является, у вас репозитарий отдает IEnumerable. Кто помешает те же Linq методы вызвать для него?ни кто, действительно ни кто, но вот нет запроса GetAll, есть один плюс IEnumerable будет одинаково фигово отрабатывать на больших выборках на всех бд, что сразу всплывет, если же выборка не большая, то этого ни кто не заметит, если такой запрос единичный, то наверное это не страшно ( хотя я не приветствую ). Другой сценарий при использовании paging, это как надо задать сколько элементов нужно пропустить и сколько взять, что бы после фильтрации IEnumerable осталось нужное количество объектов, ну наверное пропустить 0 взять 1000000 ( за такой запрос медаль только за храбрость и дадут ), но это случай с большими выборками ( разобрали ).
вот нет запроса GetAll
это не дерево, а форма записи, простейший вариант
но вот нет запроса GetAll
Это никак не уменьшает объем писанины по сравнению с IQueryable. В совокупности с билдерами даже увеличивает.я не разу ни где не писал, что объем уменьшиться
Во-первых можно передать пустой запрос и в этом случае ни один из билдеров не сработает и будет втянута вся таблица.пустые запросы можно отслеживать
Во-вторых почти всегда есть кейс — показать все записи в UI в виде таблицы или четь в этом родеесли записей не много то да. Вы реально хотите увидеть таблицу в UI из 999999 ( да же не миллион ) записей?
Во-третьих, даже если вы требуете передавать параметры пейджинга, то никто не помешает передать размер страницы равный int.MaxValue.вы вообще читаете то что я вам пишу?
ну наверное пропустить 0 взять 1000000 ( за такой запрос медаль только за храбрость и дадут ),
Сколько я не видел репозиториев — почти все позволяли втянуть всю таблицу, а разработчики этим активно пользовались.а вот это вообще шикарно, в итоге мы получаем неизвестность в плене выполнения запросов, и как результат дубляж запросов по коду, при частых изменениях в бизнесе, модификации объектов — смерть.
Entity Framework или почему я реализую Repository