О приложении
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 в типы модели, а также хранение настроек, характерных для всей колонки.
Пример диаграммы классов RatingMetaType:
Пример реализации 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 (шаблон). Шаблон умеет сохраняться в бинарные данные/загружаться из бинарных данных. Шаблон описывает набор табличных схем, схема описывает коллекцию колонок, а у колонки в свою очередь есть метатип.
Преобразование шаблона в byte[] сделано через сериализацию и пометку классов/свойств аттрибутами. Мы используем наш собственный xml-сериализатор. Естественно можно было использовать встроенные в .NET сериализаторы.
Разобрались с метаданными, перейдем к самим данным. Таблица – хранит коллекцию рядов, ряд хранит значения. Значения хранятся в виде MetaValue. Из MetaValue можно узнать метатип и определить, что это за метатип. У таблицы есть схема таблицы. Именно эта связь превращает наши сырые данные в более высокоуровневые данные. В связке они и образуют модель. Читая метаданные о колонках, можно перебирать значения в DataRow.
С данными и метаданными мы разобрались, а теперь вспомним, что все это предполагалось генерировать на основе формы. В схеме также хранится форма редактирования с контроллами:
Заметим, что есть еще контрол формы вложенная таблица, для обеспечения жизнедеятельности в Table и в DataRow есть ParentId.
Что касается связи с родительской таблицей, то этот метатип задается двумя значениями: именем таблицы и именем колонки. RelationMetaType умеет возвращать описание связанной записи и связанное значение. Связанная запись определяется RowId. Таким образом из RelationMetaType можно узнать имя таблицы, взять ее из шаблона, вытащить ряд из связанной таблицы по RowId и получить доступ ко всем значениям связанной записи.
Слой данных
На момент старта проекта не было нормальной поддерки 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. В нашем случае изолюрующий слой данных и возвращающий/принимающий сущности модели:
Ввиду большого объема и нехитрой логики реализации слоя данных не буду приводить реализацию 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