Друзья, вновь пришло время авторской колонки корпоративного блога PG Day’17. Предлагаем вашему вниманию сравнительный анализ работы с PostgreSQL из популярных ORM от varanio.


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


Посмотрим, насколько это удается современным библиотекам на PHP. Давайте рассмотрим несколько типичных кейсов и сравним ORM с голым SQL, написанным вручную.


Для примера возьмем две таблицы: книги и авторы книг, отношение многие-ко-многим (у книг может быть много авторов, у авторов может быть много книг). Т.е. в базе это будут books, authors и связующая таблица author_book:


Вот схема
CREATE TABLE authors (
   id bigserial,
   name varchar(1000) not null,
   primary key(id)
);

CREATE TABLE books (
   id bigserial,
   name VARCHAR (1000) not null,
   text text not null,
   PRIMARY KEY (id)
);

CREATE TABLE author_book (
   author_id bigint REFERENCES authors(id),
   book_id bigint REFERENCES books(id),
   PRIMARY key(author_id, book_id)
);

Рассмотрим несколько кейсов использования.


Кейс 1. Создание записей


Добавим авторов и книг.


Голый SQL


Ну, тут всё просто и прямолинейно:


Голый SQL
    $stmt = $pdo->prepare(
        "INSERT INTO books (name, text) VALUES (:name, :text) RETURNING id"
    );
    $stmt->execute(
        [':name' => 'Книга', ':text' => 'Текст']
    );
    $bookId = $stmt->fetchColumn();

    $stmt = $pdo->prepare(
        "INSERT INTO authors (name) VALUES (:name) RETURNING id"
    );
    $stmt->execute(
        [':name' => 'Автор']
    );
    $authorId = $stmt->fetchColumn();

    $pdo->prepare(
        "INSERT INTO author_book (book_id, author_id) VALUES (:book_id, :author_id)"
    )->execute(
        [':book_id' => $bookId, ':author_id' => $authorId]
    );

Многовато писанины. Можно не использовать прям совсем голый PDO, а взять что-нибудь чуть полаконичнее, какую-нибудь легкую обертку. Но в любом случае надо писать запросы вручную и знать синтаксис SQL.


Laravel (Eloquent SQL)


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


Итак, делаем две модели. По умолчанию даже имена таблиц нигде указывать не надо, если они называются как классы. Надо указать $timestamps = false, чтобы не сохраняло автоматически время обновления модели.


Классы моделей Eloquent
 namespace App;

 use Illuminate\Database\Eloquent\Model;

 class Book extends Model
 {
     public $timestamps = false;

     public function authors()
     {
         return $this->belongsToMany(Author::class);
     }
 }

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    public $timestamps = false;

    public function books()
    {
        return $this->belongsToMany(Books::class);
    }
}

Как видно, мы запросто описали отношение many-to-many буквально парой строк кода. Создание записей в базе и связь между ними делается достаточно просто:


$book = new \App\Book;
$book->name = 'Книга';
$book->text = 'Текст';
$book->save();

$author = new \App\Author;
$author->name = 'Автор';
$author->save();

// делаем связь
$book->authors()->save($author);

Или списком:


$book = \App\Book::create(['name' => 'Книга', 'text' => 'Текст']);
$author = \App\Author::create(['name' => 'Автор']);
$book->authors()->save($author);

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


Symfony (Doctrine ORM)


В доктрине используется подход DataMapper. По уверениям документации, объекты бизнес-логики отделены от способа сохранения. Здесь объекты получаются из Репозитория (Repository), т.е. сущность не знает как себя получить, это знает только Repository, а для сохранения потребуется EntityManager.


Сгенерировать классы из существующих таблиц можно одним движением:


bin/console doctrine:mapping:import --force AppBundle yml
bin/console doctrine:generate:entities AppBundle

Первая команда создаст yml-файлы для сущностей, описывающие типы полей в базе, взаимосвязь объектов (например, many-to-many) и т.д. Вторая команда создаст классы сущностей.


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


Зато сами классы-сущности у нас получились совершенно простые, т.е. POJO (plain old php object):


Классы-сущности
namespace AppBundle\Entity;

/**
 * Authors
 */
class Authors
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    /**
     * @var
 \Doctrine\Common\Collections\Collection
     */
    private $book;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->book = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Authors
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Add book
     *
     * @param \AppBundle\Entity\Books $book
     *
     * @return Authors
     */
    public function addBook(\AppBundle\Entity\Books $book)
    {
        $this->book[] = $book;

        return $this;
    }

    /**
     * Remove book
     *
     * @param \AppBundle\Entity\Books $book
     */
    public function removeBook(\AppBundle\Entity\Books $book)
    {
        $this->book->removeElement($book);
    }

    /**
     * Get book
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getBook()
    {
        return $this->book;
    }
}

namespace AppBundle\Entity;

/**
 * Books
 */
class Books
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    /**
     * @var string
     */
    private $text;

    /**
     * @var \Doctrine\Common\Collections\Collection
     */
    private $author;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->author = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Books
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set text
     *
     * @param string $text
     *
     * @return Books
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Get text
     *
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Add author
     *
     * @param \AppBundle\Entity\Authors $author
     *
     * @return Books
     */
    public function addAuthor(\AppBundle\Entity\Authors $author)
    {
        $this->author[] = $author;

        return $this;
    }

    /**
     * Remove author
     *
     * @param \AppBundle\Entity\Authors $author
     */
    public function removeAuthor(\AppBundle\Entity\Authors $author)
    {
        $this->author->removeElement($author);
    }

    /**
     * Get author
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getAuthor()
    {
        return $this->author;
    }
}

Создаем объекты и сохраняем. Примерно так:


$em = $this->getDoctrine()->getManager();

$author = new Authors();
$author->setName("Автор");

$book =  new Books();
$book->setName("Книга");
$book->setText("Текст");

$book->addAuthor($author);
$author->addBook($book);

$em->persist($book);
$em->persist($author);
$em->flush();

Вывод


В целом, использование ORM для простых случаев создания записей в таблицах является более предпочтительным способом, чем чистый SQL. Методы setName и т.д. в коде читаются лучше, чем SQL-запрос. Нет жесткой зависимости от БД.


Кейс 2. Обновление названия книги


Голый SQL


$stmt = $pdo->prepare('UPDATE books SET name=:name WHERE id=:id');
$stmt->execute([
    ':name' => 'Книга 2', ':id' => 1
]);

Laravel (Eloquent)


$book = \App\Book::find(1);
$book->name = 'Книга 2';
$book->save();

Symfony


$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository(Books::class);
$book = $repository->find(1);
$book->setName("Книга 2");
$em->persist($book);

Вывод


Обновление какого-то поля в целом тоже вполне можно делать через ORM, не вдаваясь в детали SQL.


Кейс 3. Получить список названий книг с авторами


Для тестов создадим такие записи в таблице:


Данные
delete from author_book;
delete from books;
delete from authors;

insert into authors 
(id, name) 
values 
(1, 'Автор 1'),
(2, 'Автор 2'),
(3, 'Автор 3');

insert into books 
(id, name, text) 
values 
(1, 'Книга 1', 'Много текста 1'),
(2, 'Книга 2', 'Много текста 2'),
(3, 'Книга 3', 'Много текста 3');

insert into author_book 
(author_id , book_id) 
values 
  (1,1),
  (1,2),
  (2,2),  
  (3,3);

Голый SQL


Если брать голый SQL для вывода списка книг с авторами, то это будет примерно так (допустим, авторов хотим получить в виде json):


select 
  b.id as book_id, 
  b.name as book_name, 
  json_agg(a.name) as authors 
from books b 
   inner join author_book ab
      on b.id = ab.book_id
   INNER join authors a 
      on ab.author_id = a.id
GROUP BY 
   b.id

Результат:


 book_id | book_name |        authors         
---------+-----------+------------------------
       1 | Книга 1   | ["Автор 1"]
       3 | Книга 3   | ["Автор 3"]
       2 | Книга 2   | ["Автор 1", "Автор 2"]
(3 rows)

Laravel


Сделаем сначала втупую из мануалов а-ля "Getting Started":


    $books = \App\Book::all();

    /** @var $author \App\Author */
    foreach ($books as $book) {
        print $book->name . "\n";
        foreach ($book->authors as $author) {
            print $author->name . ";";
        }
    }

Код получился гораздо проще, чем голый SQL. Все просто и понятно, works like magic. Только при детальном рассмотрении магия там достаточно фиговая. Eloquent делает аж 4 запроса:


select * from "books";
-- и еще по запросу на каждую книгу: 
select 
   "authors".*, 
   "author_book"."book_id" as "pivot_book_id", 
   "author_book"."author_id" as "pivot_author_id" 
from "authors" 
   inner join "author_book" 
       on "authors"."id" = "author_book"."author_id" 
where "author_book"."book_id" = ?

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


Во-первых, конструкции select * и select authors.*. За такое сразу партбилет на стол. Если книги будут "жирными" ("Война и Мир" или "Британская энциклопедия"), то ни к чему тянуть сразу их текст, когда нужен только список названий. К тому же, со временем в таблицах количество полей обычно все нарастает и нарастает, т.е. такое приложение будет работать всё медленнее и медленнее, жрать всё больше и больше памяти. Я уж не говорю о том, что количество запросов authors.* равно количеству книг.


Что тут можно предпринять? Во-первых, можно указать, какие поля берем из книги, т.е (['id', 'name']). Ну, и использовать with() для т.н. "eager loading". Итого:


$books = \App\Book::with('authors')->get(['id', 'name']);

Стало немного получше, но всё равно далеко от идеала:


select "id", "name" from "books";
select 
  "authors".*, 
  "author_book"."book_id" as "pivot_book_id", 
  "author_book"."author_id" as "pivot_author_id" 
from "authors" 
  inner join "author_book" 
    on "authors"."id" = "author_book"."author_id" 
where 
  "author_book"."book_id" in (?, ?, ?);

Тут две проблемы: authors идут всё равно со звездочкой. Кроме того, появился оператор in() с перечислением всех id, который нормально работает при маленьком количестве книг, но для большого списка это будет работать очень медленно, по крайней мере в PostgreSQL. Хотя, конечно, быстрее, чем по запросу на каждый. И с этим уже, похоже, ничего не сделать, по крайней мере я ничего не нашел.


