Рассказывает Стенли Винтергрин, наш подписчик
Всем привет! В этой статье я поделюсь своим опытом по работе с базой данных в приложении для Android. Это будет один из вариантов реализации паттерна data access object на языке Java.
Постановка задачи
Заказчик, который занимается предстраховыми осмотрами автомобилей, хочет автоматизировать рабочий процесс. Задача приложения — собирать и передавать на обработку данные об автомобиле.
Проектирование показало, что у приложения будет 3 модуля:
- Авторизация.
- Проведение осмотра.
- Синхронизация.
Сущности
Есть сущности, которыми мы будем оперировать в коде, опираясь на техзадание этого проекта и API сервера: пользователи, шаблоны и осмотры. Пользователи получают от сервера шаблон сценария и проводят осмотр. Составленный на основе полученной информации готовый осмотр отправляется на сервер.
Доступ к данным
Выделим для этих сущностей 3 вида доступа. В данном случае это будут следующие интерфейсы:
UsersAccess
— для пользователей;ScenariosAccess
— для шаблонов сценариев;InspectionsAccess
— для осмотров.
Далее создадим data access object, который предоставляет доступы. Этот интерфейс будет выглядеть так:
interface DAO
{
UsersAccess usersAccess();
ScenariosAccess scenariosAccess();
InspectionsAccess inspectionsAccess();
}
Теперь у нас есть все необходимые виды доступа, но пока они не запрашивают и не изменяют данные.
Модели данных
Создадим модели, которые будут описывать необходимые операции с сущностями. Начнём с пользователей. Что в данном проекте нужно с ними делать? В техзадании сказано, что одним устройством могут пользоваться разные люди, поэтому нужно хранить список пользователей. Также известно, что пользователи ничего не будут знать друг о друге. В таком случае можно создать модель:
interface UsersModels
{
interface Users
{
User get(int id);
void add(User user);
}
}
Очевидно, что у нас также должны быть модели ScenariosModels
и InspectionsModels
. Они будут оперировать несколькими сущностями. Шаблон будет состоять из типов этапов осмотра, а типы этапов из типов элементов. Нужно организовать возможность добавлять данные в базу, получать шаблон по уникальному ключу и строить список типов этапов по ссылке на шаблон и список типов элементов по ссылке на тип этапа:
interface ScenariosModels
{
interface ScenarioTypes
{
ScenarioType get(int id);
void add(ScenarioType type);
}
interface StepTypes
{
StepType get(int id);
List<StepType> getAll(ScenarioType scenarioType);
void add(StepType type);
}
interface ItemTypes
{
ItemType get(int id);
List<ItemType> getAll(StepType stepType);
void add(ItemType type);
}
}
Готовый осмотр будет состоять из элементов, а элементы из данных. Нужно реализовать возможность получения осмотра по ключу, построение списка черновиков осмотров, списка готовых осмотров, а также возможность удалить осмотр. Таким образом, для всех подсущностей осмотра создадим модели:
interface InspectionsModels
{
interface Inspections
{
Inspection get(int id);
List<Inspection> getAll(User user);
void add(Inspection inspection);
void update(Inspection inspection);
void remove(Inspection inspection);
}
interface Steps
{
Step get(int id);
List<Step> getAll(Inspection inspection);
void add(Step step);
void removeAll(Inspection inspection);
}
interface Items
{
Item get(int id);
List<Item> getAll(Step step);
void add(Item item);
void removeAll(Step step);
}
interface Datas
{
Data get(int id);
List<Data> getAll(Item item);
void add(Data data);
void update(Data data);
void remove(Data data);
void removeAll(Item item);
}
}
Теперь передадим доступам их модели:
interface UsersAccess
{
UsersModels.Users users();
}
interface InspectionsAccess
{
InspectionsModels.Inspections inspections();
InspectionsModels.Steps steps();
InspectionsModels.Items items();
InspectionsModels.Datas datas();
}
interface ScenariosAccess
{
ScenariosModels.ScenarioTypes scenarioTypes();
ScenariosModels.StepTypes stepTypes();
ScenariosModels.ItemTypes itemTypes();
}
Теперь наш data access object готов.
Использование data access object
У нас есть три модуля. Каждый из них получит доступ только к той области базы данных, которая необходима для выполнения его задач.
При авторизации будет происходить работа только с пользователями. Поэтому модуль авторизации получит интерфейс UsersAccess
. Поскольку необходимы операции только с одной сущностью, можно оставить этой модели доступ только к таблице Users
.
При прохождении осмотра нужно опираться на шаблон и на его основе составлять объект осмотра. Придется определять, как отображать этапы осмотра и его элементы, т. е. в этом случае необходим полный доступ ко всем сущностям шаблона и сущностям осмотра. Этому модулю можно передать ScenariosAccess
вместе с InspectionsAccess
.
Во время синхронизации необходимо получать информацию о готовых осмотрах для передачи на сервер со всеми элементами и прикреплёнными данными. В этот модуль стоит передать только InspectionsAccess
.
Для сравнения посмотрите на код, который используется для получения данных, если не использовать DAO:
class SomeModel
{
private final Settings settings;
SomeModel(Settings s)
{
settings = s;
}
public List<Inspection> getInspections()
{
User currentUser = SQLite.getInstance().getUsers().getFromId(settings.getCurrentUserId());
return SQLite.getInstance().getInspections().getAllFromUser(currentUser);
}
}
Обратите внимание, что класс неявно получает зависимость от конкретной реализации SQLite. Более того, он получает доступ ко всем частям базы данных, хотя не должен иметь возможности читать, изменять и удалять данные вне его круга задач. А вот как выглядит реализация аналогичной задачи с применением DAO:
class SomeModel
{
private final Settings settings;
private final UsersModels.Users users;
private final InspectionsModels.Inspections inspections;
SomeModel(Settings s, UsersModels.Users us, InspectionsModels.Inspections is)
{
settings = s;
users = us;
inspections = is;
}
public List<Inspection> getInspections()
{
User currentUser = users.get(settings.getCurrentUserId());
return inspections.getAll(currentUser);
}
}
Теперь класс явно зависит только от модели Users
и от модели Inspections
. Стоить заметить, что SQLite нигде не упоминается.
Тестирование
У нас уже описана логика по работе с базой данных. Мы используем её в модулях, хотя реализации работы с данными пока нет. Однако, мы уже можем написать тесты для всех доступов и моделей. Итак, DAO должен успешно передавать доступы (не null
). Точно так же для доступов: они не производят никаких действий, а только передают данные. А вот для моделей можно написать тесты. Например, модель Users
изначально ничего не возвращает по ключу, но после добавления объекта станет возвращать новый объект со всеми полями в таком же виде, в котором он был добавлен в базу. По такому же принципу нужно покрыть тестами все модели.
class DAOTest
{
private DAO dao;
@Test
public void checkAccesses()
{
assertNotNull("users access", dao.usersAccess());
assertNotNull("scenarios access", dao.scenariosAccess());
assertNotNull("inspections access", dao.inspectionsAccess());
}
}
class UsersTest
{
private UsersModels.Users users;
@Test
public void checkUsers()
{
int id = randomId();
assertNull("user must be null!", users.get(id));
User user1 = makeFakeUser(id);
users.add(user1);
User user2 = users.get(id);
assertNotNull("user with id:" + id + " must exist!", user2);
assertEquals("users must be equals!", user1, user2);
}
}
Реализация
Теперь осталось выбрать, с помощью чего реализовать обработку данных для нашего data access object — например, это может быть SQLite. Удобство DAO в том, что мы сможем изменить реализацию логики работы приложения, не затрагивая при этом блок по работе с базой данных. Например, в проекте MyM1y (система учёта финансовых операций) с самого начала для хранения данных был выбран SQLite. В процессе работы над проектом было решено заменить реализацию на более легковесную. Была выбрана другая библиотека — Boxes, и реализация работы с базой данных в проекте была полностью заменена. Это можно наблюдать в коммите painless jump from sqlite to custom nosql orm. Класс stan.mym1y.clean.db.SQLite.java был полностью вырезан со всеми зависимостями и заменён на stan.mym1y.clean.boxes.Boxes.java. Обратите внимание, что это было сделано без изменения модулей по взаимодействию с базой данных.
Заключение
Используя DAO, можно удобно разделять уровни доступа при работе с базой данных, чётко видеть эти уровни доступа и легко оперировать ими, не привязываясь к конкретной реализации хранения данных. Это позволяет применять такой подход вместе с TDD и точнее настраивать реализацию работы с БД (без изменений в других модулях) или полностью заменить одну реализацию на другую.