Repository pattern в контексте работы с IndexedDB и Dexie.js используется как слой абстракции между бизнес-логикой приложения и низкоуровневым API хранения данных. Основная цель такого подхода — изолировать работу с базой данных, упростить тестирование, централизовать запросы и обеспечить единообразный доступ к данным независимо от структуры хранилища.
Dexie предоставляет удобный API поверх IndexedDB, но при прямом использовании в бизнес-логике приложения возникает сильная связность компонентов с конкретной реализацией хранилища. Repository pattern решает эту проблему, вводя промежуточный слой, который скрывает детали Dexie и предоставляет доменно-ориентированные методы.
Repository — это класс или модуль, который:
Вместо:
db.users.where('age').above(18).toArray()
используется:
userRepository.getAdults()
Такое разделение делает код более устойчивым к изменениям структуры базы данных.
Dexie, несмотря на высокоуровневый API, всё равно остаётся ORM-подобным слоем доступа к IndexedDB. Это означает:
.where().filter().sortBy() засоряют
сервисы;Repository pattern решает это за счёт централизованного API.
Типичная архитектура с репозиторием поверх Dexie выглядит так:
UI / Controllers
↓
Service Layer
↓
Repository Layer
↓
Dexie (IndexedDB)
Repository становится единственной точкой взаимодействия с базой данных.
class UserRepository {
constructor(db) {
this.db = db;
}
async getById(id) {
return this.db.users.get(id);
}
async getAll() {
return this.db.users.toArray();
}
async add(user) {
return this.db.users.add(user);
}
async upd ate(id, changes) {
return this.db.users.update(id, changes);
}
async delete(id) {
return this.db.users.delete(id);
}
}
Этот уровень уже устраняет прямую зависимость бизнес-логики от Dexie.
Суть repository pattern проявляется в добавлении методов, отражающих бизнес-логику:
class UserRepository {
constructor(db) {
this.db = db;
}
async getActiveUsers() {
return this.db.users.where('isActive').equals(1).toArray();
}
async getAdults() {
return this.db.users.where('age').aboveOrEqual(18).toArray();
}
async searchByEmail(email) {
return this.db.users.where('email').equalsIgnoreCase(email).first();
}
}
Теперь изменения в структуре таблицы или индексов не требуют правок в сервисах приложения.
Dexie позволяет строить сложные цепочки запросов, но они плохо читаются при дублировании:
const result = await db.orders
.where('status')
.equals('paid')
.and(order => order.total > 100)
.toArray();
В репозитории это превращается в доменный метод:
class OrderRepository {
constructor(db) {
this.db = db;
}
async getPremiumPaidOrders(minTotal) {
return this.db.orders
.where('status')
.equals('paid')
.and(order => order.total > minTotal)
.toArray();
}
}
На уровне сервиса:
const orders = await orderRepository.getPremiumPaidOrders(100);
Repository может выполнять трансформацию данных:
class UserRepository {
constructor(db) {
this.db = db;
}
mapToDomain(userRow) {
return {
id: userRow.id,
fullName: `${userRow.firstName} ${userRow.lastName}`,
age: userRow.age,
isActive: Boolean(userRow.isActive)
};
}
async getById(id) {
const user = await this.db.users.get(id);
return user ? this.mapToDomain(user) : null;
}
}
Это снижает зависимость бизнес-логики от структуры таблиц Dexie.
Dexie поддерживает транзакции, и repository может централизовать их использование:
class OrderRepository {
constructor(db) {
this.db = db;
}
async createOrderWithItems(order, items) {
return this.db.transaction('rw', this.db.orders, this.db.items, async () => {
const orderId = await this.db.orders.add(order);
for (const item of items) {
await this.db.items.add({
...item,
orderId
});
}
return orderId;
});
}
}
Сервису не нужно знать о транзакционной модели Dexie.
В крупных приложениях репозитории могут зависеть друг от друга:
class UserRepository {
constructor(db) {
this.db = db;
}
async getById(id) {
return this.db.users.get(id);
}
}
class OrderRepository {
constructor(db, userRepository) {
this.db = db;
this.userRepository = userRepository;
}
async getOrdersWithUsers() {
const orders = await this.db.orders.toArray();
const result = await Promise.all(
orders.map(async order => {
const user = await this.userRepository.getById(order.userId);
return { ...order, user };
})
);
return result;
}
}
Такой подход усиливает модульность, но требует контроля над количеством запросов.
Repository слой позволяет централизованно внедрять оптимизации:
Пример кеширования:
class UserRepository {
constructor(db) {
this.db = db;
this.cache = new Map();
}
async getById(id) {
if (this.cache.has(id)) {
return this.cache.get(id);
}
const user = await this.db.users.get(id);
this.cache.se t(id, user);
return user;
}
}
Repository pattern упрощает тестирование, поскольку Dexie можно заменить мок-объектом:
const mockDb = {
users: {
get: jest.fn(),
toArray: jest.fn()
}
};
const repo = new UserRepository(mockDb);
test('getById returns user', async () => {
mockDb.users.get.mockResolvedValue({ id: 1, name: 'A' });
const result = await repo.getById(1);
expect(result).toEqual({ id: 1, name: 'A' });
});
Бизнес-логика не зависит от IndexedDB и Dexie напрямую.
Протекание Dexie наружу
// плохо
class UserRepository {
getUsers() {
return this.db.users.where('age').above(18);
}
}
Здесь возвращается Dexie Query, что ломает абстракцию.
Слишком толстый repository
Repository не должен превращаться в сервисный слой с бизнес-логикой:
Дублирование логики запросов
Если одинаковые .where() повторяются в разных местах,
они должны быть вынесены в методы repository.
При росте приложения repository слой выполняет роль стабилизирующего элемента архитектуры:
Dexie при этом остаётся инфраструктурной деталью, скрытой за репозиториями, а вся предметная логика выражается через методы уровня домена.