О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

CouchDB — это документо-ориентированная NoSQL база данных, разработанная с упором на надёжность, горизонтальное масштабирование и бесшовную синхронизацию. Репликация master-master делает её идеальной для offline-first приложений, где данные должны синхронизироваться между устройствами. В этом руководстве рассматривается создание приложений с поддержкой синхронизации на CouchDB с точки зрения senior-разработчика.

Почему CouchDB

CouchDB особенно хорошо подходит для определённых сценариев:

  1. Репликация Master-Master: двунаправленная синхронизация между любыми узлами
  2. Offline-First: PouchDB в браузере синхронизируется при наличии подключения
  3. HTTP API: RESTful интерфейс, специальные драйверы не требуются
  4. Разрешение конфликтов: встроенная обработка параллельных правок
  5. CouchApps: отдача HTML/JSON напрямую из базы данных

Лучше всего подходит для:

  • Мобильных приложений, которым нужна работа offline
  • Развёртываний в нескольких регионах, где требуется синхронизация данных
  • Высоконагруженных систем логирования
  • Приложений с нестабильным подключением

Установка

Использование Docker

docker run -d --name couchdb \
-p 5984:5984 \
-e COUCHDB_USER=admin \
-e COUCHDB_PASSWORD=password \
couchdb:latest

Docker Compose

version: '3.8'
services:
couchdb:
image: couchdb:latest
ports:
- "5984:5984"
environment:
COUCHDB_USER: admin
COUCHDB_PASSWORD: password
volumes:
- couchdb_data:/opt/couchdb/data
volumes:
couchdb_data:

Установка в Linux

# Ubuntu/Debian
apt-get install couchdb
# CentOS/RHEL
yum install couchdb

Доступ к Fauxton (admin UI): http://localhost:5984/_utils/

Основы CouchDB

HTTP API

CouchDB использует чистый RESTful HTTP API:

# Проверить сервер
curl http://localhost:5984/
# Создать базу данных
curl -X PUT http://admin:password@localhost:5984/mydb
# Список баз данных
curl http://admin:password@localhost:5984/_all_dbs
# Удалить базу данных
curl -X DELETE http://admin:password@localhost:5984/mydb

Операции с документами

# Создать документ
curl -X POST http://admin:password@localhost:5984/mydb \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "[email protected]"}'
# Получить документ
curl http://localhost:5984/mydb/document_id
# Обновить документ (необходимо включить _rev)
curl -X PUT http://admin:password@localhost:5984/mydb/document_id \
-H "Content-Type: application/json" \
-d '{"_rev": "1-xxx", "name": "John Updated"}'
# Удалить документ
curl -X DELETE http://admin:password@localhost:5984/mydb/document_id?rev=1-xxx

Интеграция с Node.js

Использование nano

npm install nano
const nano = require('nano')('http://admin:password@localhost:5984');
// Создать базу данных
async function setupDatabase() {
try {
await nano.db.create('myapp');
console.log('Database created');
} catch (err) {
if (err.statusCode !== 412) throw err; // Уже существует
}
}
const db = nano.db.use('myapp');
// CRUD-операции
async function createDocument(doc) {
const response = await db.insert(doc);
return { id: response.id, rev: response.rev };
}
async function getDocument(id) {
try {
return await db.get(id);
} catch (err) {
if (err.statusCode === 404) return null;
throw err;
}
}
async function updateDocument(id, updates) {
const doc = await db.get(id);
const updated = { ...doc, ...updates };
return db.insert(updated);
}
async function deleteDocument(id) {
const doc = await db.get(id);
return db.destroy(id, doc._rev);
}
// Пакетные операции
async function bulkInsert(docs) {
return db.bulk({ docs });
}
async function getAllDocuments() {
const response = await db.list({ include_docs: true });
return response.rows.map(row => row.doc);
}

Views и запросы

Создание Views

Views определяются в design documents:

const designDoc = {
_id: '_design/app',
views: {
// Простое view: emit всех документов по типу
by_type: {
map: function(doc) {
if (doc.type) {
emit(doc.type, null);
}
}.toString()
},
// View со значениями
users_by_email: {
map: function(doc) {
if (doc.type === 'user') {
emit(doc.email, {
name: doc.name,
created: doc.createdAt
});
}
}.toString()
},
// Составные ключи
posts_by_date: {
map: function(doc) {
if (doc.type === 'post') {
var date = new Date(doc.createdAt);
emit([date.getFullYear(), date.getMonth() + 1, date.getDate()], doc.title);
}
}.toString()
},
// С reduce для подсчёта
count_by_type: {
map: function(doc) {
if (doc.type) {
emit(doc.type, 1);
}
}.toString(),
reduce: '_count'
}
}
};
await db.insert(designDoc);

Запросы к Views

// Получить все документы заданного типа
const users = await db.view('app', 'by_type', {
key: 'user',
include_docs: true
});
// Запрос по диапазону
const posts = await db.view('app', 'posts_by_date', {
startkey: [2024, 1, 1],
endkey: [2024, 12, 31],
include_docs: true
});
// Пагинация
const page = await db.view('app', 'users_by_email', {
limit: 10,
skip: 20,
include_docs: true
});
// Получить количество
const counts = await db.view('app', 'count_by_type', {
group: true
});

Mango Queries (CouchDB 2.0+)

Более привычный SQL-подобный синтаксис:

// Создать индекс
await db.createIndex({
index: { fields: ['type', 'createdAt'] },
name: 'type-date-index'
});
// Запрос с selector
const result = await db.find({
selector: {
type: 'user',
createdAt: { $gte: '2024-01-01' }
},
sort: [{ createdAt: 'desc' }],
limit: 10
});

