О приложении

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

Комментарии (9)


  1. gentee
    13.01.2016 14:51

    Каких конкурентов знаете?
    У меня есть на github похожий по сути проект (создание и работа с таблицами для «чайников»), только он устанавливается на web-сервере — нужны только PHP и MySQL, ну и бесплатный.


  1. gcnew
    13.01.2016 15:00

    Что касается конкурентов, то на ios есть TapForms, на android есть memento, в windows store сильных конкурентов нет. Есть еще FileMaker, у него раньше было приложение Bento. Самое сложное в данном типе приложений — сделать его простым и мощным, что невозможно, так как одно другому противоречит. Этим все приложения и страдают: одни слабые но удобные, другие мощные и неудобные (для гиков).


  1. Terranz
    14.01.2016 11:54

    я так и не понял, а что в результате то выдаёт программа?


    1. gcnew
      14.01.2016 12:03

      Позволяет создавать/вести базы данных. При этом не требуется знание SQL и т.п. База создается через WYSWYG designer с drag and drop.


      1. Terranz
        14.01.2016 12:04

        я не об этом) что это программа для проектирования БД я понял
        а вот что на выходе получается? SQL файл со структурой, или файл sqlite базы данных, или что?
        мы же не проектируем базу данных ради проектирования базы данных, её же надо куда-то подключить, где-то использовать


        1. gcnew
          14.01.2016 12:07

          Хранится все это в SQLite базе. Базе соответствует 1 файл. В файле есть данные, а также системная таблица с записью о метаданных.


        1. gcnew
          14.01.2016 12:21

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


          1. Terranz
            14.01.2016 12:33

            а, так это библиотека просто?


            1. gcnew
              14.01.2016 12:46

              Я бы не сказал, что это библиотека. Как оно внутри хранится — это особенности реализации. Это скорее СУБД поверх другой СУБД. Мы бы могли с тем же успехом использовать свой формат для хранения наших данных (например xml) или сервис Parse.com с оффлайн работой. Причины не залазить в саму базу следующие:

              1. названия таблиц — UniqueName, название колонок — UniqueName. Нормальные названия лежат в Blob поле системной таблице в зазипованом xml.
              2. Для синхронизации ведется специальный журнал изменений, поэтому если поменять поле, а в журнал не записать, то нарушится целостность данных. На двух синхронизированных клиентах могут получиться разное состояние данных.