Точнее, помимо ORM есть еще Query Builder:


     $result = DB::table('books')
         ->join('author_book', 'books.id', '=', 'author_book.book_id')
         ->join('authors', 'author_book.author_id', '=', 'authors.id')
         ->select('books.id', 'books.name', 'authors.name')
         ->get();

Но это, повторяю, не ORM. Это тот же SQL, только вместо пробелов стрелочки и куча методов, которые надо знать дополнительно.


Symfony


Для начала тоже попробуем по-простому:


$doctrine = $this->getDoctrine();
$books = $doctrine
    ->getRepository(Books::class)
    ->findAll();

foreach ($books as $book) {
    print $book->getName() . "\n";
    foreach ($book->getAuthor() as $author) {
        print $author->getName() . ";";
    }
}

Код первой попытки почти такой же как в Laravel. SQL-запросы, в общем, тоже:


SELECT 
    t0.id AS id_1, 
    t0.name AS name_2, 
    t0.text AS text_3 
FROM books t0;

-- и еще 3 запроса таких:
SELECT 
    t0.id AS id_1, 
    t0.name AS name_2 
FROM authors t0 
    INNER JOIN author_book 
         ON t0.id = author_book.author_id 
WHERE 
     author_book.book_id = ?

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


У стандартных методов типа findAll и т.д., похоже, нет способа указать, что мне надо только такие-то поля и сразу приджойнить такие-то таблицы. Но, зато в Доктрине есть SQL-подобный синтаксис DQL, абстрагированный от конкретной СУБД, которым можно воспользоваться. Он оперирует не таблицами, а сущностями.


 $query = $this->getDoctrine()->getManager()->createQuery('
    SELECT 
         partial b.{id, name}, partial a.{id, name} 
    FROM AppBundle\Entity\Books b 
       JOIN b.author a'
 );
 $books = $query->getResult();

Ну да, получилось типа того, что надо, один запрос, с одним полем:


SELECT 
   b0_.id AS id_0, 
   b0_.name AS name_1, 
   a1_.id AS id_2, 
   a1_.name AS name_3 
FROM 
   books b0_ 
INNER JOIN author_book a2_ 
   ON b0_.id = a2_.book_id 
INNER JOIN authors a1_ 
   ON a1_.id = a2_.author_id

Выводы


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


DQL в принципе сойдет на замену SQL, и он не особо привязан к СУБД, но это еще один странноватый синтаксис, который надо учить отдельно.


Кейс 4. Чуть более сложный UPDATE


Допустим, стоит задача обновить двум последним авторам имя на "Жорж".


голый SQL


Тут всё просто, запрос с подзапросом.


UPDATE authors
SET name = 'Жорж'
WHERE id in (
    SELECT id
    FROM authors
    ORDER BY id DESC
    LIMIT 2
);

Laravel


Сначала я попробовал сделать так:


 \App\Author::orderBy('id', 'desc')->take(2)->update(["name" => "Жорж"]);

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


Тогда, покурив мануал и SO, удалось родить такую конструкцию:


\App\Author::whereIn(
        'id',
        function($query) {
            $query->select('id')
                ->from((new \App\Author())->getTable())
                ->orderBy('id', 'desc')
                ->limit(2);
        }
    )->update(['name' => 'Жорж']);

Это работает хорошо, хоть и не особо читабельно. Да и query builder опять какой-то подъехал.


Symfony


Сразу скажу, что выразить через DQL мне этот запрос вообще не удалось, с вложенными подзапросами там всё плохо.


Есть, конечно, query builder, но получалось что-то совсем зубодробительное, и я бросил эту затею. ORM должен помогать экономить время, а не наоборот. Надеюсь, опытные симфонисты в коментах подскажут какой-нибудь легкий и изящный способ сделать update с подзапросом.


Вывод


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


Как всегда обычно и бывает, истина где-то посередине. Для простых CRUD-операций ORM вполне может сэкономить время разработки и улучшить читабельность кода. Однако шаг вправо, шаг влево — и гораздо удобнее пользоваться нативным SQL. Например, сложные выборки/обновления (особенно, аналитические отчеты с оконными функциями и рекурсивными CTE). Компромиссным вариантом является маппинг результатов нативных запросов на объекты, Доктрина это позволяет.


В споре ORM vs SQL не победил никто.


Тем временем, всех кто намучался с ORM, тормозящими запросами и плохой производительностью в рабочих ситуациях, приглашаем на PG Day'17. У нас подготовлено для вас множество различных докладов и мастер-классов для самых разных баз данных!

