Оргенизация слоя хранения данных это всегда не простая задача, с кучей кейсов, вроде блокировки перезаписи, транзакции, сохранение целостности данных. В какой-то момент появились СУБД и обещали решить эти проблемы, но с ними появились новые - как нам работать с состоянием, которое мы храним?
Существует 4 основных подхода для организации рабоыт с базой данных, Table Gateway, Row Data Gateway, Active Record и Data Mapper. Все эти подходы объеденяет то, что они скрывают от нас базу данных и нюансы работы с ними (в частности SQL). На сегодняшний день самым популярным подходом являются Active Record и Data Mapper, все о них слышали, но для того что бы более полно представлять, как развивалась идея, стоит рассказать и о первых двух.
Table Data Gateway, который так же можно встретить под названием DAO (Data Access Object), предоставляет нам объектное представление отдельных таблиц, с доступом ко всем рядам оных. Его основное предназначение - спрятать SQL как деталь реализации, предоставиви простой CRUD интерфейс для работы с таблицами в соответствии с нуждами приложения. Рассмотрим на примере (для удобства я заменил обычный SQL на Database компонент из Laravel с целью использовать тамошний query builder)
class UserTableGateway {
private $table;
public function __construct() {
$this->table = DB::table('users');
}
public function insert($name, $email, $password) {
$this->table->insert(compact('name', 'email', 'password'));
}
// пароль должен обновляться отдельно от профиля
public function updateProfile($id, $name, $email) {
$this->table->where('id', $id)->update(compact('name', 'email'));
}
public function findCoolestUser() {
return $this->table->orderBy('rating', 'DESC')->first(); //
}
}
$usersTable = new UserTableGateway();
$usersTable->insert('Sergey', '[email protected]`, password_hash('somepassword', PASSWORD_DEFAULT));
$user = $usersTable->findCoolestUser(); // вернет нам один ряд
Основная идея тут - держать все что относится к формированию запросов в одном месте, что бы наш SQL, query builder, или же PDO, не расползался по системе. Не изолировать код от хранилища, а предоставить удобный способ взаимодействия с ним. Возможность инкапсулировать различные запросы в рамках этого подхода позволяет сильно уменьшить вероятность ошибок. Однако работать с "сырыми" данными не очень удобно, обычно мы все же хотим работать со всеми данными как с объектами.
Можно было бы конечно сделать объект, и держать логику хранения данных внутри. Но в таком случае у нас наши сущности будут зависеть от базы данных, и их будет не так уж легко тестировать. Тесты будут требовать подключения к базе данных и от того станут медленными.
Для решения этой проблемы, нам нужна прослойка между сущностью и базой данных. Row Data Gateway. При этом подходе мы проэцируем отдельные ряды нашей таблицы на объекты, которые служат промежуточным звеном и инкпсулируют все детали о том как сохраняется информация в себе. В итоге наши сущности могут работать через этот gateway используя штатные средства языка программирования.
class UserGateway {
// сделав свойства публичным я хочу подчеркнуть то,
// что этот класс это просто структура данных, состояние без поведения
private $id;
public $name;
public $email;
public $password;
// еще раз, это проекция таблицы, а не объект бизнес логики
public function __constructor(string $id = null) {
if ($id) {
$this->id = $id;
$this->fetchRow();
} else {
// если ID не задан, то мы генерируем ноывй
$this->id = Cuid::cuid();
}
}
public function insert() {
DB::table($this->tableName)->insert($this->getAttributes());
}
public function update() {
DB::table($this->tableName)->where('id', $this->id)->update($this->getAttributes());
}
private function fetchRow() {
$row = DB::where('id', $this->id')->first();
if (!$row) {
throw new \UnableToCreateGatewayException('');
}
}
private function getAttributes() {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'password' => $this->password,
];
}
}
class User {
private $gateway;
public function __construct(UserGateway $gateway) {
$this->gateway = $gateway;
}
public function save() {
}
}
Отдельно стоит заметить, что поскольку Row Data Gateway представляет собой отдельную строку таблицы, для выборки нужных рядов нам нужно уже делать отдельный компонент Finder.
Как вы можете видеть, с данным подходом мы уже можем добавлять поведение для сущности User
, работая изнутри с UserGateway
Active Record - это Row Data Gateway, к которому добавили логику предметной области. Дописать...
AR и Row Data Gateway не очень удобно тестировать. Нам хочется полной независимости от базы данных.
TODO
По сути мы работаем с объектами нашей базы данных как с обычными объектами. Разлица лишь в том, что каждый способ предоставляет больше возможностей для изоляции от хранилища. Table Data Gateway заставляет нас работать со всей таблицей целиком, Row Data Gateway - с отдельными рядами таблицы. Active Record является развитием идеи Row Data Gateway но уже позволяет инкапсулировать дополнительное поведение. Data Mapper же вообще не приязан к структуре базы данных, и, так же как и в случае с AR, мы можем примешивать объектам поведение.
Поскольку первые два подхода практически не используются, рассмотрим Active Record и Data Mapper. В чем координальное отличие?
В рамках Active Record (или Active State) мы работаем с объектами как отображением элементов нашей базы данных, как если бы у нас был прямой доступ к ним без SQL прослойки. В DataMapper мы работаем исключительно с нашими объектами, которые лежат в памяти, и просим отдельную штуку (мэппер) что бы тот синхронизировал состояние объектов в памяти и в базе (сохранил состояние по сути).
Доустим у нас есть некий граф объектов, с которым мы работаем в рамках бизнес транзакции. И мы должны сохранить изменения в базу, то есть объект на верху графа и все связанные с ним сущности. Если руководствоваться паттерном "Информационный эксперт", заниматься этим должен тот, кто знает как это делать. В случае AR эта логика выносится прямо в сущности и мы явно задаем в каком порядке что сохранять. В случае же с DM у сущности нет таких знаний и мы выделяем все в отдельный компонент, что дает нам дополнительные варианты как это организовать удобнее для нас. От тупого разруливания графа "руками", до алгоритмов, которые разруливают это автоматически.
Как доставать объекты из хранилища - это отдельная темам для разговора, и тот и другой подход никак не это не влияют. Спор AR vs DM - это спор о том кто сохраняет объекты.
Это основное различие в подходах. Мэпить любую структуру на любую структуру мы можем и при том и при другом подходе, но только при варианте с Active Record обычно так не делают (что бы сохранить иллюзию прямой работы с базой), так как этот шаблон подразумевает максимально упрощенную реализацию. Именно по этому при использовании AR обычно говорят о тестной связанности с базой, а при DM - слабой (мы вообще от нее не зависим по сути).
Остальные нюансы, вроде UnitofWork, Ideniny Map, работа со связями, ленивая инициализация связанных объектов, все это - приблизительно одинаково реализуется при обоих подходах и никакого отношения непосредственно к ActiveRecord или DataMapper не имеет. Но без этого всего сами подходы не предоставляют нам той гибкости, которую мы ожидаем получить от ORM.
На негодняшний день в чистом виде в рамках существующих ORM вы можете встретить только DataMapper. ORM на основе Active Record так или иначе внутри используют намного более сложные концепции, дабы упростить разработчикам жизнь и снизить сложность.
- Doctrine2 - Самое мощное решение из существующих на данный момент.
- Spot2 - легковесная ORM а базе Doctrine DBAL без магии.
- Analogue - легковесая ORM, использующая в качестве основы Laravel Database
- Atlas.Orm - сырая, но интересная ORM, которая пытается полностью вынести Persistance Model из слоя бизнес логики, но без какой либо магии.
TODO
TODO
TODO