Я бы хотел обсудить принцип разделения ответственности (Separation of Concerns, SoC) в контексте ORM, а также посмотреть почему этот принцип так важен. Также мы рассмотрим примеры нарушения границ ответственности между доменной логикой и логикой сохранения данных.

Принцип разделения ответственности


В каждом приложении мы имеем дело с несколькими понятиями (concerns). Как минимум три из них как правило четко определены: UI, бизнес логика и база данных. Принцип разделения ответственности тесно связан с принципом единственной обязанности (Single Responsibility Principle, SRP). Вы можете думать о SoC как о SRP примененном не к единственному классу, а к всему приложению. В большистве случаев эти два принципа могут использоваться взаимозаменяемо.

В случае с ORM принцип SoC относится к разделению логики предметной (доменной) области и логики сохранения данных в БД. Мы можем утверждать, что код приложения имеет хорошую степень разделения ответственностей если доменные классы в нем не знают о том, как они сохраняются в базе данных. Конечно, не всегда возможно достичь полного разделения этих двух областей приложения. Иногда требования производительности таковы, что приходится нарушать эти границы. Но в любом случае, всегда стоит стремиться к настолько полному разграничению ответственностей, насколько возможно.

И конечно, мы не можем просто так отделить доменную логику приложения от логики сохранения данных в БД, нам требуется что-то, что соединит их вместе. Именно в этом нам помогают ORM. ORM выступает медиатором между кодом доменной модели и базой данных. В большинстве случаев, ORM способна сделать это таким образом, что ни код предметной области, ни БД не знают о существовании друг друга.

image


Почему SoC важен?


Существует немало информации о том, как поддерживать хорошую степерь разделения ответственностей. Но почему это важно?

image


При сохранении различных ответственностей в едином классе, нам приходится коддерживать их консистентность одновременно с каждой операцией в рамках этого класса. Это очень быстро приводит к комбинаторному взрыву. Более того, сложность приложения нарастает гораздо быстрее, чем думают большинство разработчиков. Каждая дополнительная ответственность увеличивает сложность класса на порядок.

Чтобы справиться со всей этой сложностью, нам необходимо разделить эти ответственности:

image


SoC — это не просто вопрос хорошего или красивого кода. Принцип SoC жизненно важен для поддержания приемлемой скорости разработки. Более того, он важен для успеха вашего проекта.

Человек может удерживать в кратковременной памяти не более девяти объектов одновременно. Приложение без четкого разделения ответственностей очень быстро переполняет кратковременную память разработчика из-за огромного количества комбинаций, в которых различные неразделенные понятия могут взаимодействовать друг с другом.

Разделение этих понятий на высоко связанные части позволяет вам «разделить и властвовать» над разрабатываемым вами приложением. Гораздо проще управлять сложностью небольшого, изолированного компонента, который слабо связан с остальными компонентами приложения.

Когда логика сохранения данных в БД проникает в доменную логику


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

Пример 1: Работа с персистентным статусом объекта в классе доменной модели.

public void DoWork(Customer customer, MyContext context)
{
    if (context.Entry(customer).State == EntityState.Modified)
    {
        // Do something
    }
}

Текущее персистентное состояние объекта (т.е. существует ли он уже в БД или нет) не имеет никакого отношения к логике доменной модели. В идеале, доменные объекты должны оперировать только теми данными, которые напрямую относятся к бизнес-логике приложения.

Пример 2: Работа с идентификаторами

public void DoWork(Customer customer1, Customer customer2)
{
    if (customer1.Id > 0)
    {
        // Do something
    }
    if (customer1.Id == customer2.Id)
    {
        // Do something
    }
}

Работа с идентификаторами в классах предметной области — пожалуй, наиболее распространенный тип смешения разных видов ответственностей приложения. Идентификаторы — деталь имплементации того, как ваши объекты сохраняются в БД. Как правило они используются для сравнения объектов между собой. Если вы также используете их для этой цели, гораздо лучшим решением будет переопределить операторы сравнения (equality members) в базовом классе доменного объекта и писать ‘customer1 == customer2? вместо ‘customer1.Id == customer2.Id’.

Пример 3: Разделение свойств доменного класса по персистентному признаку

public class Customer
{
    public int Number { get; set; }
    public string Name { get; set; }
 
    // Не сохраняется в БД, можем хранить здесь все что угодно
    public string Message { get; set; }
}

Если вы имеете тенденцию писать такой код, то вам следует остановиться и обдумать доменную модель. Подобный подход может говорить о том, что вы включили в доменную модель элементы, которые не имеют к ней отношения.

Когда доменная логика проникает в базу данных


Пример 1: Каскадное удаление

Настройка БД для каскадного удаления — один из примеров проникновения логики предметной области в логику сохранения данных. В идеале, БД сама по себе не должна содержать информации о том, когда должно срабатывать удаление данных. Подобное знание — это забота домена. Ваш C#/Java/etc код должен быть единственным местом для хранения подобной логики.

Пример 2: Хранимые процедуры

Использование хранимых процедур, которые изменяют данные в БД — еще один пример. Не позволяйте доменной логике проникать в базу данных, храните код, изменяющий состояние данных, в вашей доменной модели.

Тут необходимо сделать два замечания. Во-первых, в большинстве случаев, нет ничего плохого в том, чтобы иметь хранимые процедуры, которые не изменяют данные в БД (read-only stored procedures). Помещение кода, приводящего к побочным эффектам (side effects), в доменную модель и кода без побочных эффектов в хранимые процедуры прекрасно соотносится с принципом CQRS.

Во-вторых, существуют случаи, когда избежать использования SQL не получится. К примеру, если вам необходимо удалить группу объектов по какому-то признаку, SQL оператор DELETE сделает эту работу намного быстрее. В таких случаях, использование SQL, изменяющего данные в БД, оправданно, но необходимо держать все подобные исключения под строгим контролем и не давать им разрастаться.

Пример 3: Значения по умолчанию

Значения по умолчанию в таблицах БД — другой пример доменной логики, проникнувшей в базу данных. Значения свойств, которые доменная сущность имеет по умолчанию, должны быть определены в коде, а не отданы на откуп БД.

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

Заключение


