Способы проектирования баз данных в Android

в 16:46, , рубрики: android development, sqlite, Разработка под android, метки: ,

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

Два взгляда на проблему

Как известно, в университетах учат строить базы данных по всем правилам: декомпозировать предметную область на сущности, выделить атрибуты и определить первичные ключи, определить отношения между сущностями, привести все это, как минимум, к 3-ей нормальной форме и т.д. Один из “побочных” эффектов такого подхода — падение производительности на операциях чтения, при достаточно сильной декомпозиции и нормализации, так как в запросах необходимо выполнять большее количество джойнов. И чем больше у вас записей в таблицах, тем дольше они выполняются.

Добавим сюда сильно ограниченные аппаратные возможности мобильных платформ, в частности крохотный объем оперативной памяти. Ее и без того мало, так в дополнение к этому, Android ограничивает количество доступной RAM на процесс в зависимости от версии ОС от 16 до 48 МБ. И даже из этих нескольких мегабайт СУБД получает лишь часть, ведь есть еще и само приложение. Ну и в заключение, сам SQLite, в виду своих особенностей поддерживает только два уровня изолированности транзакций. Они либо сериализуются, либо вообще отключены!

В ситуации когда производительность приложения начинает упираться в производительность СУБД на помощь и может прийти альтернативный подход, назовем его key-value ориентированным. Вместо декомпозиции сущности на атрибуты и создания отдельных полей в таблице на каждый атрибут, сущность сохраняется “как есть” в одно единственное поле типа BLOB, иначе говоря сериализуется.

Рассмотрим пример для полной ясности. Пусть наша модель данных в Java-коде выглядит следующим образом:

class Group {
    private Long _id;
    private String number;
    private List<Student> students;

    // getters and setters
    ...
}

class Student {
    private Long _id;
    private String name;
    private String surname;
    private Group group;

    // getters and setters
    ...
}

Таким образом, в “стандартном” варианте мы получим две таблицы с соответствующими наборами атрибутов.

create table Group(
  _id primary key integer autoincrement,
  number text);

create table Student(
  _id primary key integer autoincrement,
  name text,
  surname text,
  group_id integer foreign key);

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

При применении же key-value подхода таблицы будут выглядеть так

create table Group(
  _id primary key integer autoincrement,
  value blob);

create table Student(
  _id primary key integer autoincrement,
  value blob,
  group_id integer foreign key);

при этом группы и студенты сериализуются отдельно по разным таблицам. Либо вообще вот так:

create table Group(
  _id primary key integer autoincrement,
  value blob);

когда группа сериализуется прямо со всеми студентами в одну таблицу.
Рассмотрим достоинства и недостатки обоих подходов и какую выгоду из этого можно извлечь.

Сравнение подходов, плюсы и минусы

Возможности реляционной алгебры

При использовании стандартного подхода мы получаем все преимущества к которым так привыкли при использовании реляционного подхода, а именно язык SQL для удобной выборки, фильтровки и сортировки данных, а также модификации схемы БД. Для того чтобы получить коллекцию сущностей нам необходимо лишь сформировать требуемое условие и забрать наши данные из БД. В key-value же подходе, задача по фильтрованию или упорядочиванию данных лежит на плечах разработчика.

Объем файла БД

При использовании стандартного подхода файл БД как правило имеет меньший объем. Это обуславливается отсутствием избыточности при хранении данных, в следствии нормализации. В теории чем выше степень нормализации, тем меньше избыточность, однако, возрастает нагрузка на БД при чтении этих данных. Значительные ресурсы тратятся на джойны таблиц. При использовании key-value подхода степень избыточности данных выше, так как, как правило, уровень нормализации значительно меньше, что приводит к увеличению размера файла БД.

Гибкость при изменении схемы БД

Обычно с развитием проекта схема БД преобразуется не однократно, добавляются новые поля, удаляются ранее используемые, сущности могут дробиться на несколько новых или наоборот проводиться их денормализация и объединение нескольких таблиц в одну. В случае, если при обновлении схемы мы можем пожертвовать накопленными в БД данными, то все просто: мы создаем новый файл БД каждый раз когда обновляем схему, а старый удаляем. Но что делать, если данные должны быть сохранены и преобразованы к новому формату?
В таком варианте стандартный подход имеет преимущества. Достаточно написать соответствующие апдейт-скрипты, которые преобразуют схему БД к необходимому виду и обновят новые поля значениями по умолчанию или высчитают их с применением той или иной логики. При использовании же сериализации, обновление схемы БД уже не такая простая задача. Необходимо преобразовать схему с сохранением всех данных, а так же обновить сами данные, десиреализовав их, инициализировав новые поля и сериализовав обратно. Возрастает как логическая сложность операции, так и временные затраты необходимые на обновление.

Синхронизация доступа к экземплярам сущностей

Один из основных недостатков key-value подхода, как мне кажется, то что для того чтобы изменить всего одно поле в сущности нам необходимо десериализовать весь объект целиком. Это значительно усложняет доступ к объектам. Например, в случае когда группа сериализуется в базу вместе со всеми студентами, то для того, чтобы изменить фамилию одного из студентов, нам необходимо вынуть из БД всю группу, поменять одну фамилию и сохранить обратно. В случае, если в приложении несколько потоков, сервисов и/или контент провайдеров, которые могут работать с одними и теми же сущностями, то задача многократно усложняется. Чем больше потенциальных “писателей”, тем больше блокировок будет возникать и тем сложнее нам будет обеспечивать синхронизацию доступа к объектам. В случае же стандартного подхода эта задача решается на уровне СУБД.

Производительность

С одной стороны, key-value подход позволяет добиться более высокой производительности при выборке небольших объемов данных. Количество джойнов сокращается, конкретный запрос да и СУБД в целом работает быстрее. С другой, при больших объемах данных, если нам необходимо выполнять фильтрацию или сортировку этих данных по полю которое сериализуется вместе со всем объектом, то для выполнения этой операции нам сначала будет необходимо причитать все сущности, а только потом уже отфильтровать все лишнее, что может привести не к выигрышу производительности, а к ее еще большему ухудшению. В качестве альтернативы можно хранить поля участвующие в запросе фильтровки или сортировки стандартным подходом, а всю остальную сущность в виде BLOBa, но такую кашу потом будет сложно поддерживать.

Объем кода

В стандартном подходе возрастает количество SQL кода, различные скрипты создания и модификации схемы БД, запросы и условия, DAO-объекты и т.д. В key-value, количество подобного кода сокращается, зато возрастает количество кода выполняющего различные сортировки, группировки и фильтрацию по условиям ведь все это приходится выполнять “вручную”, когда при стандартном подходе это делает СУБД, а нам необходимо только написать требуемый запрос.

Сериализация

Минус key-value подхода может состоять в падении производительности связанном с использованием стандартной Java сериализации / десериализации, которая как известно не отличается высокой скоростью. Здесь в качестве альтернативы можно использовать одну из библиотек решающих эту проблему, например protobuf от Google. Помимо скорости дополнительным плюсом, в случае исползьования protobuf’a, будет версионность, т.к. данный протокол поддерживает версионирование объектов.

Заключение

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

Автор: TheDimasig

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


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