Offline-First с PouchDB

PouchDB работает в браузерах и синхронизируется с CouchDB:

Настройка в браузере

<script src="https://cdn.jsdelivr.net/npm/pouchdb@8/dist/pouchdb.min.js"></script>
// Локальная база данных (IndexedDB в браузере)
const localDB = new PouchDB('myapp');
// Удалённая CouchDB
const remoteDB = new PouchDB('http://localhost:5984/myapp', {
auth: {
username: 'admin',
password: 'password'
}
});
// Непрерывная синхронизация
const sync = localDB.sync(remoteDB, {
live: true,
retry: true
})
.on('change', info => {
console.log('Sync change:', info);
})
.on('paused', err => {
console.log('Sync paused', err ? 'with error' : 'up to date');
})
.on('active', () => {
console.log('Sync resumed');
})
.on('denied', err => {
console.error('Sync denied:', err);
})
.on('error', err => {
console.error('Sync error:', err);
});
// Остановить синхронизацию при необходимости
// sync.cancel();

CRUD-операции

// Создать
async function createItem(item) {
const doc = {
_id: new Date().toISOString(), // Или используйте PouchDB.uuid()
type: 'item',
...item,
createdAt: new Date().toISOString()
};
return localDB.put(doc);
}
// Прочитать
async function getItem(id) {
try {
return await localDB.get(id);
} catch (err) {
if (err.status === 404) return null;
throw err;
}
}
// Обновить
async function updateItem(id, updates) {
const doc = await localDB.get(id);
return localDB.put({
...doc,
...updates,
updatedAt: new Date().toISOString()
});
}
// Удалить
async function deleteItem(id) {
const doc = await localDB.get(id);
return localDB.remove(doc);
}
// Список всех
async function getAllItems() {
const result = await localDB.allDocs({
include_docs: true,
startkey: 'item_',
endkey: 'item_\ufff0'
});
return result.rows.map(row => row.doc);
}

Интеграция с React

import { useState, useEffect } from 'react';
import PouchDB from 'pouchdb';
const db = new PouchDB('tasks');
function useTasks() {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Начальная загрузка
loadTasks();
// Слушать изменения
const changes = db.changes({
since: 'now',
live: true,
include_docs: true
}).on('change', () => {
loadTasks();
});
return () => changes.cancel();
}, []);
async function loadTasks() {
const result = await db.allDocs({ include_docs: true });
setTasks(result.rows.map(r => r.doc).filter(d => d.type === 'task'));
setLoading(false);
}
async function addTask(title) {
await db.put({
_id: `task_${Date.now()}`,
type: 'task',
title,
completed: false,
createdAt: new Date().toISOString()
});
}
async function toggleTask(task) {
await db.put({
...task,
completed: !task.completed
});
}
async function deleteTask(task) {
await db.remove(task);
}
return { tasks, loading, addTask, toggleTask, deleteTask };
}
function TaskList() {
const { tasks, loading, addTask, toggleTask, deleteTask } = useTasks();
const [newTask, setNewTask] = useState('');
if (loading) return <div>Loading...</div>;
return (
<div>
<form onSubmit={e => {
e.preventDefault();
addTask(newTask);
setNewTask('');
}}>
<input
value={newTask}
onChange={e => setNewTask(e.target.value)}
placeholder="New task"
/>
<button type="submit">Add</button>
</form>
<ul>
{tasks.map(task => (
<li key={task._id}>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task)}
/>
<span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
{task.title}
</span>
<button onClick={() => deleteTask(task)}>Delete</button>
</li>
))}
</ul>
</div>
);
}

Разрешение конфликтов

Когда один и тот же документ редактируется на нескольких устройствах:

// Получить документ с конфликтами
const doc = await db.get(docId, { conflicts: true });
if (doc._conflicts) {
// Получить конфликтующие ревизии
const conflicts = await Promise.all(
doc._conflicts.map(rev => db.get(docId, { rev }))
);
// Разрешение: объединить или выбрать победителя
const resolved = mergeConflicts(doc, conflicts);
// Сохранить разрешённую версию
await db.put(resolved);
// Удалить проигравшие ревизии
for (const conflict of doc._conflicts) {
await db.remove(docId, conflict);
}
}
function mergeConflicts(winner, losers) {
// Пример: объединить массивы, для скаляров оставить последнее значение
const merged = { ...winner };
for (const loser of losers) {
// Объединить массивы tags
if (loser.tags) {
merged.tags = [...new Set([...(merged.tags || []), ...loser.tags])];
}
// Сохранить самое свежее обновление
if (loser.updatedAt > merged.updatedAt) {
merged.content = loser.content;
merged.updatedAt = loser.updatedAt;
}
}
return merged;
}

Ключевые выводы

  1. Модель документов: мыслите JSON-документами, а не таблицами
  2. Проектируйте под синхронизацию: заранее продумайте схемы ID и стратегию разрешения конфликтов
  3. Views для запросов: создавайте views под типовые паттерны доступа
  4. PouchDB для offline: бесшовная синхронизация браузер-сервер
  5. Eventual consistency: принимайте её, а не боритесь с ней
  6. Обработка конфликтов: стратегия должна быть всегда

CouchDB особенно хороша для приложений, которым нужна надёжная синхронизация данных между устройствами и регионами — используйте её сильные стороны, а не пытайтесь навязать реляционные паттерны.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив