Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save codedokode/c4cbc4d7dc8e45ea074a to your computer and use it in GitHub Desktop.
Save codedokode/c4cbc4d7dc8e45ea074a to your computer and use it in GitHub Desktop.

Revisions

  1. codedokode revised this gist Nov 10, 2016. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,7 @@
    Это старая версия урока, которая больше не обновляется. Новая версия расположения тут: https://github.com/codedokode/pasta/blob/master/db/patterns-oop.md

    ------------------------

    # Паттерны работы с базой данных

    Разберемся, как правильно с применением ООП сохранять и загружать данные из базы. Существуют такие подходы:
  2. codedokode revised this gist Dec 27, 2014. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -127,7 +127,7 @@ $newNews = new News($pdo);
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД. После нее поля id и date заполняются автоматически
    $newNews->save($newNews);
    $newNews->save();
    ```

    Такой подход используется, например в Yii 1: http://www.yiiframework.com/doc/guide/1.1/ru/database.ar
    @@ -168,7 +168,7 @@ $newNews = new News();
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД
    $mapper->save();
    $mapper->save($newNews);
    ```

    Этот подход используется в ORM Doctrine2: http://odiszapc.ru/doctrine/ Только там Mapper называется Repository.
  3. codedokode revised this gist Dec 27, 2014. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -116,7 +116,7 @@ $news = new News($pdo); // в некоторых фреймворках пере
    // возвращает массив объектов-новостей
    $lastestNews = $news->findLatestNews();

    // возвращает новость с номером 10
    // возвращает новость с id = 10
    $someNews = $news->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    @@ -127,7 +127,7 @@ $newNews = new News($pdo);
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД. После нее поля id и date заполняются автоматически
    $newNews->save();
    $newNews->save($newNews);
    ```

    Такой подход используется, например в Yii 1: http://www.yiiframework.com/doc/guide/1.1/ru/database.ar
  4. codedokode revised this gist Oct 31, 2014. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -179,6 +179,8 @@ $mapper->save();

    ## Doctrine 2

    Doctrine 2 — это библиотека реализующая паттерн DataMapper. Ты просто добавляешь в свои сущности аннотации, задающие соответствие полей объектов и полей в базе данных, а Doctrine дает тебе классы-репозитории, которые позволяют загружать и сохранять твои объекты в базу данных. Doctrine 2 — очень мощная и популярная, хотя и непростая для начинающего, библиотека. Чтобы с ней работать, надо понимать саму идею ORM, паттерны UnitOfWork и IdentityMap. И придется много читать мануал по ней.

    Напишу еще несколько вещей, которые мы не рассмотрели, но которые есть в больших ORM вроде Doctrine 2:

    - свой язык запросов DQL, похожий на SQL
  5. codedokode revised this gist Oct 16, 2014. 1 changed file with 1 addition and 0 deletions.
    Original file line number Diff line number Diff line change
    @@ -131,6 +131,7 @@ $newNews->save();
    ```

    Такой подход используется, например в Yii 1: http://www.yiiframework.com/doc/guide/1.1/ru/database.ar

    Также он использовался в Doctrine 1.

    Этот подход относительно прост, но он имеет недостаток: мы смешиваем бизнес-логику (методы работы со свойствами новости) и работу с БД в одном классе. Хотя объект-новость вполе может сущестовать и сам по себе. Для решения этой проблемы нам нужен DataMapper.
  6. codedokode revised this gist Oct 16, 2014. 1 changed file with 27 additions and 21 deletions.
    48 changes: 27 additions & 21 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -139,30 +139,36 @@ $newNews->save();

    В DataMapper мы выносим код сохранения/загрузки сущностей (и все что связано с базой данных) в отдельный класс. Вот пример такого класса:

    class NewsMapper
    {
    ....
    public function save(News $news) { ... }
    public function getById($id) { ... }
    public function findLatestNews() { ... }
    }
    ```php
    <?php
    class NewsMapper
    {
    ....
    public function save(News $news) { ... }
    public function getById($id) { ... }
    public function findLatestNews() { ... }
    }
    ```

    И вот пример использования:

    $mapper = new NewsMapper($pdo);
    // Поиск новости по id
    $someNews = $mapper->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    // Обновляем запись в БД
    $mapper->save($someNews);

    // создание новой
    $newNews = new News();
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД
    $mapper->save();
    ```php
    <?php
    $mapper = new NewsMapper($pdo);
    // Поиск новости по id
    $someNews = $mapper->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    // Обновляем запись в БД
    $mapper->save($someNews);

    // создание новой
    $newNews = new News();
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД
    $mapper->save();
    ```

    Этот подход используется в ORM Doctrine2: http://odiszapc.ru/doctrine/ Только там Mapper называется Repository.

  7. codedokode revised this gist Oct 16, 2014. 1 changed file with 82 additions and 72 deletions.
    154 changes: 82 additions & 72 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -28,41 +28,44 @@ $news = $query->fetchAll(); // получаем массив массивов

    Остальные подходы подразумевают создание класса-сущности (entity), который представляет собой одну запись в таблице. Например, для работы с таблицей новостей мы можем создать сущность `News`, представляющую собой одну новость:

    class News
    ```php
    <?php
    class News
    {
    public $id;
    public $title;
    public $text;
    public $categId;
    /**
    * Дата в виде объекта DateTime
    */
    public $date;

    /**
    * Возвращает возраст новости в днях
    */
    public function getAgeDays()
    {
    public $id;
    public $title;
    public $text;
    public $categId;
    /**
    * Дата в виде объекта DateTime
    */
    public $date;
    /**
    * Возвращает возраст новости в днях
    */
    public function getAgeDays()
    {
    // Находим разницу между сегодня и датой публикации
    $today = new DateTime();
    $interval = $today->diff($this->date);
    return $interval->d;
    // Находим разницу между сегодня и датой публикации
    $today = new DateTime();
    $interval = $today->diff($this->date);
    return $interval->d;
    }
    /**
    * Проверяет все ли поля заполнены перед вставкой в БД
    */
    public function validate(ErrorList $errors)
    {
    if (!$this->title) {
    $errors->add('title', 'Необходимо указать название новости');
    }
    /**
    * Проверяет все ли поля заполнены перед вставкой в БД
    */
    public function validate(ErrorList $errors)
    {
    if (!$this->title) {
    $errors->add('title', 'Необходимо указать название новости');
    }
    if (!$this->text) {
    $errors->add('text', 'Необходимо заполнить текст новости');
    }

    if (!$this->text) {
    $errors->add('text', 'Необходимо заполнить текст новости');
    }
    }
    }
    ```

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

    @@ -72,53 +75,60 @@ $news = $query->fetchAll(); // получаем массив массивов

    Это более простой способ. При его использовании методы для сохранения/загрузки сущности из БД добавляются прямо в нее. Чтобы не копипастить их в каждый класс, их обычно добавляют в базовый класс, а сущность наследуют от него. При этом обычно в сущности делается метод, возвращающий информацию о соответствии полей объекта таблице и полям в базе данных (чтобы можно было правильно составить SQL запрос):

    class News extends ActiveRecordBase
    ```php
    <?php
    class News extends ActiveRecordBase
    {
    .....

    protected function getTableName()
    {
    .....
    protected function getTableName()
    {
    return 'news'; // имя таблицы с новостями
    }
    protected function getFields()
    {
    // список полей, которые отображаются на таблицу
    return array('id', 'title', 'text', 'date', 'categId');
    }
    /**
    * Вызывается перед вставкой в таблицу
    */
    protected function beforeInsert()
    {
    if (!$this->date) {
    // ставим дату создания если не задана
    $this->date = new DateTime();
    }
    return 'news'; // имя таблицы с новостями
    }

    protected function getFields()
    {
    // список полей, которые отображаются на таблицу
    return array('id', 'title', 'text', 'date', 'categId');
    }

    /**
    * Вызывается перед вставкой в таблицу
    */
    protected function beforeInsert()
    {
    if (!$this->date) {
    // ставим дату создания если не задана
    $this->date = new DateTime();
    }
    }
    }
    ```

    Соответственно, вот как выглядит пример поиска записей, вставки и обновления записи:

    $news = new News($pdo); // в некоторых фреймворках передавать объект БД не надо −
    // сущность сама берет объект откуда-нибудь

    // возвращает массив объектов-новостей
    $lastestNews = $news->findLatestNews();

    // возвращает новость с номером 10
    $someNews = $news->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    // Обновляем запись в БД
    $someNews->save();

    $newNews = new News($pdo);
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД. После нее поля id и date заполняются автоматически
    $newNews->save();
    ```php
    <?php

    $news = new News($pdo); // в некоторых фреймворках передавать объект БД не надо −
    // сущность сама берет объект откуда-нибудь

    // возвращает массив объектов-новостей
    $lastestNews = $news->findLatestNews();

    // возвращает новость с номером 10
    $someNews = $news->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    // Обновляем запись в БД
    $someNews->save();

    $newNews = new News($pdo);
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД. После нее поля id и date заполняются автоматически
    $newNews->save();
    ```

    Такой подход используется, например в Yii 1: http://www.yiiframework.com/doc/guide/1.1/ru/database.ar
    Также он использовался в Doctrine 1.
  8. codedokode revised this gist Oct 16, 2014. 1 changed file with 1 addition and 0 deletions.
    Original file line number Diff line number Diff line change
    @@ -12,6 +12,7 @@
    В этом варианте мы не используем никаких классов, а просто загружаем/вставляем данные в базу с использованием PDO:

    ```php
    <?php
    $query = $pdo->prepare("SELECT * FROM news WHERE categId = :categId LIMIT 20");
    $query->execute(array(':categId' => $categId));
    $news = $query->fetchAll(); // получаем массив массивов
  9. codedokode revised this gist Oct 16, 2014. 1 changed file with 5 additions and 3 deletions.
    8 changes: 5 additions & 3 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -11,9 +11,11 @@

    В этом варианте мы не используем никаких классов, а просто загружаем/вставляем данные в базу с использованием PDO:

    $query = $pdo->prepare("SELECT * FROM news WHERE categId = :categId LIMIT 20");
    $query->execute(array(':categId' => $categId));
    $news = $query->fetchAll(); // получаем массив массивов
    ```php
    $query = $pdo->prepare("SELECT * FROM news WHERE categId = :categId LIMIT 20");
    $query->execute(array(':categId' => $categId));
    $news = $query->fetchAll(); // получаем массив массивов
    ```

    Этот способ не требует создания никаких классов, но он очень ограничен и ведет к плохому коду если в нашем приложении больше 1-2 таблиц: ты начнешь путаться, где какой массив и какие у него поля. В общем, с таким подходом ничего хорошего нам не светит.

  10. codedokode revised this gist Oct 16, 2014. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -63,7 +63,7 @@

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

    Загрузка и сохранение сущностей в SQL базу данных еще называется ORM (Object-Relational Mapping). ORM пытаются избавить нас от необходимости писать однотипные примитивные SQL запросы, позволяя нам работать на более высоком уровне.
    Код, реализующий загрузку и сохранение сущностей в SQL базу данных еще называется ORM (Object-Relational Mapper). ORM пытаются избавить нас от необходимости писать однотипные примитивные SQL запросы, позволяя нам работать на более высоком уровне.

    ## ActiveRecord

  11. codedokode created this gist Oct 16, 2014.
    172 changes: 172 additions & 0 deletions Паттерны работы с базой данных.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,172 @@
    # Паттерны работы с базой данных

    Разберемся, как правильно с применением ООП сохранять и загружать данные из базы. Существуют такие подходы:

    - примитивный подход с использованием PDO/массивов
    - ActiveRecord http://design-pattern.ru/patterns/active-record.html
    - DataMapper http://design-pattern.ru/patterns/data-mapper.html
    - TableDataGateway http://design-pattern.ru/patterns/table-data-gateway.html

    ## Примитивный подход

    В этом варианте мы не используем никаких классов, а просто загружаем/вставляем данные в базу с использованием PDO:

    $query = $pdo->prepare("SELECT * FROM news WHERE categId = :categId LIMIT 20");
    $query->execute(array(':categId' => $categId));
    $news = $query->fetchAll(); // получаем массив массивов

    Этот способ не требует создания никаких классов, но он очень ограничен и ведет к плохому коду если в нашем приложении больше 1-2 таблиц: ты начнешь путаться, где какой массив и какие у него поля. В общем, с таким подходом ничего хорошего нам не светит.

    Однако этот подход наиболее эффективен при работе с огромным количеством записей.

    Такой подход например поддерживается классом `Zend_Db_Select` в Zend Framework 1: http://framework.zend.com/manual/1.12/ru/zend.db.select.html

    ## Сущности

    Остальные подходы подразумевают создание класса-сущности (entity), который представляет собой одну запись в таблице. Например, для работы с таблицей новостей мы можем создать сущность `News`, представляющую собой одну новость:

    class News
    {
    public $id;
    public $title;
    public $text;
    public $categId;
    /**
    * Дата в виде объекта DateTime
    */
    public $date;
    /**
    * Возвращает возраст новости в днях
    */
    public function getAgeDays()
    {
    // Находим разницу между сегодня и датой публикации
    $today = new DateTime();
    $interval = $today->diff($this->date);
    return $interval->d;
    }
    /**
    * Проверяет все ли поля заполнены перед вставкой в БД
    */
    public function validate(ErrorList $errors)
    {
    if (!$this->title) {
    $errors->add('title', 'Необходимо указать название новости');
    }
    if (!$this->text) {
    $errors->add('text', 'Необходимо заполнить текст новости');
    }
    }
    }

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

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

    ## ActiveRecord

    Это более простой способ. При его использовании методы для сохранения/загрузки сущности из БД добавляются прямо в нее. Чтобы не копипастить их в каждый класс, их обычно добавляют в базовый класс, а сущность наследуют от него. При этом обычно в сущности делается метод, возвращающий информацию о соответствии полей объекта таблице и полям в базе данных (чтобы можно было правильно составить SQL запрос):

    class News extends ActiveRecordBase
    {
    .....
    protected function getTableName()
    {
    return 'news'; // имя таблицы с новостями
    }
    protected function getFields()
    {
    // список полей, которые отображаются на таблицу
    return array('id', 'title', 'text', 'date', 'categId');
    }
    /**
    * Вызывается перед вставкой в таблицу
    */
    protected function beforeInsert()
    {
    if (!$this->date) {
    // ставим дату создания если не задана
    $this->date = new DateTime();
    }
    }
    }

    Соответственно, вот как выглядит пример поиска записей, вставки и обновления записи:

    $news = new News($pdo); // в некоторых фреймворках передавать объект БД не надо −
    // сущность сама берет объект откуда-нибудь

    // возвращает массив объектов-новостей
    $lastestNews = $news->findLatestNews();

    // возвращает новость с номером 10
    $someNews = $news->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    // Обновляем запись в БД
    $someNews->save();

    $newNews = new News($pdo);
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД. После нее поля id и date заполняются автоматически
    $newNews->save();

    Такой подход используется, например в Yii 1: http://www.yiiframework.com/doc/guide/1.1/ru/database.ar
    Также он использовался в Doctrine 1.

    Этот подход относительно прост, но он имеет недостаток: мы смешиваем бизнес-логику (методы работы со свойствами новости) и работу с БД в одном классе. Хотя объект-новость вполе может сущестовать и сам по себе. Для решения этой проблемы нам нужен DataMapper.

    ## DataMapper

    В DataMapper мы выносим код сохранения/загрузки сущностей (и все что связано с базой данных) в отдельный класс. Вот пример такого класса:

    class NewsMapper
    {
    ....
    public function save(News $news) { ... }
    public function getById($id) { ... }
    public function findLatestNews() { ... }
    }

    И вот пример использования:

    $mapper = new NewsMapper($pdo);
    // Поиск новости по id
    $someNews = $mapper->getById(10);
    // меняем название
    $someNews->title = 'Новое название';
    // Обновляем запись в БД
    $mapper->save($someNews);

    // создание новой
    $newNews = new News();
    $newNews->title = 'Сенсация!';
    $newNews->text = 'Текст новости';
    // вставка в БД
    $mapper->save();

    Этот подход используется в ORM Doctrine2: http://odiszapc.ru/doctrine/ Только там Mapper называется Repository.

    ## TableDataGateway

    Это что-то напоминающее DataMapper, но он может быть реализован без объектов-сущностей. Например, в ZF есть `Zend_Db_Table` который его реализует: http://framework.zend.com/manual/1.12/ru/zend.db.table.html — там результаты возвращаются в виде объектов класса `Zend_Db_Table_Row`. Соответственно, для любых сущностей используется один и тот же класс и это сильно напоминает подход с массивами.

    ## Doctrine 2

    Напишу еще несколько вещей, которые мы не рассмотрели, но которые есть в больших ORM вроде Doctrine 2:

    - свой язык запросов DQL, похожий на SQL
    - описание через конфиг или аннотации: ты можешь с помощью специальных комментариев-аннотаций указать, как поля объекта связаны с полями в таблице: http://odiszapc.ru/doctrine/basic_mapping/
    - IdentityMap ( http://design-pattern.ru/patterns/identity-map.html ): если ты повторно выбираешь ту же самую сущность из базы, тебе возвращается ссылка на существующую сущность. Доктрина следит чтобы каждая сущность существовала ровно в одном экземпляре, и это помогает избежать противоречий когда есть несколько экземпляров и непонятно в каком из них актуальные данные
    - UnitOfWork ( http://design-pattern.ru/patterns/unit-of-work.html ): когда ты делаешь изменения в сущностях, они не сохраняются автоматически. Ты должен явно вызвать метод flush() и тогда EntityManager найдет все изменившиеся, новые и удаленные сущности и соответственно обновит/вставит/удалит записи в базе одной транзакцией.
    - работа с ассоциациями (связями). Например, Новость может относиться к Категории и быть помечена Тегами, а также под ней могут быть оставлены Комментарии (у которых в свою очередь есть Авторы). При этом если мы должны иметь возможность создавать такие связи и разрывать их. Представь, как сложно такое реализовать самому (трудно представить? попробуй напиши код).

    Doctrine 2 не требует от тебя унаследовать класс-сущность от какого-то базового класса, он позволяет связать любой класс с базой данных — главное чтобы в нем были методы get../set.. для чтения и записи полей. Также, придется потратить время на то, чтобы разобраться, как правильно использовать этот ORM и как настроить в нем кеширование метаданных, чтобы он работал с приемлемой скоростью.

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