Проектирование модели расширенных типов над базой данных

в 5:17, , рубрики: Блог компании Enterra, проектирование баз данных, хранение данных

О приложении

MobiDB Database Designer – приложение для создания реляционных баз данных. Предназначено как для бизнеса, так и для повседневного использования, поддерживает синхронизацию и много пользователей. Позволяет хранить различные типы данных. Подходит для планирования, управления проектами, формирования счет-фактур, организации доставки, инвентаризации, ведения пациентов в больнице и т.п.

Идея проекта

Идея проекта заключалась в создании дизайнера базы данных с таблицами и соответствующих форм просмотра/ввода на тач-устройствах. При этом хотелось, чтобы было удобно и просто создавать базу данных. В итоге решили дизайнить базу на основе формы. Т.е. накидывалась форма ввода данных, на ее основе создавалась база данных и карточки для ввода этих данных. Удаление/добавление контрола приводило к удалению/добавлению колонки в базе данных.

Выбор технологии

Проект изначально предусматривалось делать для нескольких мобильных платформ (для начала android, позже планировали сделать windows store приложение). Писать нативный код означало писать код для всех платформ с нуля. Использовать кроссплатформенные решения (чтобы UI тоже был общим), не хотелось. Хотелось, чтобы приложение было “родным” на каждой платформе. Имея опыт .NET разработки и изучив возможные кроссплатформенные решения, мы выбрали Xamarin. Можно было написать общий код и разный UI, чего мы и хотели. Данная статья не будет касаться специфики Xamarin, а посвящена архитектурным вопросам.

Разрабатывали проект последовательно, сначала модель данных, затем слой данных, после этого UI часть. Описание UI части не входит в рамки данной статьи.

Базу нужно было где-то хранить, клиент должен был работать offline, поддерживаться всеми основными платформами, поэтому ничего лучшего, чем SQLite не нашлось.

Модель

Наша база данных подразумевала наличие высокоуровневых типов данных. Т.е. не числа и строки, а к примеру валюта, почта, рейтинг, ссылка, координаты и т.п. В общем случае это не проблема: обычно для работы с базой данных делают классы модели и мапят её на БД. В java есть hibernate, spring persistence; в .net Entity framework, nhibernate. Однако вспомним, что мы говорим про редактор базы данных, и модель может меняться пользователем. Т.е. про мапинги и адаптеры таблиц (как в .net dataset) следует забыть. Возникла проблема, как организовать хранение данных? В нашей идеологии аналог маппингов представляет схема данных, поддерживающая динамическую модель.

Далее, в SQLite очень ограниченный набор типов:

  • TEXT
  • NUMERIC
  • INTEGER
  • REAL
  • BLOB

Нужно было каким-то образом хранить наши данные в этом ограниченном наборе. Заметим, что поле валюта будет иметь один и тот же знак валюты, рейтинг будет иметь одинаковый шаг и максимум для всех рядов.
Если хранить описание типа вместе со значением, то получится избыточность данных, так как эти данные будут дублироваться для всех строк. Нужно выносить эту информацию в отдельную сущность. Также не все наши типы соответсвуют типам SQLite. Например, поле Location это два числа широта и долгота. Можно было, конечно, хранить их в разных колонках, но это нелогично, так как это неделимая информация, не представляющая смысла в отдельности. Поэтому также было необходимо выполнять конвертацию из типов данных SQLite в данные модели. Данную сущность назвали MetaType, а данные модели MetaValue.

В обязанности MetaType входит преобразование типов данных SQLite в типы модели, а также хранение настроек, характерных для всей колонки.

image

Пример диаграммы классов RatingMetaType:

image

Пример реализации TimeMetaType

public class TimeMetaType : TextualMetaType
    {
        public override string ConvertToString(MetaValue value)
        {
            var timeValue = value as MetaValue<DateTime>;
            if (timeValue == null)
            {
                throw new ArgumentException("MetaValue<long> was expected");
            }
            return string.Format("{0:t}", timeValue.Value);
        }

        public override ElementaryType GetElementaryType()
        {
            return ElementaryType.String;
        }

        public override MetaValue LoadFromElementaryType(object value)
        {
            var dateValue = DateTime.Parse((string)value);
            return new MetaValue<DateTime>(this, new DateTime(1, 1, 1, dateValue.Hour, dateValue.Minute, dateValue.Second));
        }

        public override object SaveToElementaryType(MetaValue value)
        {
            var concreteValue = value as MetaValue<DateTime>;
            if (concreteValue == null)
            {
                throw new ArgumentException("MetaValue<DateTime> was expected");
            }
            return concreteValue.Value.ToString("HH:mm:ss");
        }

        public override bool TryParse(string stringValue, out MetaValue value)
        {
            DateTime result;
            value = null;
            if (!DateTime.TryParse(stringValue, out result))
            {
                return false;
            }
            value = new MetaValue<DateTime>(this, result);
            return true;
        }
    }

Каждой базе данных в нашем приложении соответствует SQLite база данных. Данные хранятся в обычных таблицах SQLite. Здесь мы вспомним про нашу динамическую модель, которую тоже нужно сохранять в базе данных. Мы назвали эту сущность Template (шаблон). Шаблон умеет сохраняться в бинарные данные/загружаться из бинарных данных. Шаблон описывает набор табличных схем, схема описывает коллекцию колонок, а у колонки в свою очередь есть метатип.

image

Преобразование шаблона в byte[] сделано через сериализацию и пометку классов/свойств аттрибутами. Мы используем наш собственный xml-сериализатор. Естественно можно было использовать встроенные в .NET сериализаторы.

Разобрались с метаданными, перейдем к самим данным. Таблица – хранит коллекцию рядов, ряд хранит значения. Значения хранятся в виде MetaValue. Из MetaValue можно узнать метатип и определить, что это за метатип. У таблицы есть схема таблицы. Именно эта связь превращает наши сырые данные в более высокоуровневые данные. В связке они и образуют модель. Читая метаданные о колонках, можно перебирать значения в DataRow.

image

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

image

Заметим, что есть еще контрол формы вложенная таблица, для обеспечения жизнедеятельности в Table и в DataRow есть ParentId.

Что касается связи с родительской таблицей, то этот метатип задается двумя значениями: именем таблицы и именем колонки. RelationMetaType умеет возвращать описание связанной записи и связанное значение. Связанная запись определяется RowId. Таким образом из RelationMetaType можно узнать имя таблицы, взять ее из шаблона, вытащить ряд из связанной таблицы по RowId и получить доступ ко всем значениям связанной записи.

image

Слой данных

На момент старта проекта не было нормальной поддерки PCL, поэтому использовали Mono.android библиотеку. Позднее слой данных переписали, поскольку появилась SQLite PCL:
sqlitepcl.codeplex.com

В SQLite PCL достаточно специфичное API. Подробнее можно посмотреть здесь marcominerva.wordpress.com/2014/02/13/the-new-portable-class-library-for-sqlite

Для бизнес логики мы сделали специальную абстракцию IDatabaseManager. В нашем случае изолюрующий слой данных и возвращающий/принимающий сущности модели:

image

Ввиду большого объема и нехитрой логики реализации слоя данных не буду приводить реализацию SqliteDatabaseManager на основе SQLite PCL.

В результате у нас получилось довольно мощное приложение, где пользователь может самостоятельно создавать сложную структуру данных в соответствии со своими задачами.Оценить реализацию вы можете, загрузив приложение из Google Play:
https://play.google.com/store/apps/details?id=com.perpetuumsoft.mobidb.lite
или из Windows Store:
https://www.microsoft.com/store/apps/9nblggh1jp5v

Автор: Enterra

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js