Иногда нам приходится добавлять JSON аттрибут в нашу базу данных для удобной поддержки слабо-структурированных данных. Например:
социальные сети пользователя. Он может указать Вконтакте, Facebook, Twitter и так далее. Все это можно хранить в 1 JSON аттрибуте;
настройки нотификации пользователя. Какие письма он хочет получать, какие нет;
конфиги/сериализованные представления объектов - представление объекта IceCube для рекурентных событий, конфиги на основании которых приложение должно выглядеть/работать по другому и т.д.;
контекст
Допустим имеется некий аналог трелло с дополнительными фичами:
Пользователь может создать доску для своих карточек - введя название доски
Пользователь может создать карточку внутри своей доски - надо ввести название, описание и указать приоритет карточки
Пользователь может увидеть созданную карточку и свое имя в поле “Автор” у созданной карточки
Администратор может слегка изменить форму создания карточки + создать шаблон по умолчанию для всех новых карточек:
Aдмин может изменить плейсхолдеры в форме для названия, описания карточки
Админ может задать шаблон для новых карточек - предустановленные значения названия, описания и приоритета
Админ может задать пользователя для шаблонной карточки - его имя будет отображаться в графе “Автор”
Все эти настройки админа решено хранить в поле JSON(B) таблицы card_templates. Структура конфига:
проблема
Как было упомянуто выше, мы используем JSON из-за его гибкой структуры, но это не значит что пользователь может ввести все что захочет. Мы (разработчики приложения) сами задаем удобную для нас структуру JSON аттрибута и пишем логику исходя из этой структуры. Любое отклонение от зафиксированной схемы может вызвать ошибку в приложении. Например, мы сделали API для того чтобы администратор мог создавать свои шаблоны, но практически сразу начали замечать что все созданные конфиги отличаются по структуре от той, что мы ожидаем - нам нужна валидация. Увы, ActiveRecord на данный момент не позволяет нам удобно описывать валидации для наших JSON данных, поэтому нам нужно придумать что-то еще.
data transfer object
Нам нужен некоторый объект, который получает любой хеш, возвращая при этом данные в строго заданной структуре. Для таких целей может подойти DTO (Data transfer object), задача которого как раз в этом и состоит - он нужен для передачи данных между разными частями приложения, при этом эти данные он может структурировать так, как это требуется для работы приложения.
решение
Мы решили что воспользуемся паттерном DTO и теперь нужно его реализовать. Или не нужно? :)
dry-struct (link) - один из гемов dry-rb реализует как раз то, что нам нужно, предлагая удобный способ для описания наших структур. Файловая иерархия DTO в нашем проекте:
Каждый файл по отдельности:
types.rb
Нэймспейс где будут храниться типы из dry-struct для описания наших структур
base_dto.rb
Базовый класс для всех наших DTO. Здесь же есть инструкция что ключи входного хеша должны быть преобразованы в символы
templates/card_settings_dto.rb
Описали структуру конфига, который может создать администратор. Аттрибут default_card - сложная структура, описываемая в другом DTO классе
templates/card_settings/example_dto.rb
Описали структуру дефолтной карточки. Аттрибуты priority и author - вложенные объекты, описанные в других DTO классах
templates/card_settings/priority_dto.rb
Описали шейп объекта приоритетов + добавили констрейнты для допустимых значении.
templates/card_settings/author_dto.rb
Шейп объекта author состоит просто из его имени.
примеры использования
Пустой хеш:
Как видно из примера выше, наш DTO проставляет значения согласно тем значениям по умолчанию, что мы задали.
Хеш, содержащий неправильные значения (нарушающее constrained правила):
При передаче значении, непредусмотренных constrained директивой мы получаем исключение.
Хеш, содержащий правильные значения, но имеющий лишние аттрибуты + некоторые аттрибуты структуры отсутствуют:
Наш DTO вырезал ненужные аттрибуты + добавил пропущенные аттрибуты, согласно их значению по умолчанию.
заключение
C помощью паттерна DTO и ее реализации в виде dry-types можно достаточно просто добавить валидацию к нашим JSON данным. Также это позволит явно описать шейп нашего JSON - объекта и обращаться к нему, чтобы вспомнить что-же хранится в нашем объекте и какие у него могут быть значения :)