Большинство подобных «протечек» возникает из-за того, что люди думают о своем приложении не в терминах предметной области, а в терминах данных. Многие разработчики рассматривают разрабатываемую ими систему именно так. Для них, классы — это всего лишь хранилище для данных, которые они переносят от БД к UI, а ORM — всего лишь утилита, помогающая не копировать вручную данные из результатов выполнения SQL запросов в эти объекты. Очень часто бывает сложно сделать сдвиг в парадигме мышления. Но после того как он сделан, люди как правило открывают целый мир выразительных доменных моделей, которые позволяют разрабатывать приложения намного быстрее, особенно на больших проектах.

Конечно, не всегда возможно достичь желаемого уровня разделения ответственностей в коде приложения. Но в большинстве случаев ничто не мешает сделать это. Большинство проектов проваливаются не из-за того, что они оказываются не способны выполнить какое-либо из технических требований. Большинство терпят неудачу из-за того, что оказываются погребены под массой беспорядочного кода, который мешает разработчикам менять что-либо в нем. Каждое изменение в подобном коде приводит к каскаду багов и неожиданных побочных эффектов по всему приложению.

SoC — принцип, который позволяет избежать подобного исхода.

Ссылка на оригинал статьи: Separation of Concerns in ORM

Комментарии (99)


  1. Mithgol
    20.07.2015 08:06
    +5

    Чёткого отделения логики от базы не получится в том числе и потому, что примеров проникновения логики в базу (с целью ускорения работы логики) обыкновенно бывает больше, чем три приведённых выше примера. Как только число строк в таблице начинает измеряться тысячами и десятками тысяч, так сразу захочется прибавить к этим примерам ещё одну логику внутри базы, а именно индексы. А сможет ли ORM, вообще ничего не зная о той логике, которой подчинён поиск по базе, создать именно те индексы, которые ускорят такой поиск (и одновременно не насоздавать лишних индексов, которые не принесут пользы, а только замедлят пополнение и обновление базы)? Никоим образом не сможет. Это самообман.


    1. ApeCoder
      20.07.2015 11:13

      Мне кажется, что есть что-то в предметной области, что ORM может отмепить на индексы. Например, сделать какие-нибудь «хинты» типа «Типичный размер этой коллекции порядка десятков тысяч» и «нас интересуют быстрые выборки по вот этому сочетанию атрибутов». Теоретически ORM может при меппинге сгенерировать соответствующие индексы.


      1. lair
        20.07.2015 11:35
        +1

        Вам кажется. Типичный для предметной области запрос «нас интересуют быстрые выборки по любому сочетанию атрибутов». Если вы построите такой индекс, то система захлебнется при записи.


        1. ApeCoder
          20.07.2015 11:41

          Тогда невозможно построить индекс в принципе, не важно на каком уровне его декларировать — БД или приложения


          1. lair
            20.07.2015 11:42
            +1

            Возможно, просто решение об этом принимается исходя не только из знаний о предметной области, но и из постоянного наблюдения за БД и готовности чем-то жертвовать (и это не считая всяких денормализационных подходов). Только предметная область при этом не меняется.


            1. ApeCoder
              20.07.2015 11:50

              Совершенно согласен. Любая реализация объектной модели несет в себе следы ограничений ее интепретатора. Будь то JIT или SQL. Соответственно мы должны как-то сообщить интерпретатору что данном случае мы от него ожидаем. Например что запросы по определенным атрибутам нам представляются наиболее важными (а это уже требование из предметной области, как мне представляется)


              1. lair
                20.07.2015 11:52

                Любая реализация объектной модели несет в себе следы ограничений ее интепретатора. Будь то JIT или SQL.

                Это (в идеально-общем случае) нарушает принцип persistence ignorance.


                1. ApeCoder
                  20.07.2015 11:55

                  Я думаю, что этот принцип — недостижимый идеал, к которому, тем не менее, надо стремиться.


                  1. lair
                    20.07.2015 11:59

                    Вот поэтому и не надо вносить в объектную модель хинты для персистентного хранилища.


                    1. ApeCoder
                      20.07.2015 12:04

                      Это не хинт «создай индекс» это описание свойств «это сочетание атрибутов для меня важно в смысле поиска»


                      1. lair
                        20.07.2015 12:09

                        Я уже говорил, что в домене для поиска могут быть важны все атрибуты (и более того, регулярно так и бывает). А уж для покрывающего индекса у вас тем более информации не хватит.

                        У вас же «поиск» происходит не только и не столько из интерфейса, есть еще сценарии изнутри самого когда, которые с одной стороны стабильны, а с другой — меняются при каждом рефакторинге, есть структура агрегатов, если у вас DDD, которая тоже влияет на порядок запросов, есть много других странных вещей, которые являются только и исключительно деталью реализации, к домену отношения не имеет, но на хранилище влияет.


                        1. ApeCoder
                          20.07.2015 13:26

                          Вероятно надо несколько опуститься: нет никакой предметной области, есть объектная модель. Вопрос в том, может ли ORM получив некоторую дополнительную информацию об объектной модели в терминах этой модели создать индексы или мы обречены командовать ORM в терминах хранилища. А так же можно ли использовать ту же самую информацию в других местах: например при генерации UI опускать создание контролов для фильтрации если у нас мало элементов в коллекции и, наоборот, предлагать фильтрацию по умолчанию по важным признакам.


                          1. lair
                            20.07.2015 13:29

                            Эээ, как это «нет никакой предметной области»? Вы вот так решили взять и похоронить Фаулера и Эванса? Нехорошо.

                            Вопрос в том, может ли ORM получив некоторую дополнительную информацию об объектной модели в терминах этой модели создать индексы

                            Вообще-то, это давно есть, и называется «метаданные». Только это информация не в терминах модели, потому что она всегда будет специфична для хранилища.

                            А так же можно ли использовать ту же самую информацию в других местах: например при генерации UI опускать создание контролов для фильтрации если у нас мало элементов в коллекции и, наоборот, предлагать фильтрацию по умолчанию по важным признакам.

                            И получить непредсказуемое (пользователем) поведение UI? Я заодно хочу напомнить, что чем сложнее UI, тем вероятнее, что он отделен от доменной модели слоем презентационной модели.


                            1. ApeCoder
                              20.07.2015 14:21

                              1. Я имею ввиду в рассматриваемой задаче нет. Есть объектная модель — не важно как получена, надо построить реляционную базу по ней.

                              2. Есть ли правила по которым проектировщик строит базу в зависимости от объектной модели и нельзя ли закодировать эти правила?

                              3. В каждом сложном UI есть простые куски.


                              1. lair
                                20.07.2015 14:25

                                Есть объектная модель — не важно как получена, надо построить реляционную базу по ней.

                                А откуда взялась такая задача? Вы выдернули кусок из контекста, но вы уверены, что ваш контекст изначально верен?

                                Есть ли правила по которым проектировщик строит базу в зависимости от объектной модели и нельзя ли закодировать эти правила?

                                Те правила, которые можно закодировать, во некоторых ORM (например, в EF) уже закодированы.

                                В каждом сложном UI есть простые куски.

                                Опыт показывает, что с вероятностью 0.9 это будут не те куски, про которые вы (или я) думаете.


                                1. ApeCoder
                                  20.07.2015 14:33

                                  1) Я предлагаю рассмотреть такую задачу. Так как у нас ORM а не что-то еще.
                                  2) Что там с построением индексов?
                                  3) Мне опыт подсказывает другое, но возможно у нас с вами разный опыт.


                                  1. lair
                                    20.07.2015 14:36

                                    Я предлагаю рассмотреть такую задачу. Так как у нас ORM а не что-то еще.

                                    У ORM нет задачи построить реляционную базу по объектой модели, у него есть задача преобразовать реляционные данные в объекты и обратно. И да, на основании данных, которые есть у ORM, действительно можно много чего интересного настроить, только это не доменная модель, а модель хранилища (а это важное различие).

                                    Что там с построением индексов?

                                    Индексы там по умолчанию тоже строятся — по первичному и внешним ключам.


                                    1. ApeCoder
                                      20.07.2015 14:58

                                      Мне бы хотелось максимально абстрагироваться от хранилища. Идеально строить объектную модель предметной области (хоть и с небольшой оглядкой на модель хранения) а затем чтобы ORM брало на себя весь меппинг, как структурный так и поведенческий.


                                      1. lair
                                        20.07.2015 15:00

                                        Мне бы хотелось максимально абстрагироваться от хранилища. Идеально строить объектную модель предметной области (хоть и с небольшой оглядкой на модель хранения)

                                        Эти два предложения друг другу противоречат.

                                        ORM брало на себя весь меппинг, как структурный так и поведенческий.

                                        Это святой Грааль, такой же недостижимый, как и прообраз. Вы не можете избавиться от impedance mismatch.


                                        1. ApeCoder
                                          20.07.2015 15:04

                                          1) Почему они противоречат?
                                          2) Дык я ж говорил что идеал недостижим — хотелось бы к нему приблизиться максимально.


                                          1. lair
                                            20.07.2015 15:20

                                            Почему они противоречат?

                                            Потому что абстрагирование движет вас в одну сторону, а оглядка на модель хранения — в другую.

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

                                            Неплохо иногда задумываться, правда ли вам нужна чашка двухтысячелетней давности, или же подойдет термос.


                                            1. ApeCoder
                                              20.07.2015 15:26

                                              1) Я об этом и говорю — и так всегда было — стремимся к идеалу, но учитываем ограничения
                                              2) Пока выбираю термос, но интересно, можно ли его сделать тонким и легким как чашка


                                              1. lair
                                                20.07.2015 15:31

                                                Я об этом и говорю — и так всегда было — стремимся к идеалу, но учитываем ограничения

                                                Так может просто идеал не тот?

                                                Пока выбираю термос, но интересно, можно ли его сделать тонким и легким как чашка

                                                Уже сделали, вот только теперь это не та чашка, в которую в соответствующем году собрали некую кровь.


                                                1. ApeCoder
                                                  20.07.2015 15:45

                                                  1) Это просто один из вариантов использования слова «идеал» — не достижимая цель, к которой можно только приблизиться. Виртуальная точка, задающая направление.

                                                  2) Да ну, мне кажется мы весьма далеки от идеальной ORM


                                                  1. lair
                                                    20.07.2015 15:47

                                                    Это просто один из вариантов использования слова «идеал» — не достижимая цель, к которой можно только приблизиться. Виртуальная точка, задающая направление.

                                                    Вот я вам и говорю: может, у вас с направлением что-то не то?


                                                    1. ApeCoder
                                                      20.07.2015 15:50

                                                      Может и не то, давайте обсудим. Почему именно не то? Желание работать на как можно более высоком уровне — неправильно?


                                                      1. lair
                                                        20.07.2015 15:51

                                                        Когда вы уходите на более высокий уровень, вы теряете контроль за уровнями ниже. В этом смысл абстракции.

                                                        Вас это устраивает?


                                                        1. ApeCoder
                                                          20.07.2015 15:57

                                                          Да. Но я хочу уходить не сразу везде, а, допустим на 95% в тех 5% где мне надо, я хочу спускаться и ниже. Хочу GC, и я хочу одновременно GC.Collect, я хочу абстрагироваться от машинного кода, но я хочу одновременно и смотреть результат JIT там, где узкое место.

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


                                                          1. lair
                                                            20.07.2015 16:02

                                                            В этот момент вы получаете текущую абстракцию — что само по себе плохой идеал.


                                                            1. ApeCoder
                                                              20.07.2015 16:08

                                                              Дык понятно, что идеал это нетекучая абстракция. Но все реальные абстракции текут. И всегда будут течь хоть чуть-чуть.


                                                              1. ApeCoder
                                                                20.07.2015 16:13

                                                                А раз она течет, я хочу чтобы подстановка тазиков на уровне ниже была рассмотрена заранее и была штатной процедурой. А так же иметь карту дырок.


                                                              1. lair
                                                                20.07.2015 16:16

                                                                Так вы определитесь, ваш идеал — это нетекучая абстракция, в которой вы не имеете контроля за нижним уровнем, или абстракция, в которую вы в любой момент можете воткнуть отвертку?


                                                                1. ApeCoder
                                                                  20.07.2015 16:43

                                                                  Оба два. В условиях реальных ограничений, в каждый конкретный момент времени оно должно позволять втыкать отвертку там где абстракции текут. Но при при устремлений времени к бесконечности в условиях стремления ограничений к нулю эта область куда можно воткнуть отвертку должна сужаться.


                                                                  1. lair
                                                                    20.07.2015 16:46

                                                                    Не может быть «оба два». Только что-то одно. Либо вы признаете, что идеальный ORM не дает вам контроля за персистентностью через объектную модель, которую он мапит, либо нет.


                                                                    1. ApeCoder
                                                                      20.07.2015 20:25

                                                                      Давайте соединим в одно. Он дает контроль за перзистентностью, но не требует его нигде.


                                                                      1. lair
                                                                        20.07.2015 20:37

                                                                        Если он дает контроль, значит, он не абстрактный, потому что для каждого хранилища элементы контроля свои.


                                                                        1. ApeCoder
                                                                          20.07.2015 20:39

                                                                          Не свои, а они могут быть свои.


                                                                          1. lair
                                                                            20.07.2015 20:41

                                                                            Они совершенно точно свои. Вы не можете построить единую систему контроля для оракла и ms sql, потому что у них даже типы данных отличаются.


                                                                            1. ApeCoder
                                                                              20.07.2015 20:47

                                                                              На C# нельзя писать одновременно под Linux и Windows потому, что можно вызывать windows dll


                                                                              1. lair
                                                                                20.07.2015 20:49

                                                                                Скажем так, вы не можете использовать всю функциональность .net на linux и windows одинаково. С идеальным ORM будет та же проблема.


                                                                                1. ApeCoder
                                                                                  20.07.2015 20:57

                                                                                  Да и меня это устраивает. Только там где мне важно быстродействие я буду доставать отвертку.


                                                                                  1. lair
                                                                                    20.07.2015 20:59

                                                                                    И нарушать абстракцию. О чем и речь.


                                                                                    1. ApeCoder
                                                                                      20.07.2015 21:06

                                                                                      Да, но в идеале я буду делать это в минимуме случаев — то есть в нуле


                                                                                      1. lair
                                                                                        20.07.2015 21:43

                                                                                        До тех пор, пока у вас есть такая возможность, абстракция протекает.


                              1. Flammar
                                20.07.2015 15:25

                                (1) Такая возможность по крайней мере у Hibernate была ещё по крайней мере в 2009. Уже по крайней мере тогда была возможность строить объектную модель как со стороны БД, так и со стороны Java-классов.


                            1. PsyHaSTe
                              20.07.2015 18:42

                              Насколько я могу судить, человек просто говорит о линейной свертке по индексам. То есть у нас есть переменные-индексы и константы-статистики производительности запросов по этим индексам. Мы задаем целевую функцию как обычный скаляр. Используя эти данные и обычную динамическую оптимизацию мы можем перестраивать индексы под нужные требования, стремясь к оптимуму. Фактически то же делаем и мы, просто руками, а не математикой. Это как отладка против трассировки. Можно и последним заниматься, но зачем, когда есть такой удобный quick watch?..


                              1. lair
                                20.07.2015 18:48

                                Вопрос в том, (а) что считать целевым значением и (б) как тестировать эффективность примененных изменений.


                                1. PsyHaSTe
                                  20.07.2015 19:23

                                  Ну как обычно, этим занимается системный аналитик, достаточного уровня, чтобы понимать слова «свертка», «оптимум», «Паретто-оптимальность» и т.п. плюс набор экспертов для анкетирования. Эксперты берутся из предметной области, кому собственно софт пишем, а аналитик-математик должен быть в штате.


                                  1. lair
                                    20.07.2015 20:40

                                    Я это, сварщик ненастоящий, консерваториев не заканчивал, все умные слова знаю из википедии, так что можно мне на примерах?

                                    Вот есть маленькая такая системка документооборота, с одним видом документов, документ приходит, регистрируется, назначается исполнителю, переназначается дальше, обрабатывается последним. Что мы возьмем за параметры для оптимизации и как мы будем расчитывать эффект от оптимизации, принесенный индексами в БД?


                                    1. PsyHaSTe
                                      20.07.2015 21:46

                                      Ну простейшим вариантом является нейросеть, то есть изначально мы индексы ставим у случайных полей и смотрим, быстро ли выполнился запрос. Если быстро, то вес соответствующих индексов увеличиваем, если нет — уменьшаем. Через несколько миллионов итераций (например, поставили на один день оптимизацию) у нас будет достаточно хорошее распределение индексов, которое можно будет использовать для построения индексов. Так как у нас все индексы и выходная величина — время выполнения — это числовые характеристики, то это можно без человека автоматически делать.

                                      Это что касается вероятностных подходов. Для аналитики нужны спецы предметной области (бухгалтеры всякие, хз, уж Я-то точно не спец в документообороте) для определения, что у нас критично, а что нет, и аналитик, который это формализует в итоговую формулу: бд тут вообще не причем, О-нотация наше всё.


                                      1. lair
                                        20.07.2015 21:52

                                        изначально мы индексы ставим у случайных полей и смотрим, быстро ли выполнился запрос.

                                        Что значит «быстро ли выполнился запрос»? Какой запрос? В каких условиях?

                                        Для аналитики нужны спецы предметной области (бухгалтеры всякие, хз, уж Я-то точно не спец в документообороте) для определения, что у нас критично, а что нет, и аналитик, который это формализует в итоговую формулу: бд тут вообще не причем, О-нотация наше всё.

                                        Я вполне готов сыграть вам роль такого специалиста предметной области.


                                        1. PsyHaSTe
                                          21.07.2015 03:31

                                          Что значит «быстро ли выполнился запрос»? Какой запрос? В каких условиях?

                                          Время выполнения в миллисекундах. Быстро или нет — вам решать. Можно шкалирование ввести, это задача уже определения нечетких множеств на лексические переменные. В конечном счете всё сводится к цифрам и автоматическому пересчету. Что касается условий, то они не важны, т.к. с увеличением количества экспериментов у нас сходится по вероятности весовой коэффициент индекса на конкретном поле конкретной таблицы.


                                          1. lair
                                            21.07.2015 11:15

                                            Самый важный вопрос — это какой запрос вы будете замерять. Ну и отдельно хочется заметить, что условия очень важны, потому что оптимизация ста записей и ста миллионов записей — это разные вещи, равно как и оптимизация под десять пользователей и под десять тысяч пользователей.


                                            1. PsyHaSTe
                                              21.07.2015 11:49

                                              Ну очевидно, что выборка должна быть репрезентативной. Запросы тоже сами выбираете, какие оптимизировать и в каком количестве.

                                              Короче, весьма реально даже на текущем уровне вычислительной техники.


                                              1. lair
                                                21.07.2015 11:53

                                                Понимаете ли, в чем проблема… самое сложное — это выбрать, какие запросы оптимизировать (т.е., какие, собственно, показатели мы измеряем), и как их балансировать друг между другом (особенно в конкурентных сценариях), т.е. чем ради чего мы готовы жертвовать (например, запаздыванием актуальности (stale data) против времени ответа (latency)). После того, как это выбрано, человек сделает быстрее, чем нейронная сеть, просто потому, что стоимость эксперимента все-таки высока.


    1. FractalizeR
      20.07.2015 11:20
      +2

      Разве индексы БД являются частью предметной области / логики приложения?


      1. Mithgol
        20.07.2015 11:46

        Во всяком случае, без знания логики приложения трудно создавать нужные индексы (которые ускоряют исполнение важных запросов) и воздерживаться от создания ненужных индексов (которые не ускоряют исполнение никаких достаточно важных запросов, а только замедляют собою пополнение и обновление базы) — следовательно, трудно возложить создание индексов на ORM, если мы понимаем ORM как прокладку между базою и приложением, ничего не знающую о логике приложения.

        Если ORM — это нечто существенно более сложное, чем простая прокладка… например, если ORM собирает статистику о частоте поступления запросов к базе из приложения и о времени исполнения запросов базою, а затем использует эту статистику для самостоятельного принятия решений о создании индексов в базе… тогда другое дело. Но и тогда знание логики приложения подменяется внешним анализом поведения, эмпирически-концептуальным по своей сути. Мне это не представляется изящным.


        1. FractalizeR
          20.07.2015 12:21

          Вы не могли бы привести пример хотя бы одного ORM на любом языке, который автоматически управляет индексами? Я пока не представляю себе, как это возможно реализовать.


          1. Mithgol
            20.07.2015 13:32
            +1

            Я также не представляю; я рассуждал теоретически.


          1. serbod
            28.07.2015 23:16

            1С-Предприятие. Оно не только индексы, но и хранимые процедуры создает для некоторых операций. Включается галочкой «отбор и сортировка» для каждого поля.


            1. Aclz
              29.07.2015 01:08
              +1

              Разве что индексы оно создаёт не автоматически, как рассуждалось выше, а путём параметрической настройки, производимой пользователем.


              1. serbod
                29.07.2015 01:22

                Галочка — это очевидный пользователю способ включения индекса для ускорения отбора и сортировки по конкретному полю. А есть еще неявно создаваемые индексы и процедуры, которые зависят от совокупности настроек. Например, в настройках свойств регистров нет ничего про индексы и процедуры, они создаются автоматически, в зависимости от состава измерений, взаимосвязей, периодичности, итд…


                1. Aclz
                  29.07.2015 01:31
                  +1

                  Речь немного не об этом. Этот алгоритм создания дефолтных индексов для каждого типа метаданных детерминирован, прост как три рубля, жестко зафиксирован и не всегда оптимален. Он никак не учитывает характер данных и будущие потребности по их выборке.


                  1. serbod
                    29.07.2015 01:39
                    -2

                    Но он есть, он работает и он эффективен в 99.9% случаев. А в оставшихся 0.1% случаев не всякий DBA сделает лучше, ибо проблема скорее всего на уровне дизайна метаданных.


                    1. konsoletyper
                      29.07.2015 06:42
                      +1

                      А в оставшихся 0.1% случаев не всякий DBA сделает лучше

                      Вот не согласен. Видел я, какие порнографические запросы этот ваш 1С генерит. Да что там генерит, разбирал я, например, такие вещи в ЗУП, как генерация табеля. 1200 строк запроса с несколькими временными таблицами и 4-мя уровнями вложенности подзапросов. Разобраться в этом нереально. Разобраться в развесистом плане запроса — тем более. Вручную написанный код, с вручную созданными индексами и схемой БД, и несколькими запросами, разнесёнными в методы, и денормализацией где надо, справляется с этой задачей за 2 секунды.

                      В общем, 1С — это рельсы. Пока надо ехать прямо, справляется отлично. Как только надо чуть-чуть сменить направление — всё рушится. Отличная штука для решения узкого круга задач хозяйственного учёта, но как general purpose — увольте.


                    1. FractalizeR
                      29.07.2015 10:27

                      Он есть, но он на 100% зависит от действий пользователя, а не от типа и формата ваших данных. Какова разница между выполнением CREATE INDEX в Postgres и установкой галочки в 1С?


                1. konsoletyper
                  29.07.2015 06:34

                  Галочка — это очевидный пользователю способ включения индекса для ускорения отбора и сортировки по конкретному полю

                  А по совокупности полей?
                  Например, в настройках свойств регистров нет ничего про индексы и процедуры, они создаются автоматически, в зависимости от состава измерений,

                  А на реквизиты индексов? Проблема в том, что и тут пользователь должен явно сказать, что является измерением, а что — реквизитом. Т.е. неявно описать индекс. А что если в некоторых запросах такой индекс оказывается недостаточно селективным?
                  взаимосвязей

                  Так ведь автоматическое создание индексов для всех foreign key — это совсем тривиальная задача, некоторые СУБД даже настройку такую имеют. Или некоторые ORM, такие как Hibernate. Проблема в том, что в подавляющем числе случаев такие индексы просто не нужны, поэтому я никогда не включаю создание схемы в Hibernate и пишу DDL руками.

                  Вот что я видел реально крутое на эту тему, так это в MSSQL профайлер, который может по логу запросов предлагать строить индексы. Очень хорошо предлагает, надо сказать. Опять же, прошу обратить внимание на уровень, на котором работает данная штуковина, т.е. уровень хранения данных, а не ORM.

                  Кстати, то, что в 1С, я бы даже ORM не назвал. Формально это реализация паттерна Active Record, см. Фаулера.


        1. konsoletyper
          20.07.2015 12:37
          +1

          Задача ORM — не освободить разрабочика от любой работы с хранилищем данных (в частности, СУБД), а разделить логику хранения и бизнес-логику. Я обычно пишу просто entity в виде POJO, пишу для них юнит-тесты, которые вообще ничего не знают о БД. Когда логика написана и протестирована, можно создавать хранилище для неё. При этом используется несколько различных инструментов: для миграций (куда, в частности, включается создание индексов в случае реляционных СУБД), для маппинга на объекты и т.д. В случае корректного написания маппинга и миграционных скриптов такая модель будет себя прекрасно чувствовать в БД, и все бизнес-операции будут так же работать на persistent entities. ORM не может быть слишком умной, ибо тогда она будет нарушать принцип SRP, да и потому, что пока машины не научились писать код и самостоятельно принимать решения по части архитектуры.

          Кстати, если в проекте используется ORM, это вовсе не значит, что ORM должен быть единственным способом для доступа к БД. ORM хороши, когда у нас для обработки внешних воздействий (согласование заказа, приёмка поступления) используется какая-то сложная логика, которую хорошо выражать в терминах сущностей, меняющих своё состояние. Наоборот, отчёты ничего не модифицируют, зато для них нужны сложные критерии выборки и хардкорно оптимизированные запросы, поэтому для них я использую raw SQL.


          1. lair
            20.07.2015 13:21

            ORM хороши, когда у нас для обработки внешних воздействий (согласование заказа, приёмка поступления) используется какая-то сложная логика, которую хорошо выражать в терминах сущностей, меняющих своё состояние. Наоборот, отчёты ничего не модифицируют, зато для них нужны сложные критерии выборки и хардкорно оптимизированные запросы, поэтому для них я использую raw SQL.

            Здравствуй, CQRS.


            1. FractalizeR
              20.07.2015 14:42

              Вы не могли бы пояснить? При использовании CQRS мы имеем две различные модели данных. Одна для чтения, другая для записи. Это вовсе не то же самое, что просто иметь персистентные сущности + SQL код для построения отчетов.


              1. lair
                20.07.2015 14:47

                При использовании CQRS мы имеем два потока данных — один на запись, другой на чтение. Когда у вас какая-то часть системы, отделенная по принципу «здесь мы только читаем», идет по альтернативному потоку — вы уже используете CQRS.


                1. ApeCoder
                  20.07.2015 14:59

                  А как в рамках CQRS обеспечивается непротиворечивость представления двух потоков о хранимых данных? И недублирование кода?


                  1. lair
                    20.07.2015 15:02

                    А как в рамках CQRS обеспечивается непротиворечивость представления двух потоков о хранимых данных?

                    А откуда возьмется противоречие? Один поток соответствует бизнес-требованиям на запись, другой поток соответствует бизнес-требованиям на чтение. Если они реализованы верно, то единственный источник противоречия — бизнес-треования.

                    И недублирование кода?

                    Если вкратце, то никак. Эта проблема особо никого не волнует — если в чтении/записи единая структура, то (а) можно использовать одни и те же сущности и (б) не факт, что CQRS вообще нужен. Если же структура разная, то откуда дублирование?


                    1. ApeCoder
                      20.07.2015 15:08

                      1) Если например есть общие сущности какие-нибудь. Например что у сотрудника есть такие то поля + соблюдается соотношение, что возраст на дату X = X — Дата рождения
                      2) А они всегда структурно 100% различные например при записи у нас есть Имя подразделения, в при чтении Цвет мячика?


                      1. lair
                        20.07.2015 15:19

                        соблюдается соотношение, что возраст на дату X = X — Дата рождения

                        А зачем вам это соотношение в потоке записи? Оно, скорее всего, нужно только при чтении.

                        А они всегда структурно 100% различные например при записи у нас есть Имя подразделения, в при чтении Цвет мячика?

                        Именно структурно — очень часто. Например, при записи у вас есть идентификатор подразделения, в котором работает сотрудник, а при чтении — его наименование (или наименование + идентификатор).


                        1. ApeCoder
                          21.07.2015 08:43

                          1) Я наверное, как-то неправильно понимаю CQRS — вот, допустим, у нас есть приложение которое показывает формочку для ввода даты рождения и показывает возраст в процессе ввода. Отдельно у нас есть некая штука которая показывает статистику по работникам в разрезе возраста. Я так понимаю что в рамках CQRS эти две штуки должны быть разделены, но в то же время хочется контроля целостности концепций между потоками.

                          2) А при записи наименование подразделения есть?


                          1. lair
                            21.07.2015 11:19

                            вот, допустим, у нас есть приложение которое показывает формочку для ввода даты рождения и показывает возраст в процессе ввода. Отдельно у нас есть некая штука которая показывает статистику по работникам в разрезе возраста. Я так понимаю что в рамках CQRS эти две штуки должны быть разделены, но в то же время хочется контроля целостности концепций между потоками.

                            Да тут, по большому счету, и без CQRS интересно. «статистика по работникам в разрезе возраста», скорее всего, должна считаться на SQL-сервере (потому что иначе это будет очень медленно). Вы, естественно, не можете использовать тот же механизм при вводе (мы не рассматриваем вариант «сделать sql-функцию и дергать ее с формы для расчета), поэтому так или иначе вам придется либо отказаться от показа возраста в процессе ввода (а точно надо?), либо сдублировать код. Если вы очень изобретательны, вы можете объявить расчет возраста бизнес-правилом и написать для него трансляторы в UI-код и SQL-код. Добавление CQRS в этом конкретном случае ни на что не влияет.

                            А при записи наименование подразделения есть?

                            Нет, зачем? Представьте себе типовой интерфейс — подразделение выбирается из списка, название показывает контрол, но в модели оно отсутствует.


                1. FractalizeR
                  21.07.2015 10:10

                  Фаулер описывает CQRS иначе.

                  The change that CQRS introduces is to split that conceptual model into separate models for update and display, which it refers to as Command and Query respectively following the vocabulary of CommandQuerySeparation.


                  Термин «поток данных» в рамках объектно-ориентированного проектирования вообще представляется мне туманным. Может быть, я вас неправильно понял?


                  1. lair
                    21.07.2015 11:26

                    Фаулер описывает CQRS иначе.

                    When in doubt, go to the source.

                    Но если серьезно, то Фаулер пишет в настолько общих словах, что они применимы к чему угодно. Обратите внимание на «most commonly» во фразе «By separate models we most commonly mean different object models» — это означает, что отличие может быть не только в объектных моделях. Янг в статье по ссылке выше показывает, что отличие может быть в сервисных интерфейсах, я от себя добавлю, что можно иметь UpdateCommand<Customer>() и GetObjectQuery<Customer>, и это все еще ничему не будет противоречить.

                    Да, в большом и красивом CQRS у вас две явно различные модели, блаблабла. Но всегда ли у вас настолько большое приложение?

                    Термин «поток данных» в рамках объектно-ориентированного проектирования вообще представляется мне туманным. Может быть, я вас неправильно понял?

                    Представьте себе «типовое сложное» веб-приложение. Пользователь ввел данные на форме — они преобразовались в какую-то модель (input model) — обработаны контроллером/прикладным сервисом — преобразованы в доменную модель — обработаны агрегатом/доменным сервисом — преобразованы в модель хранения — засунуты в БД. Вот он, ваш поток данных на запись. Все то же самое, но в обратную сторону (и с view model в конце) — поток данных на чтение.


                    1. FractalizeR
                      22.07.2015 10:40

                      Я не думаю, что стоит называть CQRS любую архитектуру, в которой есть чтение и запись данных в разных объектах. Фундаментальное отличие CQRS в том, что для чтения и для записи используются различные модели вплоть до уровня базы данных (разные таблицы).

                      Если у вас есть таблица БД Users, модель данных Users и два репозитария ReadUsers и WriteUsers (тупой пример, согласен, но для иллюстрации, думаю, пойдет), то это не CQRS несмотря на то, что чтение и запись данных производятся разными объектами потому, что нет серьезного разделения между чтением и записью данных. Разнесение методов чтения и записи в разные классы на архитектуру в данном случае существенно не влияет.

                      А вот если в архитектуре есть денормализованная таблица UserData, содержащая все денормализованные данные пользователей (Query table), и серия таблиц, содержащих все данные, требуемые для построения этой таблицы на любой момент времени (Command Tables) — такого рода архитектуру (вместе с объектами, которые ее поддерживают, конечно) я бы назвал CQRS.

                      Насколько я понимаю, Янг говорит о том же:

                      This separation however enables us to do many interesting things architecturally, the largest is that it forces a break of the mental retardation that because the two use the same data they should also use the same data model.


                      1. lair
                        22.07.2015 11:52
                        +1

                        Я не думаю, что стоит называть CQRS любую архитектуру, в которой есть чтение и запись данных в разных объектах. Фундаментальное отличие CQRS в том, что для чтения и для записи используются различные модели вплоть до уровня базы данных (разные таблицы).

                        Ну написано же:

                        Applying CQRS on this would result in two services

                        CustomerWriteService
                        [...]
                        CustomerReadService
                        [...]

                        That is it. That is the entirety of the CQRS pattern. There is nothing more to it than that…


                        И я прекрасно понимаю, почему Янг так пишет — потому если ваша система потребляет это API, то сегодня оно может быть построено вокруг единого хранилища, завтра — вокруг системы с раздельной оптимизацией, и вы этого никак не заметите. А если вы еще и изначально заложитесь на асинхронность внесения изменений (т.е., что возврат управления из метода записи не означает, что вы уже видите эти данные в методе чтения), то вы можете делать дальше что угодно, включая очереди, шины сообщений и любые красивые слова.

                        А вот если в архитектуре есть денормализованная таблица UserData, содержащая все денормализованные данные пользователей (Query table), и серия таблиц, содержащих все данные, требуемые для построения этой таблицы на любой момент времени (Command Tables) — такого рода архитектуру (вместе с объектами, которые ее поддерживают, конечно) я бы назвал CQRS.

                        Во-первых, нет такой вещи, как Command table, команды — они в прикладном слое, в хранилище команд нет. А во-вторых, то, что вы описываете, называется Event Sourcing, и тоже не является обязательным пререквизитом для CQRS (хотя и часто используется вместе с ним).


                        1. FractalizeR
                          24.07.2015 10:57

                          Да, похоже, что вы правы. Спасибо!


            1. Flammar
              20.07.2015 15:30

              Вспоминаю в Java 1.6 новые быстрые методы межпоточного взаимодействия из серии compare-and-set и compare-and-swap…


    1. yul
      20.07.2015 16:51

      Если уж подходить в автоматизации индексов, то я бы делал это с другой стороны — собирал статистику использования и на основании этих данных создавал бы индексы. И не надо пихать это в приложение.
      Но есть и другие моменты, N+1 запрос, сложные поисковые условия и т.п., которые уже не разрулишь, не влезая в код.


  1. FaNtAsY
    20.07.2015 08:24
    +4

    Не могу согласиться с первым примером — каскадным удалением.
    База данных — это не только набор таблиц, но и отношения между ними, внешние ключи. Таким образом БД гарантирует целостность данных.
    Поэтому каскадное удаление также и зона ответственности БД.
    Кроме того, СУБД выполнит эту операцию быстрее.


    1. vintage
      20.07.2015 13:03
      +3

      Лучше вообще ничего не удалять и тем более каскадно, кроме случаев когда требуется именно их бесследное уничтожение.


      1. FaNtAsY
        20.07.2015 13:27

        Ну, это смотря для чего лучше :)
        Каждый случай я бы рассматривал отдельно.


  1. fogone
    20.07.2015 09:05
    +6

    думаю, число проектов провалившихся из за плохого кода сравним с количеством проектов которые так и не стартовали из за попытки полностью отделить бизнес логику от логики работы с БД средствами орм. И уж совсем несравнимое количество тех, что загнулись из за плохого менеджмента.


  1. FiresShadow
    20.07.2015 09:49
    +1

    Настройка БД для каскадного удаления — один из примеров проникновения логики предметной области в логику сохранения данных.

    Почему вы называете каскадное удаление логикой предметной области? То, удаляем ли мы каскадно данные или же продолжаем хранить потерявшие смысл подчиненные данные — это что, логика предметной области? Имхо, каскадное удаление — это оптимизация хранилища, с целью экономии места в хранилище, на основании предсказания о том, что после того, как нас перестанет интересовать автомобиль, нас точно никогда не заинтересует его дверная ручка, даже если её перевесят на интересующий нас автомобиль.

    Вы хотите, чтобы у вас вся логика была в коде, в том числе каскадное удаление. При этом вы заявляете, что невозможно сделать так, чтобы в коде было указано, что удалять данные следует каскадно, но при этом каскадное удаление делалось бы средствами бд. А вот сколько времени вы потратили, пытаясь найти способ, как это сделать? Задавали ли вы вопросы на каких-нибудь форумах? Имхо, решение очевидно. Указали атрибутами предсказания, и потом эти атрибуты собираем и на их основании генерируем внешние ключи с каскадным удалением.

    Какие вы видите альтернативы каскадному удалению на стороне бд? 1)Каждый раз делать каскадное удаление вручную? 2)Удалить внешние ключи? 3)Указать необходимость каскадного удаления на стороне ORM, если она поддерживает такую функциональность (если да, а в чём тогда принципиальное отличие — указать правило в маппинге ORM или в бд)? Имхо, нет ничего плохого в каскадном удалении на стороне бд, даже без генерации внешних ключей на основании атрибутов. Между прочим, вопрос о том, что следует помещать на уровень бд, уже рассматривался в комментариях к предыдущей статье автора. Автор видимо не читал. А зря…


    1. vkhorikov Автор
      20.07.2015 14:01
      +2

      В целом согласен, действительно настраивать каскад в БД или в самой ORM — разница не большая. Лично я обычно стараюсь делать так, чтобы база данных была настолько «тупой» насколько можно, поэтому если есть возможность, то все-таки выношу каскады из БД. Комментарии читал :)


      1. konsoletyper
        20.07.2015 14:10
        +2

        Кстати, есть один аргумент в пользу управления каскадами со стороны ORM. ORM может держать состояния всех сущности, с которыми она работает. Если ORM самостоятельно обрабатывает каскады, то он может правильно обновить состояния. Если же БД самостоятельно что-то удалит без ведома ORM, то для ORM потом будет полной неожиданностью, что пытаясь что-то сделать с сущностью, он не может найти её в БД.


        1. vintage
          20.07.2015 14:28

          Находясь в кластере нужно всегда быть к этому готовым.


          1. lair
            20.07.2015 14:33
            +2

            Да и без кластера — тоже. Это стандартный для любого кооперативного приложения сценарий.


          1. konsoletyper
            20.07.2015 15:57

            JPA к этому готовит следующим образом:
            1. все entity имеют своё состояние в рамках текущей транзакции;
            2. при выходе за границы транзакции entity переходят в состояние detached, а для них — свои правила.

            Поэтому если у нас кластер, то все эффекты от соседей по кластеру будут наблюдаться в другой транзакции, которую мы в идеале не видим (а если вопрос пошёл об отключении изоляции и eventual consistency, то зачем вообще ORM?). В таком сценарии всё поломать можно только если между ORM-ными вещами фигачить мутирующий SQL. К кластеру это никакого отношения не имеет.


      1. Aclz
        20.07.2015 23:07

        Лично я обычно стараюсь делать так, чтобы база данных была настолько «тупой» насколько можно, поэтому если есть возможность, то все-таки выношу каскады из БД.

        Сломать ссылочную целостность БД, реализованную на уровне БД, на порядок сложнее, чем сломать её, реализованную в ORM: в первом случае движок СУБД это делает за нас, во втором нам приходится следить за всем самим.


  1. omikad
    20.07.2015 12:55
    +1

    Есть гораздо более сложный пример смешения логики БД и кода — транзакции. Если вы попробуете полностью отделить код от БД, то получите что код работающий над mssql транзакционен, а на mongo — нет. Для одних БД вам удобно будет использовать паттерн Unit of Work, для других придется смириться с пониманием что каждая команда будет выполнена отдельно. Эти два слоя всегда влияют друг на друга, мы должны их разводить как можно дальше, но полностью развести — невозможно. Да и с другой стороны, очень удобно в коде вводить специфичные для БД вещи — использовать Unit of Work, отмечать в классах ORM свойства индексы, связи, ключи и т.п. Это удобный инструментарий, и отказываться от него ради академически верного абстрагирования не имеет смысла.


    1. FractalizeR
      20.07.2015 14:44
      +1

      Я думаю, транзакции вполне можно воспринимать, как часть логики приложения, а не часть БД. Если сложность проекта позволяет относиться к ним как к средству атомарного управления сущностями.


  1. Flammar
    20.07.2015 20:40

    Мне видится, что СУБД и ORM — это неисчерпаемый источник «еды» для желающих потроллить на тему принципа единственности обязанности и сепарации ответственностей. СУБД — потому, что СУБД развивались с упором на целостность данных и быстродействие, и к тому же производители стремились «впихнуть» в СУБД как можно больше смежных фишек типа обработки бизнес-логики (и кое-где даже отрисовывания веб-фронтенда в опциональных пакетах стандартной поставки), и никто не рассчитывал на то, что отлаженный монолит в будущем будет нужен по частям. ORM — потому, что изначально были адаптером объектно-ориентированной логики к СУБД с перемешанными ответственностями. Понятно, что часть логики, реализованной в СУБД, было бы логически правильнее перенести в ORM. Но между ORM и СУБД лежит «неблизкий путь» через сетевые соединения, и для быстродействия желательно уменьшить потребность в сетевом трафике. Вспомним распределённые транзакции: с переносом управления транзакциями на уровень ORM любые транзакции фактически стали бы распределёнными, по крайней мере по количеству участвующих в них сетевых узлов.

    Так что, по-моему, надо осознавать, что 1) в СУБД и ORM принцип сепарации ответственностей традиционно нарушается 2) эти нарушения имеют под собой веские основания, в основном из области соображений быстродействия и лёгкости поддержания целостности данных. Хороший пример исключения из правила, о котором должен знать каждый, кто знает правило.


  1. gandjustas
    21.07.2015 00:54

    А при чем тут ORM?

    Вопрос в том как сохранить целостность данных. Ведь в конечном счете не важно как реализована модель и на чем написан код. Важно какие данные вы сохраняете и что потом получают пользователи.

    Например без контроля целостности на уровне базы сделать так, чтобы нельзя было продать два билета на одно место на концерт.

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

    Поэтому идеал не в том, чтобы изолировать логику от базы, а в том, чтобы логика в коде приложения и база данных работали согласовано, не было дублирования кода и тонн «водпроводного кода», который перекладывает данные туда-сюда.

    ЗЫ. Современные ОРМы в этом помогают.