Поделиться с друзьями
-->

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


  1. wladyspb
    15.05.2017 16:08
    +3

    Как обычно, «Истина где-то рядом»…
    Идеальным мне лично представляется ORM с возможностью писать чистые запросы, и далее в коде —
    «select * from table where id = ?» — через ORM,
    «select a.field, b.field as field2 from… left join on… where a.id in(....) or b.name like ....» — всё же писать на чистом SQL


    1. oxidmod
      15.05.2017 17:31

      Стоит добавить, что доктрина кроме DQL/QueryBuilder поддерживает и нативные запросы


      1. ellrion
        15.05.2017 17:36
        +1

        Как и Eloquent, как впрочем и большинство ORM


    1. DarkSpirit22
      15.05.2017 17:56

      Такое есть в X++ (Microsoft Dynamics AX)


    1. arturgspb
      16.05.2017 08:48
      +3

      Я пару лет назад понял и принял одно очень простое правило — списков объектов нет, есть только отчёты и там проще и быстрее в поддержке м развитии sql. Для выборки одного объекта на карточку этого объекта скорее всего orm может прокатить до определенного момента, в некоторых случаях, все равно на sql и там перейдешь. Для добавления или редактирования одного объекта тоже orm прокатит. А вот для тех, кто статистику с 100к записями в модели сериализует вместо hashmap или пр. есть отдельный котёл. )


      1. VolCh
        16.05.2017 09:05

        А что не так со списком объектов? Вот прямо сейчас пишу вывод отчёта в виде шаблонизации и простенькой агрегации (суммирования итогов) в приложении списка объектов CashflowMonthlyReportEntry. Можно было бы и агрегацию в БД делать, но это два тяжелых запроса, отличающихся только наличием одного поля и GROUP BY по нему — не уверен, мягко говоря, в способности СУБД понять это и использовать результат предыдущего запроса их кэша в качестве основы для второго. Можно было бы и на массивах это сделать, но с объектами как-то удобнее в плане автодополнения, навигации, рефакторинга и т. п. в IDE.


        1. QuickJoey
          16.05.2017 11:42

          А вам не надо два тяжёлых запроса. Положите всё во времянку, и дальше делайте что угодно, выборка, выборка с group by по одному полю, по второму, агрегация вообще всего. Хотя это опять же надо в хранимой процедуре делать, а вы их, насколько помню, не любите)


          1. VolCh
            16.05.2017 12:36

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


            1. Fesor
              16.05.2017 14:05

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


              а размазывание логики одного уровня по разным слоям приложения.

              но вы же от SQL всеравно не уйдете. А значит у вас уже все размазано между слоями. Нет? А если у нас часть агрегации делается в базе (потому что это проще) и часть в приложений — это вроде как и есть размазывание, нет?


              1. VolCh
                16.05.2017 14:15
                +1

                В 90+% случаев об SQL даже не "подозреваю" :) И в 99+% случаев логики в СУБД не хранится, только данные их схема (если не считать логикой автоматически вставляемые ограничения по ключам). Формирование SQL запроса динамически я не отношу к размазыванию логики. А вот уже вьюшки хранить в базе — размазывание по сути.


                1. Fesor
                  16.05.2017 14:26
                  +1

                  Ну в целом я с вами согласен.


        1. Fesor
          16.05.2017 14:00

          Можно было бы и на массивах это сделать, но с объектами как-то удобнее в плане автодополнения, навигации, рефакторинга и т. п. в IDE.

          нормальная IDE это все умеет и для SQL.


          1. VolCh
            16.05.2017 14:15

            Я не про SQL, а про оперирование результатами запроса и(или) формирование его параметров.


            1. Fesor
              16.05.2017 14:24

              А просто мэппер не подходит? без обратной конвертации (для этого ORM есть).


              1. VolCh
                16.05.2017 14:31

                Просто мэппер — это половина ORM (или ORM в режиме read only)?


                1. Fesor
                  16.05.2017 15:26

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


  1. MadridianFox
    15.05.2017 16:39
    +2

    А вот интересно, какие вообще плюсы от использования ORM?
    Абстрагирование от конкретной БД в них так себе, да и кому оно надо? Т.е. я понимаю, что это мифическая возможность без головной боли менять СУБД как перчатки. Но это работает только когда приложение не использует специфику СУБД.
    Маппинг строк таблицы на объекты? Ну тоже не оч. Зачастую бизнес-сущность хранится сразу в нескольких таблицах, и вместо того чтобы оперировать одним объектом, который мы вручную написали как нам надо, мы жуём кактус, состоящий из типовых объектов-строк.
    Возможность не знать SQL? да ладно? Его надо знать в любом случае.


    1. ellrion
      15.05.2017 16:42

      Возможность удобно динамически формировать запрос


      1. MadridianFox
        15.05.2017 19:06
        +4

        Для формирования запроса можно использовать QueryBuilder.


        1. mayorovp
          16.05.2017 22:06

          Редкий QueryBuilder позволяет формировать вложенные запросы...


          1. Fesor
            16.05.2017 22:46
            +1

            Ну те что мне доводилось использовать это умели.


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


    1. ellrion
      15.05.2017 16:58

      Что такое "типовые объекты-строки"?
      Если у нас данные о сущности хранятся в разных таблицах, то есть связи.


    1. iborzenkov
      15.05.2017 17:09

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

      Вот именно маппинг на объекты, а потом обновление всего после того как поменяли объекты.
      Как-бы orm могут разбивать объекты и таблицы и автоматически подтягивать вложенные по связям.
      В 90% легких запросов у нас уже есть готовый код, а в случае тяжелых — ну выявятся и оптимизируются.


      1. MadridianFox
        15.05.2017 19:16
        +2

        ORM позволяет абстрагироваться от конкретной базы, но не от хранилища данных в целом. Вот только совместимые различия баз, вероятнее всего можно сгладить просто используя QueryBuilder, который будет переводить унифицированные запросы в специфичные.
        По поводу маппинга на объекты, а точнее управления их состоянием, может быть, вот только польза от этого видна лишь в stateful приложениях. На PHP, где 90% эндпоинтов либо только читают из базы, либо только пишут в неё, держать пул объектов и следить за их идентичностью (IdentityMap) бесполезно.
        Конечно, бывают случаи повторного чтения/записи, сам с таким сталкивался. Но бывает это редко, и наверное оно не стоит того чтобы постоянно иметь немалый оверхед от ORM.
        Да, ORM предлагают красивое решение для простых случаев. Но как только начинается что-то посложнее, начинается борьба с ORM, когда ты понимаешь как можно написать запрос на sql, но ORM заставляет тебя совать друг в друга лямбды, которые строят куски запросов с помощью того-же QueryBuilder'a.


        1. VolCh
          16.05.2017 09:32

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

          Позволяет и от хранилища в целом, если приложение сильно не завязано на конкретную ORM. Вот давеча вынес модуль из монолитного приложенияс активным использованием Doctrine в REST-like HTTP-сервис почти без изменения остального кода. Основные изменения (кроме собственно выделение модуля в отдельное приложение т. п.):


          • замена class DoctrineCashflowRepository implements CashflowRepositoryInterface на class HttpCashflowRepository implements CashflowRepositoryInterface
          • удаление из контроллеров $om->flush();
          • замена Doctrine relations на ручное заполнение и, самое сложное, сохранение с помощью нового сервиса (есть идея написать свою имплементацию доктриновского ObjectManagerInterface с использованием её же UnotOfWork, IdentityMap и т. д., но надо разобраться стоит ли овчинка выделки и может имеет смысл собирать информацию из нескольких источников исключительно на этапе отдачи ответа клиенту )


          1. MadridianFox
            16.05.2017 09:41

            Это заслуга не столько ORM, сколько того, что вы заранее ввели абстракцию в виде интерфейса.
            Того же эффекта можно было добиться и без ORM, главное тут — чтобы клиентский код обращался строго к интерфейсу и не знал подробности того как этот интерфейс работает внутри.


            1. VolCh
              16.05.2017 10:08

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


    1. Alexeyco
      15.05.2017 17:47
      +1

      Смотря что считать ORM. В общем случае это еще и миграции, защита от дурака, синтаксический сахар (очень актуально у Eloquent).


      1. MadridianFox
        15.05.2017 19:18
        +1

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


        1. VolCh
          16.05.2017 09:41

          Смотря с какой стороны смотреть. Просто как инструмент накатывания (и откатывания) последовательных инкрементных изменений на схему базу — не имеют. А вот если говорить об автоматической генерации SQL-кода этих изменений на основании различий в двух версиях декларативного описания схемы базы, то можно использовать для этого заметную часть универсальных ORM-библиотек: генерация SQL-кода на основе изменений в графе объектов — это одна из двух основных задач ORM.


          1. MadridianFox
            16.05.2017 09:48

            ORM, а точнее популярные ORM-библиотеки всё же делают упор на работу с графами объектов данных и соответственно на генерацию DML запросов.
            Генерация же DDL запросов, как и их применение к базе это задача механизма миграций.


            1. VolCh
              16.05.2017 10:14

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


              1. MadridianFox
                16.05.2017 11:04

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


                1. VolCh
                  16.05.2017 11:19

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


                  1. MadridianFox
                    16.05.2017 11:47

                    Согласен. Но тогда получается, что люди выбирают не столько ORM, сколько хорошо притёртый набор библиотек в составе ORM. Что же тогда отличает ORM от набора библиотек?


                    1. VolCh
                      16.05.2017 12:45

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


                      А отличает ORM-библиотеки с функциями типа миграции и абстракции от СУБД от набора библиотек с этими функциями именно наличие собственно ORM — универсального механизма отображения объектной модели на реляционную и наоборот. Остальное в ORM (миграции, генерации схемы по объектам и объектов по базе, абстракция от СУБД и прочая, и прочая, и прочая :) по сути бесплатные бонусы


    1. Caravus
      15.05.2017 18:11

      Маппинг строк таблицы на объекты? Ну тоже не оч. Зачастую бизнес-сущность хранится сразу в нескольких таблицах, и вместо того чтобы оперировать одним объектом, который мы вручную написали как нам надо, мы жуём кактус, состоящий из типовых объектов-строк.

      ORM как раз пзволяет представлять «несколько таблиц» как один объект. По крайней мере (не упомянутый в статье) AR из Yii2. И работать с результатом как с одним объектом, а не городить огород из вложенных циклов.
      В целом же, как верно заметили выше, ORM очень экономит время на простых (и, как показывает практика, самых частых в написании) запросах типа CRUD. Мне не надо думать как там называется таблица, в какой схеме/базе она лежит, какие поля там PR… я просто вызываю метод delete/save у объекта…


      1. MadridianFox
        15.05.2017 19:45

        Вложенные циклы устраняются не использованием ORM, а использованием объектов для выстраивания структуры обрабатываемых данных.
        Не думать как там называется таблица можно введя дополнительный слой абстракции над слоем, который выполняет запросы к базе. Более того, выделение слоя, который выполняет запросы к базе, позволяет таки… выполнять запросы к базе. Т.е. ты пишешь sql запрос, и тебя совесть не мучает за то что ты делаешь хак в обход ORM.
        Мне кажется, лучше руками писать простые запросы и иметь возможность так же быстро написать сложный, чем экономить время на простых запросах и безбожно тратить его на сложных.


        1. Caravus
          15.05.2017 19:56

          Мне, возможно, не хватает какого-то опыта с ORM, но что мешает писать sql запрос через ORM и чтоб при этом «не мучала совесть»? В том же упомянутом Yii2 — ничто не мешает подсунуть голый SQL, если требуется, при этом пользоваться фишками ORM, если требуется (indexBy, подключение к базе, параметры запроса, из того что первое пришло в голову). Не понимаю что мешает и «экономить время на простых запросах» и при этом не париться со сложными.

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


          1. MadridianFox
            15.05.2017 20:06
            -1

            Когда вы используете ORM и пишете SQL код в сложных случаях, вы теряете независимость от конкретной СУБД, т.е. один из «плюсов» использования ORM.
            В случаях, когда вам необходимо абстрагироваться от СУБД, вам придётся вводить слой абстркации. Да и без такой необходимости, выделение работы с базой делает код чище.
            «Фишки ORM» совсем не фишкки ORM:

            • indexBy — работа с коллекциями
            • подключение к базе — фишка драйвера, такого как PDO, ну или библиотеки, облегчающей работу с базой
            • параметры запроса — аналогично

            то есть нужно писать документацию

            Да ладно? Вы считаете что это оверхед? А ничего что документация должна быть в любом случае?


            1. Caravus
              15.05.2017 20:12
              +1

              Я не приводил аргументов за «независимость от конкретной СУБД», не считаю вообще это плюсом.

              indexBy — работа с коллекциями
              Эту работу за меня сделал ORM, я этого не писал, мне не нужно этот код поддерживать, например.
              подключение к базе — фишка драйвера, такого как PDO, ну или библиотеки, облегчающей работу с базой
              Этой библиотекой и является ORM. Просто библиотека-надстройка над PDO.
              Да ладно? Вы считаете что это оверхед? А ничего что документация должна быть в любом случае?
              Кол-во документации различается, нет? Таки да, я считаю оверхэдом писать документацию в своём проекте для стандартных модулей моего фреймворка. Или вы мне сейчас ещё скажете что и фреймворки — зло?

              Как там на счёт ответов про «что мешает писать sql запрос через ORM»? Это, пожалуй, единственная интересная часть этой переписки.


              1. MadridianFox
                16.05.2017 00:56
                +1

                Вы путаете набор библиотек для работы с БД и коллекциями, и ORM, основной задачей которого является поддержание консистентного состояния объектов в памяти приложения в соответствии с данными в БД.
                Этим я хочу сказать, что ни работа с коллекциями, ни удобное подключение к базе, ни сахар при работе с БД не являются киллер-фичами ORM. Да, они есть в ORM, но также их можно заполучить просто используя отдельные библиотеки.
                Так что же полезного в самом ORM?

                По поводу документации. Так или иначе, сущности и логику работы приложения как-то описать надо, не важно, используете ли вы ORM или пишете запросы руками. Слой абстракции не подразумевает создание собственного сложного фреймворка для доступа к данным, достаточно писать запросы не в самом объекте предметной области, а в отдельном классе, названия методов которого вполне самодокументируемы по названию. Что может быть непонятного в методе findNewsByTitle(title) внутри которого одной-двумя строчками делается запрос и отдаётся массив объектов? Для этого не требуется обширной документации.

                Ну и напоследок про sql через ORM.
                Всё-таки ORM сам по себе является слоем абстракции. Работа с базой должна быть скрыта в этом слое. А когда мы пишем sql через ORM — мы вытаскиваем работу с базой наружу.


                1. VolCh
                  16.05.2017 09:48

                  Что может быть непонятного в методе findNewsByTitle(title) внутри которого одной-двумя строчками делается запрос и отдаётся массив объектов?

                  Так этот метод и есть часть механизма ORM — он же осуществляет маппинг реляционных данных на объекты?


                  А когда мы пишем sql через ORM — мы вытаскиваем работу с базой наружу.

                  Мы пишем sql не через ORM, а внутри ORM. Клиенту того же репозитория Doctrine без разницы используется внутри репозитория полностью автоматическая генерация запросов средствами Doctrine, ручками написанный DQL-запрос, или вылизанный SQL-запрос, если результат один и тот же.


    1. 0x1000000
      16.05.2017 08:18

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


      1. Metus
        16.05.2017 09:39
        +1

        Будет более чем уместно, если правильно сделан маппинг (M) между базой данных и объектами.


        1. 0x1000000
          16.05.2017 12:49

          Если у вас маппинг 1:1 то все хорошо и ORM уместен. Иначе приходится делегировать маппинг SQL запросам или делать это на уровне приложения получая риск проблем с производительностью.


      1. VolCh
        16.05.2017 09:55
        +1

        Я бы сказал с точностью до наоборот. Если в приложении и так используется реляционная модель (пускай и на объектах), то толку от ORM будет мало, маппинг 1:1 простой. ORM хороша именно когда маппинг не 1:1, например в случае связей 1:N. В реляционной модели результат это N сущностей одного типа, половина (в широком смысле слова) значений в которых дублируется, а объектной (графовой) — одна сущность одного типа, связанная с N сущностями другого типа без дублирования. А уж в связи N:M в реляционной модели мы получим N*M сущностей одного типа, а в объектной — N+M.


        1. 0x1000000
          16.05.2017 11:29

          Не совсем понимаю, что вы подразумеваете под маппингом 1:N. Можете привести пример? Могу лишь предположить, что вы получаете от ORM «N» промежуточных объектов и агрегируете их в один объект бизнес модели


          1. VolCh
            16.05.2017 12:48

            Маппинг 1:1 — это когда строка таблицы (или даже шире — результата запроса) маппится в объект с однозначным соответствием столбцов таблицы и свойств объекта.


    1. VolCh
      16.05.2017 08:56
      +2

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


      И маппинг может быть (в теории) сколь угодно сложным — сводить несколько записей разных таблиц в объект, разбивать одну строку таблицы на несколько объектов и т. п.


      И не надо путать ORM как архитектурный паттерн с универсальными ORM-библиотеками. Если вы в приложении по результатам SELECT-запросов формируете граф объектов, а по результатам изменения этого графа формируете INSERT/UPDATE/DELETE-запросы, то вы уже используете паттерн ORM в той или иной разновидности. Реализация сторонняя или своя, универсальная или только то, что нужно, с абстракцией от СУБД или без — это нюансы.


      1. MadridianFox
        16.05.2017 09:17

        Согласен.


    1. Fesor
      16.05.2017 14:10

      А вот интересно, какие вообще плюсы от использования ORM?

      Смотря какой.


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

      Если вы про приемы вроде наследования таблиц или еще чего такое — то это все как бы умеют ORM. А если вы про случаи когда одну сущность мы по каким-то причинам поделили на две таблицы — я не знаю зачем так делать. Этим мы только себе в ногу выстрелили.


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


      Для меня профит от ORM в операциях на запись!, когда у нас есть небольшой граф объектов и мы что-то с ним делаем. В этом случае на уровне приложения у меня кучка объектов, которые обмениваются сообщениями. Вся логика спокойно покрывается юнит тестами и тд. Ну и за счет механизмов вроде unit-of-work я могу "закоммитить" изменения всего графа в базу. И это реально удобно и реально круто!


      НО на выборки для операций чтения ORM не нужны. То есть я согласен с комментарием выше — любой список это вид репорта. Там ORM не нужны.


      1. padlyuck
        16.05.2017 16:53

        Прошу прощения за офтоп


        А если вы про случаи когда одну сущность мы по каким-то причинам поделили на две таблицы — я не знаю зачем так делать. Этим мы только себе в ногу выстрелили.
        Полностью согласен про выстрел в ногу, но как бы вы поступили в следующей ситуации:
        у нас есть сущность "Экскурсия" со своим списком полей и реляций и мы её продавали как товар. Грубо говоря одна экскурсия — одна строчка из таблицы "excursion" + по одной/несколько строк из связанных таблиц.

        Потом кто-то захотел ввести дополнительно сущность "Экскурсия с фиксированной датой" и эта сущность от базовой отличается только наличием полей "дата проведения" и "квота". Теперь у нас товар не просто строчка из таблицы "excursion", а строчка из таблицы "excursion_date" плюс привязанная к ней строчка из таблицы "excursion".


        Т.е. просто для какого-нибудь процесса вывода информации а-ля "вывести в какие даты доступна экскурсия N" можно использовать простую реляцию вида $excursionWithDate->getExcursionDates(). Но если нужно к примеру добавить экскурсию в корзину — то мне уже нужен объект собранный из двух таблиц.


        Как бы вы поступили в этой ситуации?
        P.S. знаю что бизнес-объекты рассматривать как строчки из таблицы нельзя, это было сделано для наглядности.


        1. padlyuck
          16.05.2017 16:59

          пардон, с разметкой немного ошибся


        1. VolCh
          16.05.2017 18:41

          По описанию "Экскурсия с фиксированной датой" должна быть наследником "Экскурсия", что легко решается нормальными ORM несколькими способами, в том числе созданием таблицы excursion_date с тремя полями, но по тому же описанию объекты класса "Экскурсия" — это не товар, а наименование товара, а "Экскурсия с фиксированной датой" — ограничения на покупку товаров данного наименования: есть запись с датой и квотой — есть ограничения, нет — нет. То есть не "Экскурсия с фиксированной датой", а "Квоты на Экскурсию по датам", ссылающаяся на "Экскурсия" и в логике добавления экскурсию в корзину добавляется проверка на ограничения, а так всё остаётся тем же самым.


          Структура таблиц одна и та же, но вот маппинг их на объекты кардинально разный.


          1. Fesor
            16.05.2017 18:54

            По описанию "Экскурсия с фиксированной датой" должна быть наследником "Экскурсия"

            не факт. Это может быть просто опциональной характеристикой любой экскурсии. Либо просто будет сущность которая будет ссылаться на конкретную экскурсию. Это уже от логики зависит.


            p.s. не люблю наследование.


            1. VolCh
              16.05.2017 19:13

              Исхожу исключительно из описания:


              Потом кто-то захотел ввести дополнительно сущность "Экскурсия с фиксированной датой" и эта сущность от базовой отличается только наличием полей "дата проведения" и "квота".

              Бизнес считает "Экскурсия с фиксированной датой" отдельной сущностью, для которой базовой является "Экскурсия". Технически это наследование, которое я тоже не люблю. И тут бы я с аналитиком поспорил, по задаче не похоже что тут должно быть наследование.


              1. padlyuck
                16.05.2017 19:34

                Возможно я не совсем понятно объяснил разницу между этими двумя сущностями. На живом примере:
                Обычная экскурсия "Прогулочный маршрут по центру столицы" можно купить билет и использовать его 1 раз в любой день в течении полугода с момента покупки.


                Экскурсия с фиксированной датой "Прогулочный маршрут по историческим местам". Экскурсия проводится к примеру 16.05.2017, 23.05.2017 и 30.05.2017. Купить билет возможно на любую из этих трех дат при условии что дата еще не наступила и в желаемую дату есть свободные места. Использовать билет можно только в выбранную дату.


                Контролем использования билетов занимается отдельная система, так что эта часть процесса в рамках задачи не важна


                1. Fesor
                  16.05.2017 19:54
                  +1

                  вот, чуть конкретнее, ок.


                  То есть наши "экскурсии" на самом деле ничем не отличаются. Просто для некоторых видов экскурсий у нас есть еще отдельно билеты с разными датами.


                  Следовательно различаются билеты. А билеты и так есть для каждой экскурсии. Вот там может быть и имеет смысл использовать наследование но я не уверен. Просто коллекция билетов. Но опять же не уверен — надо логику понимать лучше.


                  1. padlyuck
                    16.05.2017 20:05

                    надо логику понимать лучше.

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


        1. Fesor
          16.05.2017 18:54

          Экскурсия с фиксированной датой

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


          Предположим что это не отдельная сущность а лишь опциональная характеристика экскурсии. Типа дэйт рэйндж за который оно действует. У остальных по умолчанию будет null-object с null-вым рэйнджем. Эту характеристику в силу ограничений СУБД я запихну в отдельную таблицу excursion_date. И в объектной модели будет соответствующая пропертя которую я буду использовать на запись.


          вывода информации а-ля "вывести в какие даты доступна экскурсия N"

          А тут я сделаю SQL запрос и замэплю данные сразу на DTO которое плюну во view. Если мне не надо сохранять изменения стэйта мне не нужен ORM. Хотя если под ORM мы подразумеваем именно паттерн а не какую-то реализацию — то этим мэппингом SQL -> DTO и будет заниматься мой ORM.


          1. padlyuck
            16.05.2017 19:12

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

            Жизненный цикл фактически один и тот же. На практике разница выливается только в два момента:


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

            Предположим что это не отдельная сущность а лишь опциональная характеристика экскурсии. Типа дэйт рэйндж за который оно действует. У остальных по умолчанию будет null-object с null-вым рэйнджем. Эту характеристику в силу ограничений СУБД я запихну в отдельную таблицу excursion_date. И в объектной модели будет соответствующая пропертя которую я буду использовать на запись.

            Т.е. вы предлагаете сделать все экскурсии "как бы" с фиксированной датой, но у обычных экскурсий вместо даты/квоты будет null-object?


            1. Fesor
              16.05.2017 19:36

              Т.е. вы предлагаете сделать все экскурсии "как бы" с фиксированной датой, но у обычных экскурсий вместо даты/квоты будет null-object?

              да, иначе мы нарушим LSP (если вдруг решили наследоваться). Да и с точки зрения отображения это будет проще и логичнее. А те экскурсии для которых нет фиксированных дат — ну они всегда будут возвращать true при вызове isAvailableAt какого-нибудь.


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


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


              1. padlyuck
                16.05.2017 19:48

                понял. спасибо за разъяснения.


    1. mayorovp
      16.05.2017 22:06

      Как на PHP не знаю, но на C# крайне полезными являются язык выражений, который ближе к реляционной алгебре чем SQL, и проверка корректности запросов компилятором.


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


  1. ellrion
    15.05.2017 16:48

    Когда вы говорите про просадку перфоманса от WHERE IN или от *. То забываете упомянуть что обычно в приложении оперировать приходится не всеми хранимыми сущностями. Пагинация или бесконечная прокрутка или обработка чанками идет практически всегда. И эти факторы превращаются в экономию на спичках. А удобство и простота кода гораздо более приоритетные.


    1. ellrion
      15.05.2017 16:52
      +1

      А так же что можно стандартные селекты (например книги со всеми данными кроме текста) зашить например в скоупах (в элоквенте) и "пользовательский" код в контроллере или сервисе будет простым.


      1. varanio
        15.05.2017 17:24

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


  1. Alexeyco
    15.05.2017 17:44
    +2

    В свое время работал с ораклом, плотно. Было принято не использовать orm. Поначалу напрягало, хотя и потом тоже напрягало. Когда сменил место работы, все встало на свои места: eloquent, все самое современное. Напрягать меньше не стало, т.к. вот буквально только что написал один sql-запрос тут… Лапша из замыканий. Тотально абстрагироваться от SQL не получается и думаю, что не получится. И даже больше скажу — не нужно. Я, например, обычно запрос сначала пилю в клиенте БД, а потом уже переношу в код. Но для большинства случаев этого не нужно.

    Просто иногда люди думают, что — вот покручу ORM, и катись он к черту этот SQL. А не тут-то было. И это всегда забавно.


    1. Alexeyco
      15.05.2017 17:51

      Да, и еще важное: чтобы оценивать крутость той или иной ORM, не нужно смотреть на простоту выборки типа «where id=123». Вы лучше посмотрите, как там можно реализовать выборку сущности, которая через опорную таблицу (многие-ко-многим) связана с другой сущностью, а та с третьей. Попробуйте прикинуть, как будет выглядеть написание запроса — будет ли можно не указывать названия таблиц… и будет ли актуальна вся эта красота из документации.

      Выбор ORM — это не выбор между райскими кущами, это выбор между наименьшим злом.


      1. VolCh
        16.05.2017 10:24

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


        А уж если выбрали объектную модель и реляционное хранилище, то остаётся, если хочется минимальной поддерживаемости кода, только выбирать между разными реализациями ORM, включая написания своей собственной, возможно универсальной, а может и тупо захрадкаженным вимператином стиле маппингом типа $contract->number = $sqlQueryResult['contract_number']


        1. Alexeyco
          16.05.2017 11:07

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


          1. VolCh
            16.05.2017 11:20

            Вместо объектов — массивы, списки, хэш-таблицы и т. п.


  1. reimax
    15.05.2017 17:52

    UPDATE authors
    SET name = 'Жорж'
    WHERE id in (
        SELECT id
        FROM authors
        ORDER BY id DESC
        LIMIT 2
    );
    

    и
    UPDATE authors
    SET name = 'Жорж'
    ORDER BY id DESC
    LIMIT 2;
    

    зачем подзапрос?


    1. varanio
      15.05.2017 18:01

      UPDATE… LIMIT не работает в посгресе, например


      1. reimax
        15.05.2017 18:03
        +1

        с посгрес не работал, не знал, моя ошибка значит.


  1. helions8
    15.05.2017 17:59

    Было бы интересно сравнить не с классическими ORMмами, а с (относительно) новыми решениями типа jOOQ, когда и гибкость SQL остается, и модели есть, и типобезопасность присутствует.


    1. varanio
      15.05.2017 18:03

      Насколько я вижу, JOOQ — это query builder. Т.е. по сути тот же SQL, только вместо пробелов скобочки и точки. Нет никакого абстрагирования от базы


      1. Alexeyco
        15.05.2017 18:16

        Как нет, когда есть? Абстракция от БД — это когда одна СУБД может быть заменена другой СУБД, а не когда синтаксис на SQL не похож. Только причем тут Жук, если речь о PHP.


        1. helions8
          15.05.2017 18:31

          Ну я ж и написал «типа jOOQ». Или под PHP такого не делают?


        1. Fesor
          16.05.2017 14:12

          не надо путать DBAL и ORM. Если что-то не умеет мэпить результат SQL на объекты и обратно — значит это не ORM.


          1. Alexeyco
            16.05.2017 15:40

            Спасибо, КО, я и не писал такого. DBAL — часть ORM. Я написал что написал — можно прочесть еще раз.


      1. helions8
        15.05.2017 18:20
        +1

        Я не могу согласится с тем, что jOOQ это query builder. Он в нем есть, но кроме него есть еще и модели, и кодогенерация и т.д. Это реализация object-per-table патерна. А такой код не похож на квери билдер, согласитесь:

        BookRecord book1 = create.newRecord(BOOK);
        book1.setTitle("1984");
        book1.store();
        


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


  1. Jenly
    15.05.2017 18:37
    +1

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


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

    Если нужно получить получить информацию одним запросом, то используем EntityRepository.

    class BooksRepository extends EntityRepository
    {
    
        public function getBooksWithAuthors ()
        {
        	 $result = $this->createQueryBuilder('u')
                ->select('u, a')
                ->leftJoin('u.authors', 'a')
                ->getQuery()
                ->getResult();
    
                return $result;
                
    	}
    }
    


    Уточню, что authors — ManyToMany связь для Books и entity класс для книг создан правильно, а не так как в примере.

    class BooksController
    {
    	$doctrine = $this->getDoctrine();
    	$books = $doctrine->getRepository(Books::class)->getBooksWithAuthors();
    }
    


    И получаем все в одном запросе. Книги с их авторами.

    dump($books)
    


    1. varanio
      15.05.2017 20:47

      > ManyToMany связь для Books и entity класс для книг создан правильно, а не так как в примере
      а что именно неправильно?


      1. Jenly
        15.05.2017 21:42

        Как по мне, SQL vs ORM некорректное сравнение у своего основания.

        sql — это язык, ORM — это оверхэд над sql, как большинство языков это оверхэд над процессором.

        Задача ORM — абстрагироваться от базы данных. Каждый объект ОRM служит определенной цели в бизнес-логике. Мы перестаем думать о базе данных в принципе, а начинаем мыслить объектом и целью для которой он создан.

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

        Так что бы получить всех пользователей которые привязаны к одному банку мы сделаем, что-то вроде: $banks = $user->getBanks()->getUsers();

        Получить адреса банка пользователя: $banksAddr = $user->getBanks()->getAddrs();

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

        if($user->hasBanks()){}
        


        Так же, в каждый момент времени мы должны быть уверены, что кто-то не допишет sql, который уберет нам половину данных или наоборот запишет в базу потеряв в половину.
        Или кто-то добавил обязательное поле в таблицу, чтобы все запросы работали корректно, нам нужно поправить 1 entity class и все запросы продолжат работать.

        Всю ответственность за целостность данных берет на себя берет ORM.

        Так в пример с книгами это было бы

        class Book
        {
        
          /**
             * @ORM\ManyToMany(targetEntity="Author", inversedBy="books", fetch="EXTRA_LAZY")
             * @ORM\JoinTable(name="books_author")
             */
            private $authors;
        }
        
        class Author
        {
         /**
             * @ORM\ManyToMany(targetEntity="Book", mappedBy="authors")
             */
            private $books;
        }
        


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

        $idBook = 10;
        $authors = $em->getRepository('Books::class')->find($idBook)->getAuthors();
        


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

        class createBook
        {
        function createAction(){
          $idAuthor = 10;
          $author = $em->getRepository('Books::Author')->find($idAuthor);
          $book = new Book();
          $book->setAuthor($author);
          $em->persist($book);
          $em->flush($book);
        
        }
        }
        


        1. varanio
          16.05.2017 06:27

          В моем примере все то же самое примерно, только в yaml, а не в аннотациях. Через консольную команды я создал yaml на основе foreign keys базы


        1. VolCh
          16.05.2017 10:40

          Как по мне, SQL vs ORM некорректное сравнение у своего основания.

          Согласен. Но только в этом.


          ORM — это оверхэд над sql

          ORM — это механизм прозрачной трансформации сущностей ООП-языка в язык sql и обратно. Да, универсальные механизмы вносят некоторый оверхед по сравнению с захардкоженными преобразованиями, но если для работы с данными в приложении мы выбираем объекты (особенно полноценные, а не подобные структурам C), а для их хранения SQL-базы, то нельзя говорить об ORM как об оверхеде — это необходимый для этого выбора механизм. Можно говорить о том, сколько приносят (и приносят ли) оверхеда конкретные реализации ORM по сравнению с идеальными, но не более.


          Задача ORM — абстрагироваться от базы данных.

          Всё же, абстрагироваться от реляционной сущности хранилища, а не от его наличия вообще.


          Мы перестаем думать о базе данных в принципе

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


        1. Fesor
          16.05.2017 14:13
          +1

          Задача ORM — абстрагироваться от базы данных.

          тут могут быть недопонимания. Не от базы данных а в принципе от способа хранения данных. Разделение ответственности банальное. Это не означает что мы можем взять одну СУБД и заменить другой на раз два. Это не является целью.


  1. Jenly
    15.05.2017 18:52
    +1

    Допустим, стоит задача обновить двум последним авторам имя на «Жорж».

    Сразу скажу, что выразить через DQL мне этот запрос вообще не удалось, с вложенными подзапросами там всё плохо.


    Вот так, например

    public function get2LastAuthors ()
        {
        	 $result = $this->createQueryBuilder('u')
                ->select('a')
                ->addOrderBy('a.id', 'DESC')
                ->setMaxResults( 2 );
                ->getQuery()
                ->getResult();
                return $result;
                
    	}
    


    
    class AuthorsController
    {
    	$doctrine = $this->getDoctrine();
    	$em = $this->getDoctrine()->getManager();
    	//используем QueryBuilder
    	$authors = $em->getRepository(Books::class)->get2LastAuthors();
    	//или так
    	$authors = $em->getRepository('Books::class')->findBy([], ['id' => 'ASC'], 2);
    
    	$name = "Жорж";
    	foreach($authors as $val){
                $val->setName($name);
    	}
    	$em->flush();
    }
    



    1. varanio
      15.05.2017 20:41

      спасибо


  1. ks_1
    15.05.2017 20:03

    Довольно негативная получилась заметка в отношении ORM) Давно работаю с ORM на Perl и в защиту этого подхода могу привести пример решения последних двух задач на местном ORM — DBIX::Class. Решения должны выглядеть примерно так:

    * Книги с авторами (специфичный json_agg я заменил на GROUP_CONCAT)

    $schema->resultset('Book')->search( undef, 
      { select => [
        'me.id', 'me.title', 
        { 'GROUP_CONCAT' => 'author.name', -as => 'authors_list' }
       ],
       join => 'authors',
       group_by => 'me.id'
      } );
    


    * Правка имен авторов
    $schema->resultset('Author')->search( undef,
      { order_by => { -desc => 'id' }, rows => 2 }
     )->update({ name => 'Жорж' });
    


    1. Pilat
      16.05.2017 05:31

      Вот я не понял что предполагалось во втором примере сделать. Последним двум строкам поменять автора? И что сделает на практике ORM — два запроса или один? Когда это будет понятно? Не придётся ли включать вывод каждого сгенерированного запроса, чтобы понять что выполняется и не надо ли выполнить чистый SQL, не уйдёт ли на это всё сэкономленное время?


      Была интересная статья — сравнение перловых ORM, в частности Rose::DB::Object и DBIC. По производительности разница была до 20-ти-кратной. То есть SQL(Rose к нему очень близок) в 20 раз быстрее некоторые операции выполняет. Это цена ОРМ, не считая времени на поиск проблем при разработке.


  1. stsouthpaw
    15.05.2017 20:19

    Мне интересно посмотреть на ORM если нужно сделать временную таблицу в БД, а потом с ней дальше то то делать)


    1. xRay
      15.05.2017 21:11

      :) Да много чего еще можно на SQL.
      Можно функций понаписать и в запросе к полям использовать эти функции прямо в запросе.
      ORM такого не может да, но он не для этого.


    1. oxidmod
      15.05.2017 23:58

      ORM is for CRUD


      1. VolCh
        16.05.2017 10:42

        Как раз для CRUD ORM нафиг не нужен, особенно в PHP.


    1. VolCh
      16.05.2017 10:42

      А какие PHP-объекты этой таблице будут соответствовать?


  1. http2
    15.05.2017 21:11
    -1

    SQL vs ORM — из одной крайности в другую :)


  1. Igelko
    15.05.2017 22:03
    +2

    Я на ORM и генераторы запросов смотрю как на хорошую подсказку IDE и компилятору, что у нас из базы возвращается не какой-то сферический ResultSet, а некоторый объект с вполне определёнными полями и методами.
    Это отсекает такой класс ошибок, как написать запрос, отладить его в IDE для БД (она как правило отдельная), перенести его в код и потом с удивлением ловить ошибки в рантайме, из-за того, что поле в результате или называется не как надо, или просто забыли добавить поле в выборку.
    Хорошие IDE/ORM ещё могут слазить в базу за схемой, проверить имена маппингов, сгенерить миграции итд.


    Ещё веселее становится, когда надо динамически собирать where и делать join у запроса в зависимости от входных параметров — собирать запросы конкатенацией или шаблонами довольно утомительное и багообразующее занятие.


  1. PaulZi
    15.05.2017 22:39

    Кейс 1 и 2 в Yii2 такой же как в ларавеле, а вот 3 и 4:

    Кейс 3 Yii2
    $books = Book::find()
        ->with('authors')
        ->all();
        foreach ($books as $book) {
            print $book->name . "\n";
            foreach ($book->authors as $author) {
                print $author->name . ";";
            }
        }
    

    Тут главное что будет всего два запроса, вне зависимости от числа книг. Но тут конечно ещё должен быть прописан relation через `viaTable()`.


  1. VMichael
    15.05.2017 23:47

    Забавно смотреть, как, для того, что бы не использовать SQL образуются некие псевдоязыки даже для простейших, с точки зрения SQL задач, для понимания конструкций которых нужно мозг напрягать больше, чем при разборе SQL.
    При этом производительность остается вообще за скобками, хотя, конечно, есть туча проектов, где не Big Data, а Micro Data и где главное это быстро быстро выпустить первый прототип.


    1. VolCh
      16.05.2017 10:49

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


      1. VMichael
        16.05.2017 11:32

        Уверен, что ваше определение, это не то, что имеет ввиду подавляющее большинство программистов использующих, то, что они называют ORM.
        Они не проектируют и не пишут слои абстракции, имея ввиду, ваше определение, они используют готовые ORM решения.
        Посмотрите даже на примеры в статье.
        А то

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


        1. VolCh
          16.05.2017 12:57

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


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


    1. mayorovp
      16.05.2017 22:33

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


      1. VMichael
        16.05.2017 23:17
        +2

        Я понимаю, понимаю, что люди разные и образ мышления разный.
        Да, реляционные БД, это мой профиль.
        Кроме того я был архитектором в нескольких, не самых маленьких проектах по созданию ИТ систем.
        Системы здравствуют и живут дальше по сию пору.
        Когда нибудь, я встречу проект, где без ORM никуда и осознаю весь дзен ORM.
        Пока что, по результатам статей и обсуждений на хабре, я вижу только примеры элементарных каких то случаев, где записали строку, считали строку из таблички. Иногда, о боже, даже из 2-3 таблиц. И делается вывод ORM это круто.
        Есть еще другая категория. Это Гуру.
        Они говорят о «протекающих слоях абстракций, фасадов, скаляров, синтаксическом сахаре и гидрации данных».
        Куда уж тут простым реляционным таблицам.
        Я, признаюсь, плохо отношусь к технологиям, которые нельзя объяснить простым языком.
        Как правило это трудно поддерживать.
        Особенно, если технологией владеет некий супер гуру, который может уйти в силу разных причин.
        Да, есть задачи, когда стоит требование использовать любую из СУБД, с возможностью быстрого переключения. Мой опыт говорит, что в таком случае неизбежны проблемы с производительностью.
        Т.е. вот так просто сделать такую фичу для сложного и объемного (по данным) проекта, совсем не просто.
        Допускаю, что это у меня такие проекты, когда даже владея знаниями по какой то одной СУБД, думаешь, как извернуться, что бы это вообще работало в приемлемые сроки, а еще если навесить сверху «слой абстракции», то будет вообще труба.
        Поэтому, мне кажется, что ORM это игрушка для студентов (не знающих SQL) и для любителей сложностей и «слоев абстракции».
        Но, время движется вперед.
        Дорогу новым технологиям и молодежи :)
        Благо работы пока хватает и «старикам».


        1. Fesor
          16.05.2017 23:49

          где без ORM никуда и осознаю весь дзен ORM.

          Мне кажется что весь конфликт в разном трактовании этих трех букв. Что вы подразумеваете под ORM? Ибо из того что вы пишите выглядит так как будто бы это некий монстр вроде Hybrenate который должен использоваться всегда и везде без компромиссно и без учета того что он как бы позволяет мэпить результаты SQL на объекты.


          Я, признаюсь, плохо отношусь к технологиям, которые нельзя объяснить простым языком.

          Есть хороший доклад Грэга Янга под названием "8 lines of code". Вам должно понравиться.


          1. VMichael
            17.05.2017 00:32

            Посмотрю на досуге, спасибо.


        1. VolCh
          17.05.2017 06:07
          +1

          Механизм ORM объясняется легко: объектная модель и реляционная не соответствуют друг другу и нужен механизм их отображения друг на друга, коль скоро принято решение использовать в приложение объектную модель и реляционное хранилище для неё. Неужели не встречали таких приложений в мире где ООП мэйнстрим для прикладной разработки, а SQL мэйнстрим для хранения структурированных данных?


        1. mayorovp
          17.05.2017 08:53

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


  1. alan008
    15.05.2017 23:50
    +1

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


    1. mayorovp
      16.05.2017 22:38
      +1

      Почему вы отбрасываете вариант "самому сразу написать на ORM как надо"? :-)


      1. alan008
        17.05.2017 09:21

        Если только для себя и только для конкретного проекта/задачи и эффект от этого понятен (упростится написание кода и т.п.), то можно. Но как только эту ORM кто-то другой попробует применить для своих целей, он столкнется с этими же проблемами. Т.к. "как надо" — это не какая-то математическая правильность, а лишь факт соответствия конкретной ситуации.


        1. mayorovp
          17.05.2017 09:26
          +3

          То же самое я могу сказать и вам.


          А это лишняя работа, проще самому сразу написать на SQL "как надо"

          Если только для себя и только для конкретного проекта/задачи и эффект от этого понятен (упростится написание кода и т.п.), то можно. Но как только этот запрос кто-то другой попробует применить для своих целей, он столкнется с этими же проблемами. Т.к. "как надо" — это не какая-то математическая правильность, а лишь факт соответствия конкретной ситуации.


        1. VolCh
          17.05.2017 09:52
          +1

          Наверное все популярные универсальные ORM-библиотеки появились из "для себя" и у них есть вполне определенная область применения для реализации как функциональных, так и нефункциональных (прежде всего ресурсных) требований по взаимодействию приложения с СУБД. Если кто-то упорно пихает ORM туда, где она даже себя не заявляет как решение, то кто в этом виноват?


  1. Pilat
    16.05.2017 04:31
    +1

    С ORM всё кажется красиво, пока есть четыре таблички. Как только их 400, сразу возникает проблема автоматизации генерации описания для ORM, потом оказывается что генератор делает не всё так как надо и после генерации приходится что-то поправлять, потом оказывается что всё-таки надо указывать 10 нужных столбцов из сотни в таблице, потом вдруг появляется какая-то непонятная ошибка, потом вдруг обнаруживаешь что ORM работает в 20 раз медленнее чем SQL, потом через пару лет оказывается что современныый обновлённый ORM совсем не так совместим со старым… и появляются мысли что зачем я вложился в этот ОРМ? Он требует знать всё об ОРМ, всё об SQL, а в качестве плюшек — только синтаксический сахар, так как обычный SQL на самом деле выдаёт точно такой же результат — как видно из примеров выше — и требует примерно столько же кода. Причём диалектов ОРМ много, а SQL один.


    Это я пишу глядя на свой код давности несколько лет, который пришло время корректировать, и на код напарника, у которого почему-то пять запросов Hibernate выполняет пол секунды, а казалось бы должно быть на пару порядков быстрее. И код которого я не могу проверить, так как не разбираюсь в достаточной мере в Hibernate. Код другого — который пишет на SQL — элементарно.


    1. VolCh
      16.05.2017 10:58

      а в качестве плюшек — только синтаксический сахар, так как обычный SQL на самом деле выдаёт точно такой же результат

      В качестве основной плюшки — маппинг объектов на базу и наоборот. И если на объекты с базы ещё можно как-то маппить 1:1 малой кровью чем-то вроде PDOStatement::fetchObject, то для сохранения объектов вам понадобится ORM, чтоб приложение было мало-мальски поддерживаемым.


      1. Pilat
        16.05.2017 12:29

        Вы в вышеприведённых примерах можете показать, в каком месте объекты мапятся, тем более двунаправленно? В каком из приведённых ОRM изменение объекта в базе тут же изменится в смапленном объекте? Можете представить, что в какой-то более-менее большой базе удастся сделать этот маппинг и приложение будет продолжать работать?


        1. VolCh
          16.05.2017 13:41
          +1

          В вызовах типа find/get (с базы на объекты) и save/persist/flush (с объектов на базу).


          О "тут же изменится" в целом речи нет, с одной стороны, с другой — в рамках ORM и сопутствующих технологий типа UoF, IdM и т. п. предполагается по умолчанию, что в базе не может быть изменений неинициированных ORM и каждая сессия работы СУБД выполняется в отдельной транзакции.


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


    1. mayorovp
      16.05.2017 22:48

      Не путайте объективные причины и субъективные. Вы не знаете Hibernate — потому у вас и ошибки непонятные, и код нельзя проверить. Я думаю, у вашего напарника такая же проблема с вашим кодом...


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


      1. Fesor
        16.05.2017 22:51

        только ORM и позволяет не сойти с ума.

        тут скорее разделение ответственности, но ORM как раз об этом.


  1. GrafDL
    16.05.2017 06:31

    Как-то написал что-то типа ORM для себя, потому что надоело писать сотни одинаковых запросов. Про то как оно назывется даже и не знал тогда, что вылилось в велосипед. Просто сделал класс, представляющий таблицу в базе, где экзепляр класса — это запись в таблице, со стандартными функциями получения списка/удаления/сохранения, а так же функции построения кусочков sql-запроса (типа where, order by), которые можно переопределить. Наследуясь от этого класса можно переопределить таблицу, особенности полей и добавить методы с какими-то нетипичными/оптимизированными запросами. А сам класс-предок использует позднее связывание и строит sql с учетом особенностей потомка. Для больших результатов делал ленивую подгрузку (доп. класс с интерфейсом массива). В итоге удобно получилось и намного меньше писанины. С голым SQL теперь сталкиваюсь только для необходимой оптимизации и сильно нестандартных запросов. В общем быстрее получается написать свой кустарный ORM, чем писать сотни однообразных SQL-запросов и методов.


    1. varanio
      16.05.2017 06:54

      > сотни однообразных SQL-запросов и методов.
      Я посмотрел запросы в одном большом старом проекте, и знаете, там нет однообразных SQL-запросов. Т.е. если мы берем список юзеров, то обязательно с какой-то статистикой и т.д. На 90% запросы кастомные и не очень простые


      1. GrafDL
        16.05.2017 07:30

        В общем конечно же зависит от проекта. У меня это были в основном CRUD с фильтрами. А для отчетов конечно же отдельные классы с SQL. Тут ORM лишнее, согласен.


      1. VolCh
        16.05.2017 11:01

        Результаты SELECT-запросов представлялись в виде объектов предметной области? UPDATE/INSERT-запросы зависели от таких объектов? Если да, то просто у вас была своя ORM, которая маппила объекты на SQL и обратно.


    1. VMichael
      16.05.2017 11:08

      сотни однообразных SQL-запросов и методов

      Что же за предметная область такая, в которой сотни! типов разных объектов описываются, да еще однообразных?


      1. GrafDL
        16.05.2017 13:05

        Cистема — что-то между CRM и 1С. Cущностей то около пол сотни. А вот запросов на каждую по несколько. Чтобы не писать генерацию SQL на каждый случай, зависящий от аргументов, я сделал один генератор SQL, который бы подходил в большинстве простых случаев.


  1. neoxack
    16.05.2017 11:28

    ORM-кам — нет, QueryBuilder-ам — да


    1. vlakarados
      16.05.2017 15:34

      По мне, так это как взять крутую и полезную штуку, которая имеет свои проблемы, выкинуть и оставить только проблемы.


  1. usharik
    16.05.2017 12:19

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


  1. asmm
    16.05.2017 12:59

    О подобном дискутировали с michael_vostrikov в моей статье «Реализация бизнес-логики в MySQL»
    https://habrahabr.ru/post/312134/#comment_9850218
    с примерами кода и оценкой производительности

    Проблема ОРМ что он пытается натянуть ООП парадигму на декларативный язык SQL. Естественно вся декларативность SQL теряется, отсюда и просадки производительности


    1. VolCh
      16.05.2017 13:43

      Скорее наоборот для большинства приложений: натянуть императивные команды SQL на декларативно описанный класс.


    1. michael_vostrikov
      16.05.2017 14:02

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


      1. VolCh
        16.05.2017 14:19

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


        1. Fesor
          16.05.2017 14:23

          Но нам так и так надо на выходе получить какой-то граф, нормализовать денормализованный результат SQL запроса. Будем мы гидрировать объекты сущности или использовать какие-то динамические структуры — это уже детали.


          1. VolCh
            16.05.2017 14:29

            Перефразирую: построение сложного графа может занимать значительно больше времени чем выполнение простого запроса :)


            1. michael_vostrikov
              16.05.2017 15:59

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


    1. Fesor
      16.05.2017 14:20

      Проблема ОРМ что он пытается натянуть ООП парадигму на декларативный язык SQL

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


      Как пример — например у нас есть данные и все хорошо. Но вот нам надо составить по этим данным граф связей кто с чем. Что проще:


      • извращаться с процедурами и SQL что бы добиться желаемого
      • добавить доменные ивенты в наши объекты по которым мы будем собирать проекцию данных в neo4j какой?


      1. asmm
        16.05.2017 15:01
        +1

        Сила СУБД как раз не просто в хранении данных, а именно в их обработке. И на мой взгляд, язык SQL идеально для этого подходит. Это как раз язык манипулирования и обработки данных, специально для этого созданный.
        Перекладывая функции СУБД в приложение посредством ОРМ, мы потенциально роем себе могилу.

        Про граф связей и в чём извращение с SQL не понял, но в БД для этого есть внешние ключи.


        1. Fesor
          16.05.2017 15:30

          Сила СУБД как раз не просто в хранении данных, а именно в их обработке.

          агрегации. С этим согласен. Все остальное, в том числе поддержание консистентности и т.д. тоже но только пока у нас данные влазят на одну машину. А далее это тупой сторадж. Вы можете использовать функционал СУБД для упрощения жизни, все что связано хранением данных. Конвертирование одного представления данных в другое например, вьюшки те же… но бизнес логике там не место.


          Перекладывая функции СУБД в приложение посредством ОРМ, мы потенциально роем себе могилу.

          Мы роем себе могилу когда доводим все до абсолюта. А так — все прекрасно. Юзаем ORM когда задача подходит под OLTP, и не юзаем когда не подходит.


          Про граф связей и в чём извращение с SQL не понял, но в БД для этого есть внешние ключи.

          У нас есть таблица с пользователями. Есть внешний ключ на introducer_id. Задача — вам нужно отобразить для анализа полное дерево рефералов. Количество уровней не ограничено.


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


  1. sayber
    16.05.2017 13:14

    Прям представил себе проект, где несколько десятков разработчиков, сотни сущностей и т.д. и т.п.
    Интересно будет искать SQL код, писать его и еще что бы все разработчики отлично знали SQL.
    Как по мне, ORM для больших проектов просто необходим. Собственно как мы и делаем.
    Программисту проще оперировать объектами чем запросами. А маппинг поможет в сложных ситуациях.


    1. Fesor
      16.05.2017 14:17

      Интересно будет искать SQL код

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


      Программисту проще оперировать объектами чем запросами.

      В каких-то случаях — да. А в каких-то декларативный SQL намного удобнее. Репорты — хороший пример.


      А маппинг поможет в сложных ситуациях.

      и будет вставлять палки в колеса если слишком универсальны и мы натыкаемся на специфику.


      1. sayber
        16.05.2017 14:41

        Да, про палки согласен.
        При реализации DDD/CQRS/CommanBus… etc. очень много времени ушло на доработку архитектуры по работе с маппингом/доктриной и всем слоем персистенций. Коммиты отправили им.
        В конечном счете оно того стоило.


        1. Fesor
          16.05.2017 15:36

          очень много времени ушло на доработку архитектуры по работе с маппингом/доктриной и всем слоем персистенций.

          Либо вы делали просто command bus (без CQRS) то скорее всего у вас могли быть проблемы со слоем персистентности. А с CQRS же мы бы имели чисто объектную модель которая вычисляет свой стэйт по ивентам (делаем мы event sourcing или нет — это детали, я сейчас в общем про eventual consistency). То есть в модели на запись у нас ORM по сути нет. Есть только ивенты.


          Далее эти ивенты должны ловиться отдельными штуками и писаться с тем представлением данных которое вам нужно для конкретной задачи. Иначе толку от CQRS нет.


          Но все же интересно что именно вы дорабатывали.


    1. usharik
      16.05.2017 22:40

      Вот именно с такими проектами я уже который год и сталкиваюсь, где десятки разработчиков, сотни сущностей, причем все разработчики хорошо знают SQL и пишут почти исключительно на нем. Вакансий программистов на T-SQL или PL/SQL сильно меньше со временем не становиться.


      1. VolCh
        17.05.2017 06:19
        +1

        А вакансий на программистов со знанием популярных ORM-библиотек становится все больше. Более того, эти знания подразумеваются, даже если ORM не упоминаются: пишут Symfony — имеют в виду Doctrine, пишут Laravel — имеют в виду Eloquent, пишут Yii — имеют в виду ActiveRecord, пишут Rails — имеют в виду ActiveRecord (но другой), пишут .Net — имеют в виду EntityFramework, пишут Spring — имеют в виду Hibernate и т. д. Да и просто, если упоминается какой-то ООП-язык типа PHP, Ruby, Java, C#, C++ и какая-то SQL-база в одной вакансии, то чаще всего имеется в виду, что соискатель должен уметь отображать объектную модель на реляционную и обратно, а не редко и без популярных универсальных ORM-библиотек, то есть должен уметь написать свой ORM.


  1. mad_nazgul
    16.05.2017 13:56

    ORM — зло, т.к. пытается «впихнуть не впихуемое».
    Т.е. создать «универсальный преобразователь» из РМД в ООМД.
    Пока модели простые, все замечательно, по мере усложнения модели начинают вылазить не стыковки.
    Т.к. бизнес сущность обычно не может быть выражена один в один в виде таблицы РМД.


    1. Fesor
      16.05.2017 14:14

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


      1. VolCh
        16.05.2017 14:33

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


        1. Fesor
          16.05.2017 15:36

          Согласен.


    1. VolCh
      16.05.2017 14:23
      +1

      Т.к. бизнес сущность обычно не может быть выражена один в один в виде таблицы РМД.

      Для этого и нужны, прежде всего, ORM, чтобы выражать не один в один. Использовать универсальный преобразователь as is, расширять его для частных случаев или писать свой неуниверсальный — это вопрос выбора преобразователя, а не вопрос его необходимости. Необходим он стал, когда вы выбрали использовать РМД и ООМД не просто одновременно, а отображая друг в друга.


  1. yurybykov
    16.05.2017 18:41

    ORM и SQL это разные инструменты для работы с данными. Соответственно, применять их надо по назначению. Строить отчёты и делать выборки, это задача SQL, для этого он разработан. ORM нужен для записи и DDD.
    Read model — SQL, write model — ORM.


  1. Dewid
    16.05.2017 18:41
    -1

    Мне кажется автор, не много напутал в понятиях. Тут же не используется голый sql а все примеры на pdo и специфичных ORM для каждого фреймворка. Он не делает escape_string, и прочее он все те же самые функции которые делает orm для каждой специфичной базы пусть то Oracle, Postgreql переложил на PDO. ГДЕ ТУТ ГОЛЫЕ ЗАПРОСЫ SQL. От куда беруться такие умники.


    1. VolCh
      16.05.2017 19:05
      +1

      PDO работает с голыми SQL-запросами. Он не модифицирует запросы под базу, только предоставляет единый API для разных баз.


  1. GrafDL
    16.05.2017 19:39

    Вся беда возникает, когда инструмент используют не только когда он что-то облегчает, а для всего подряд, т.е. пытаются сделать из конкретной тулзы инструмент на все случаи жизни. А потом идут длинные споры про то что инструмент не идеален, потому что он может не всё на свете. Так не бывает. Автоматизация всегда подразумевает какие-то ограничения. Инструмент применяют там, где он удобен. Остальное — уже какое-то извращение.


  1. michael_vostrikov
    16.05.2017 21:35
    +1

    Примеры какие-то немного надуманные. "Допустим, стоит задача обновить двум последним авторам имя на "Жорж"". Часто вы так делаете в рабочих приложениях?


    Можно другие примеры рассмотреть.


    // ------
    
    select id, name, field1, field2, field3, field4 ...
    from books
    inner join author_book ab on b.id = ab.book_id
    inner join authors a on ab.author_id = a.id
    where <фильтр по authors>
    
    Book::find()->joinWith('authorBook')->joinWith('authorBook.author')->where(<фильтр по authors>)
    
    // ------
    
    select id from books where id = X
    // показать 404 если не найдено
    // $queryString = <куча конкатенаций>;
    update books set id = ..., name = ..., field1 = ..., field2 = ..., field3 = ..., field4 = ... where id = X
    
    $book = Book::find()
    // показать 404 если не найдено
    $book->load($data);
    $book->save();
    
    // ------

    Особенно весело становится, когда меняется название поля в джойне или добавляется поле в таблицу.
    Джойн надо менять во всех отчетах, список полей в создании, обновлении, и части выборок.
    С использованием ORM мы меняем связанную с таблицей сущность, с использованием SQL весь использующий ее код.


  1. AlexTheLost
    17.05.2017 19:05

    Есть ещё одно решение по мимо ORM, не использовать ОО языки или просто ОО возможности — тогда оно (ORM) не нужно. Просто работаете со списками, отображениями, векторами.
    Собственно ими(списками, отображениями, векторами) и представлены различные структуры в большинстве БД(как в реляционных так и nosql) и протоколы передачи данных(например JSON).


    1. Fesor
      17.05.2017 19:19

      то есть полностью отказаться от преимуществ actor model и начать обмазываться монадами?


      1. AlexTheLost
        18.05.2017 21:55

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


        1. Fesor
          19.05.2017 00:31
          +1

          Она хоть и имеет общие с ООП корни но различий очень много

          Ну как вам сказать, если мы пороемся и вспомним что подразумевалось под термином ООП (message passing, late binding) то как бы мы будем иметь просто определение actor model. Так что "общие корни" это мягко сказано.


          хотя бы начиная с того что акторы обмениваются сообщениями(асинхронно) а объекты вызывают методы друг у друга(синхронно).

          Да, сами акторы по хорошему существуют независимо друг от друга, но это совершенно не значит что они не ожидают ответа на свои сообщения. А вот блокировать им свое выполнение или нет — решать только им. Как никак event loop и все такое это не такая уж и редкость.


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


          акторы это принципиально более высокий уровень абстракции

          Примерно тот же уровень абстракции что и "все есть объект". То есть по сути никакой конкретики.


          А тема о SQL vs ORM.

          Нам удобно представлять систему как объекты и процессы (их можно функциями делать), а SQL это лишь декларативный способ работы с данными. То есть вот этот вот vs надо заменить на with и все счастливы.


    1. michael_vostrikov
      17.05.2017 21:45

      Тащить низкоуровневую реализацию в приложение? Не, спасибо)


    1. VolCh
      17.05.2017 21:49
      +1

      Тогда нужны другие *RM, потому что в СУБД данные представляются в виде отношений и кортежей.


      1. AlexTheLost
        18.05.2017 21:45
        -1

        Вижу в вашем возражении проблему в том что вы все же думаете в рамках ORM.
        SQL запрос возвращает вам именно коттедж или их список а не граф. Что собственно имеет нативную поддержку во многих не ООП языках — другими словами тот же список кортежей.
        Если вы хотите автоматического сохранения и другой обработки связанного графа сущностей как вам предлагаю некоторые ORM тогда да вам что-то близкое к просто SQL не подойдет. На моей практике данные встроенные возможности либо не эффективно работали и подходили для ограниченного набора задач или требовали написание обвязок на SQL-схожем языке для конкретной ORM, что сложнее обычного SQL запроса, который к тому же элементарно при написании проверить, минуточку тем же запросом в БД и не нужно писать сложные интеграционные тесты с моками (учтите ещё что под каждую ORM нужно изучать особенности её работы и ещё языка запросов).
        А можно просто написать ещё ещё один sql запрос или объединить несколько уже существующих в транзакцию.


        1. VolCh
          19.05.2017 13:31

          Я думаю в разных рамках. При фразе о "не ООП языках" я думаю, прежде всего, о языках типа C. И писал (да и пишу) достаточно сложную логикe на голом SQL как раз из-за неэффективности ORM на больших массивах данных.


        1. michael_vostrikov
          19.05.2017 15:33
          +2

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


          Вообще, одна из проблем SQL на мой взгляд в том, что оно хоть и относится к реляционным базам данным, но нормально работать с этими реляциями не позволяет. Только при создании таблицы можно указать, что у нас есть связь с вон той. А данные по этой связи в запросе достать не получится (строка из другой таблицы как значение поля), и даже join по foreign key сделать нельзя. ORM восполняет эту часть, задавая связи между объектами.


          1. VolCh
            19.05.2017 18:38
            +1

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


            1. michael_vostrikov
              19.05.2017 20:34

              Хм, да, многозначное слово. Я имел в виду выражения вида "one-to-many relationship".


              1. VolCh
                20.05.2017 13:57
                +1

                А реляционные СУБД от relation в смысле relational algebra, которая определяет такие операции как джойн над реляциями, в "простонародье" таблицами. One-to-many relationship к реляционным СУБД напрямую не относится, всякие явно и неявно заданные relationship — это семантика, которой мы описываем схему базы, но в самой СУБД её нет. ORM, кстати, один из инструментов такого наполнения, с помощью которого мы описываем, что таблица такая-то содержит данные объектов такого-то класса, вторая таблица — второго класса, а эти классы (а значит и таблицы) связаны один-ко-многим через такие-то поля. foreign key — это лишь инструмент обеспечения целостности, защита от кривых рук разработчика или пользователя, а не инструмент задания связей.


                1. michael_vostrikov
                  20.05.2017 14:38

                  Ну насчет ORM я примерно о том же. Только foreign key это именно что связь между данными. Первичный ключ это ссылка, обозначающая объект. Целостность это значит, что мы не можем ссылаться на объект, которого нет в другой таблице. Зачем бы нам нужна была целостность, если нет связи.


                  Про relation согласен, я неточно сказал. Но связь между данными это следствие декомпозиции, а значит напрямую относится к базам данных. Скажем так, SQL позволяет нормально работать с relations, но не с relationships.


                  1. VolCh
                    20.05.2017 15:12

                    "Ссылка, обозначающая объект" — это уже вкладываемая нами семантика, для базы это значение, идентифицирующее строку в таблице. То же с внешними ключами — ограничение foreign key лишь указывает базе данных, что значение в одном столбце одной таблицы должно быть из множества значений другого столбца другой (иногда этой же) таблицы и ничего более, что-то вроде динамического enum без всякой семантики связей, связь у нас в голове или в файле с маппингом ORM. Связь описывается в терминах базы данных, но сама база о наличии связи "не подозревает", она просто не оперирует подобными терминами.


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


                    1. michael_vostrikov
                      20.05.2017 16:50

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


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


  1. Ryppka
    20.05.2017 15:19

    Вот было же уже сказано, что для сколько-нибудь нетривиальных ситуаций <a href=«http://blogs.tedneward.com/post/the-vietnam-of-computer-science/>ORM не годится, исходя из общий соображений. Для бесхитростного CRUD'а — вполне. Остальное — перемалывание воды в ступе. Несвежей воды…


    1. VolCh
      20.05.2017 17:31
      +1

      Скорее для CRUD как раз ORM вещь избыточная. А вот если надо прочитать объект из базы со всеми связями (желательно лениво в общем случае), дернуть его метод, изменяющий состояние его и некоторых его связей, а потом сохранить всё изменённое одной транзакцией, то тут без ORM (универсальной библиотеки) плохо. В лучшем случае много аккуратной работы.