Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 45 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save codedokode/c4cbc4d7dc8e45ea074a to your computer and use it in GitHub Desktop.
Save codedokode/c4cbc4d7dc8e45ea074a to your computer and use it in GitHub Desktop.
Паттерны работы с базой данных

Это старая версия урока, которая больше не обновляется. Новая версия расположения тут: https://github.com/codedokode/pasta/blob/master/db/patterns-oop.md


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

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

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

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

<?php
$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, представляющую собой одну новость:

<?php
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 Mapper). ORM пытаются избавить нас от необходимости писать однотипные примитивные SQL запросы, позволяя нам работать на более высоком уровне.

ActiveRecord

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

<?php
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(); 
        }
    }
}

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

<?php

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

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

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

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

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

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

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

Этот подход используется в 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

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

Напишу еще несколько вещей, которые мы не рассмотрели, но которые есть в больших 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 придется отказаться.

@dmitryshelomanov
Copy link

а eloquent ?

@connorholt
Copy link

eloquent it is active record pattern

@yawa20
Copy link

yawa20 commented Apr 26, 2018

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

@BerezhniyDmitro
Copy link

По описанию, не понятно вобще чем AR от DM отличается равно как и по кускам кода.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment