Найти тему
ZDG

ORM

Оглавление

ORM означает Object Relational Mapping, или Объектно-Реляционное Отображение.

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

Посмотрим на пример.

В базе данных MySQL есть таблица user, которая содержит поля id, login, password.

Чтобы получить из неё данные пользователя с id=1, нужно написать SQL-запрос:

select * from user where id = 1

Ответ будет получен в виде ассоциативного массива, где хранятся названия полей и их значения, например:

['id' => '1', 'login' => 'admin', 'password' => '777']

Посмотрим на примерный код на PHP:

$result = $pdo->query('select * from user where id = 1');
$userdata = $result->fetch();
echo $userdata['login'];

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

Обрабатывать данные мы можем, обращаясь к ключам ассоциативного массива, таким как $userdata['login'] или $userdata['password'].

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

В качестве другого примера можно рассмотреть такой код:

$user = new User();
$user->get(1);
echo $user->login;

В данном случае всё выглядит гораздо приличней. У нас есть внятный объект класса User, который содержит свойства id, login, password.

Манипулировать таким объектом удобней, чем ассоциативным массивом. Как нетрудно заметить, свойства класса id, login, password соответствуют полям таблицы user в базе данных. Единственная проблема – построить отображение этих свойств из базы в объект и наоборот.

Здесь это делается с помощью метода get(). Когда мы пишем:

$user->get(1);

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

Попробуем представить, что находится внутри метода get() класса User:

Здесь я опустил некоторые вещи вроде проверок на корректность, чтобы оставить основное – отображение. И вот, собственно, мы получили то самое ORM. Как видим, этот метод не работает никаким магическим способом, просто прячет работу с низкоуровневыми SQL-запросами.

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

-2

Проблемы реализации

ORM описывает отображение, но никак не задаёт, каким именно образом его делать. Поэтому способов реализации может быть много.

Например, если взять вышеописанную схему, где объект класса User имеет методы get() и save(), мы заметим, что класс User должен во-первых иметь доступ к базе данных (через свойство pdo). Это свойство можно передавать например в конструкторе класса. Во-вторых, класс User должен содержать в себе SQL-запросы для работы с таблицей user.

То есть мы заставляем класс User вторгаться в зону ответственности, которая может быть ему и противопоказана.

Далее, если мы начнём вводить другие ORM-классы, например Profile (таблица profile), Message (таблица message), Image (таблица image) и т.п., то каждый из них должен реализовать свои методы get() и save(), используя свою таблицу и свои SQL-запросы.

Упростить данную схему можно, если сделать общий родительский класс, который умеет работать с любыми таблицами и с любым набором свойств:

-3

В методе get() имя таблицы подставляется из свойства table, которое будет разное у классов-потомков. А список свойств в конкретном объекте можно получать через рефлексию (т.е. методы, которые сообщают объекту о том, как он устроен). В разных языках это реализуется по-разному, поэтому, чтобы не усложнять, я просто заведу массив с названиями свойств (attributes).

Например, класс User, который наследуется от класса ORM, будет иметь свойство table = 'user' и свойство attributes = ['id', 'login', 'password']:

-4

Таким же образом можно задать класс Profile и остальные ORM-классы:

-5

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

Например, метод get() работает только с полем id. Но в таблице может и не быть такого поля, поэтому придётся учитывать такие случаи и применять другие методы или делать дополнительные параметры и т.д.

В целом, чем более универсальный ORM-интерфейс вы попытаетесь сделать, тем больше проблем встретите.

Другой вариант реализации это перенос работы с базой в некий централизованный компонент, у которого мы будем запрашивать объекты.

Например, если компонент называется Db, то у него может быть метод получения объекта класса User:

-6

В данном случае всю работу выполняет компонент Db, а у класса User роль исключительно пассивная:

-7

Таким же образом мы можем добавить в компонент Db методы saveUser($user), getProfile(), saveProfile($profile) и т.д., а также универсализировать некоторую часть методов как getObject($table) и saveObject($obj) и т.д.

Заключение

ORM описывает отображение свойств объекта на поля таблицы в базе, а реализация этого отображения зависит от вас. При этом совершенно не требуется, чтобы каждому свойству объекта соответствовало такое же поле в таблице. Вы можете отображать только часть свойств, или транслировать одни значения в другие, или комбинировать несколько свойств объекта в одно поле таблицы и наоборот. Всё это никак не регулируется. Главное, чтобы при чтении из таблицы вы получали корректно заполненный объект, а при записи в таблицу получали корректно заполненную строку.