Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Фреймворк Yesod

Глава 22 из книги "Архитектура приложений с открытым исходным кодом", том 2.

Оригинал: Yesod
Автор: Michael Snoyman
Перевод: Н.Ромоданов

22.4. Хранение данных с помощью Persistent

Большинство веб-приложений будут сохранять информацию в базе данных. Традиционно, это означает некоторый вариант SQL - базы данных. В этой связи, в Yesod продолжается давняя традиция с PostgreSQL в качестве нашего наиболее часто используемого хранилища данных. Но, как мы видели в последнее время, вопрос долговременного хранения данных не всегда решается с помощью SQL-базы. Поэтому проект Yesod был разработан таким образом, чтобы он мог также хорошо работать с базами данных вида NoSQL, и поставляется с MongoDB в качестве отличного решения в качестве хранилища данных.

Результатом этого дизайнерского решения является Persistent, компонент хранения данных, используемый в Yesod. Есть, в действительности, два стиля использования Persistent: сделать его настолько нейтральным к хранимым типам, насколько это возможно, и предложить пользователю полную проверку типов.

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

  • Функции, связанные с особенностями хранилища данных. Например, в Persistent предлагается возможность работы с объединениями SQL и списками и хэш-таблицами MongoDB. Предупреждается, что ее использование нарушит переносимость приложения, но если вам нужны эти функции, то они есть.
  • Простой доступ, позволяющий напрямую выполнять запросы к хранилищу данных. Мы не считаем, что нужно с помощью некоторой абстракции реализовывать все функциональные возможности, предоставляемые базовой библиотекой. Если вам просто нужно написать таблицу, связанную с подзапросом в SQL, то - вперед.

Терминология

Самым примитивным типом данных в компоненте Persisten является PersistValue. С его помощью представлены все данные, непосредственно хранящиеся в базе данных, например, числа, даты или строки. Конечно, иногда у вас будут некоторые более дружественные типы данных, которые вы захотите хранить, например, HTML. Для этого у нас есть класс PersistField. Внутри базы данных PersistField выражается через PersistValue.

Все это очень хорошо, но мы хотим объединять различные поля вместе в одну большую картину. Для этого у нас есть класс PersistEntity, который является, по существу, коллекцией классов PersistField. И, наконец, у нас есть класс PersistBackend, в котором описывается, как создавать, читать, обновлять и удалять все эти сущности.

В качестве практического примера рассмотрим хранение в базе данных информации о некоторой персоне. Мы хотим хранить имя, день рождения и изображение профиля (файл PNG). Мы создаем новую сущность Person с тремя полями: Text, Day и PNG. Каждый из них хранится в базе данных с использованием другого конструктора PersistValue: PersistText, PersistDay и PersistByteString, соответственно.

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

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

Как все это представлено в базе данных? В качестве примера рассмотрим SQL: сущность Person становится таблицей с тремя колонками (имя, дата рождения и изображение). Каждое поле хранится в виде различных типов SQL: тип Text становится типом VARCHAR, тип Day становится типом Date, а тип PNG становится типом BLOB (или BYTEA).

История для MongoDB очень похожа. Тип Person становится его собственным документом document, а его три поля каждой становится полем field в MongoDB. В MongoDB не нужны типы данных и не нужно создавать схему.

PersistentSQLMongoDB
PersistEntityТаблицаДокумент
PersistFieldСтолбецПоле
PersistValueТип столбцаНет

Безопасность типов

Компонент Persistent обрабатывает все данные, решая все проблемы поиска и доступа так, что они для вас будут незаметны. Как пользователь Persistent, вы получаете возможность полностью игнорировать тот факт, что тип Text становится типом VARCHAR. Вы можете просто объявить типы данных и использовать их.

Каждое взаимодействие с Persistent является строго типизированным. Это позволяет предотвратить случайное запоминание номера в поле даты; компилятор это не допустит. В такой ситуации просто исчезают целые классы тонких ошибок.

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

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

  • Все это основывается на том, что тест покрывает весь код. Для этого требуется дополнительное время и, что еще хуже, для этого требуется код шаблона, который компилятор должен суметь сделать для вас.
  • Вы можете быть идеальным разработчиком, который никогда не забывает написать тест, но можете ли вы сказать то же самое о каждом, кто имеет отношение к вашему коду?
  • Даже 100% тестовое покрытие не гарантирует, что вы действительно проверили каждый случай. Доказано, что когда вы все это сделаете, вы протестируете каждую строку кода.

Межбазовый синтаксис

Создать схему SQL, которая работает для нескольких движков SQL, может оказаться достаточно сложным. Как создать схему, которая также будет работать с базой данных non-SQL, например, с MongoDB?

омпонент позволяет определять ваши сущности в синтаксисе высокого уровня и автоматически создавать для вас схему SQL. В случае MongoDB, мы в настоящее время используем подход без использования схемы. Также Persistent обеспечивает, чтобы ваши типы данных в языке Haskell точно соответствовали определениям в базе данных.

Кроме того, когда есть вся эта информация, Persistent может для вас автоматически выполнять более сложные функции, такие как миграция данных.

Миграция

В Persistent не только по мере необходимости создаются файлы схем данных, но и автоматически выполняется, если это возможно, миграция базы данных. Модификация баз данных является одним из менее развитых частей стандарта SQL, и поэтому в каждом движке этот процесс происходит по-разному. В результате в каждом компоненте Persistent определяется свой собственный набор правил миграции данных. В PostgreSQL, в котором есть богатый набор правил ALTER TABLE, мы этим пользуется достаточно широко. Поскольку в SQLite таких функциональных возможностей недостаточно, мы решили создавать временные таблицы и выполнять копирование строк. Подход в MongoDB, в котором отсутствует схема данных, означает, что поддержка миграции не требуется.

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

Отношения

Компонент Persistent, по своей природе, не является реляционным, что означает, что в движке не поддержка поддержка реализации отношений. Тем не менее, во многих практических ситуациях нам может потребоваться использовать отношения. В этих случаях разработчикам будет предоставлен к ним полный доступ.

Предположим, что нам теперь с каждым пользователем необходимо хранить список его навыков. Если бы мы писали приложение, специально предназначенное для MongoDB, мы могли бы идти дальше и просто хранить этот список как новое поле в исходной сущности Person. Однако такой подход не будет работать в SQL. В SQL, мы называем такие отношения отношениями типа «один-ко-многим».

Идея состоит в том, чтобы создать ссылку на «одну» сущность (персону) в «множественной» сущности (навыках). Тогда, если мы хотим найти все навыки, которые есть у человека, мы просто находим все навыки, в которых есть ссылка на эту персону. В соответствие с этой ссылкой мы получаем идентификатор ID. Заметим, что, как вы могли бы ожидать, эти идентификаторы являются типобезопасными. Типом данных для идентификатора Person ID является тип PersonId. Таким образом, чтобы добавить наш новый навык, нам нужно в нашем определении сущности просто добавить следующее:

Skill
    person PersonId
    name Text
    description Text
    UniqueSkill person name

Эта концепция типа данных ID используется везде в Persistent и в Yesod. Вы можете на основе ID организовать диспетчеризацию объектов. В таком случае, Yesod автоматически будет искать и преобразовывать маршрут из текстового представления ID в его внутреннее представлению, по ходу дела выявляя синтаксические ошибки. Эти идентификаторы ID используются для выполнения операций поиска и удаления с использованием функций get и delete, а также для получения результата операций вставки и выборки значений с использованием функций insert и selectList.


Далее: Маршруты