CouchDB é uma base de dados NoSQL orientada a documentos, concebida para fiabilidade, escalabilidade horizontal e sincronização sem fricção. A sua replicação master-master torna-a ideal para aplicações offline-first em que os dados têm de sincronizar entre dispositivos. Este guia aborda a construção de aplicações com sincronização com CouchDB, na perspetiva de um developer sénior.
Porquê CouchDB
CouchDB destaca-se em cenários específicos:
- Replicação Master-Master: Sincronização bidirecional entre quaisquer nós
- Offline-First: PouchDB no browser sincroniza quando há ligação
- HTTP API: Interface RESTful, sem necessidade de drivers especiais
- Resolução de Conflitos: Tratamento integrado para edições concorrentes
- CouchApps: Servir HTML/JSON diretamente a partir da base de dados
Mais indicado para:
- Apps móveis que exigem capacidade offline
- Implementações multi-região que necessitam de sincronização de dados
- Sistemas de logging de alta cadência
- Aplicações com conectividade intermitente
Instalação
Usar 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:
Instalação em Linux
# Ubuntu/Debian
apt-get install couchdb
# CentOS/RHEL
yum install couchdb
Aceder ao Fauxton (UI de administração): http://localhost:5984/_utils/
Noções Básicas de CouchDB
HTTP API
CouchDB utiliza uma HTTP API RESTful pura:
# Verificar o servidor
curl http://localhost:5984/
# Criar base de dados
curl -X PUT http://admin:password@localhost:5984/mydb
# Listar bases de dados
curl http://admin:password@localhost:5984/_all_dbs
# Eliminar base de dados
curl -X DELETE http://admin:password@localhost:5984/mydb
Operações sobre Documentos
# Criar documento
curl -X POST http://admin:password@localhost:5984/mydb \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "[email protected]"}'
# Obter documento
curl http://localhost:5984/mydb/document_id
# Atualizar documento (tem de incluir _rev)
curl -X PUT http://admin:password@localhost:5984/mydb/document_id \
-H "Content-Type: application/json" \
-d '{"_rev": "1-xxx", "name": "John Updated"}'
# Eliminar documento
curl -X DELETE http://admin:password@localhost:5984/mydb/document_id?rev=1-xxx
Integração com Node.js
Usar nano
npm install nano
const nano = require('nano')('http://admin:password@localhost:5984');
// Criar base de dados
async function setupDatabase() {
try {
await nano.db.create('myapp');
console.log('Database created');
} catch (err) {
if (err.statusCode !== 412) throw err; // Já existe
}
}
const db = nano.db.use('myapp');
// Operações 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);
}
// Operações em lote
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 e Queries
Criar Views
As views são definidas em design documents:
const designDoc = {
_id: '_design/app',
views: {
// View simples: emitir todos os documentos por tipo
by_type: {
map: function(doc) {
if (doc.type) {
emit(doc.type, null);
}
}.toString()
},
// View com valores
users_by_email: {
map: function(doc) {
if (doc.type === 'user') {
emit(doc.email, {
name: doc.name,
created: doc.createdAt
});
}
}.toString()
},
// Chaves compostas
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()
},
// Com reduce para contagem
count_by_type: {
map: function(doc) {
if (doc.type) {
emit(doc.type, 1);
}
}.toString(),
reduce: '_count'
}
}
};
await db.insert(designDoc);
Consultar Views
// Obter todos os de um tipo
const users = await db.view('app', 'by_type', {
key: 'user',
include_docs: true
});
// Query por intervalo
const posts = await db.view('app', 'posts_by_date', {
startkey: [2024, 1, 1],
endkey: [2024, 12, 31],
include_docs: true
});
// Paginação
const page = await db.view('app', 'users_by_email', {
limit: 10,
skip: 20,
include_docs: true
});
// Obter contagem
const counts = await db.view('app', 'count_by_type', {
group: true
});
Mango Queries (CouchDB 2.0+)
Sintaxe mais familiar, semelhante a SQL:
// Criar índice
await db.createIndex({
index: { fields: ['type', 'createdAt'] },
name: 'type-date-index'
});
// Query com selector
const result = await db.find({
selector: {
type: 'user',
createdAt: { $gte: '2024-01-01' }
},
sort: [{ createdAt: 'desc' }],
limit: 10
});
Offline-First com PouchDB
PouchDB corre em browsers e sincroniza com CouchDB:
Configuração no Browser
<script src="https://cdn.jsdelivr.net/npm/pouchdb@8/dist/pouchdb.min.js"></script>
// Base de dados local (IndexedDB no browser)
const localDB = new PouchDB('myapp');
// CouchDB remoto
const remoteDB = new PouchDB('http://localhost:5984/myapp', {
auth: {
username: 'admin',
password: 'password'
}
});
// Sincronização contínua
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);
});
// Parar a sincronização quando necessário
// sync.cancel();
Operações CRUD
// Criar
async function createItem(item) {
const doc = {
_id: new Date().toISOString(), // Ou usar PouchDB.uuid()
type: 'item',
...item,
createdAt: new Date().toISOString()
};
return localDB.put(doc);
}
// Ler
async function getItem(id) {
try {
return await localDB.get(id);
} catch (err) {
if (err.status === 404) return null;
throw err;
}
}
// Atualizar
async function updateItem(id, updates) {
const doc = await localDB.get(id);
return localDB.put({
...doc,
...updates,
updatedAt: new Date().toISOString()
});
}
// Eliminar
async function deleteItem(id) {
const doc = await localDB.get(id);
return localDB.remove(doc);
}
// Listar tudo
async function getAllItems() {
const result = await localDB.allDocs({
include_docs: true,
startkey: 'item_',
endkey: 'item_\ufff0'
});
return result.rows.map(row => row.doc);
}
Integração com 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(() => {
// Carregamento inicial
loadTasks();
// Escutar alterações
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>
);
}
Resolução de Conflitos
Quando o mesmo documento é editado em vários dispositivos:
// Obter documento com conflitos
const doc = await db.get(docId, { conflicts: true });
if (doc._conflicts) {
// Obter revisões em conflito
const conflicts = await Promise.all(
doc._conflicts.map(rev => db.get(docId, { rev }))
);
// Resolver: fazer merge ou escolher um vencedor
const resolved = mergeConflicts(doc, conflicts);
// Guardar a versão resolvida
await db.put(resolved);
// Eliminar as revisões perdedoras
for (const conflict of doc._conflicts) {
await db.remove(docId, conflict);
}
}
function mergeConflicts(winner, losers) {
// Exemplo: fazer merge de arrays, manter escalares mais recentes
const merged = { ...winner };
for (const loser of losers) {
// Fazer merge dos arrays de tags
if (loser.tags) {
merged.tags = [...new Set([...(merged.tags || []), ...loser.tags])];
}
// Manter a atualização mais recente
if (loser.updatedAt > merged.updatedAt) {
merged.content = loser.content;
merged.updatedAt = loser.updatedAt;
}
}
return merged;
}
Principais Conclusões
- Modelo de documentos: Pense em documentos JSON, não em tabelas
- Desenhar para sincronização: Planeie antecipadamente esquemas de ID e resolução de conflitos
- Views para queries: Crie views para padrões de acesso comuns
- PouchDB para offline: Sincronização sem fricção do browser para o servidor
- Consistência eventual: Abrace-a, não lute contra ela
- Tratamento de conflitos: Tenha sempre uma estratégia
CouchDB destaca-se em aplicações que precisam de sincronização fiável de dados entre dispositivos e regiões — abrace os seus pontos fortes em vez de forçar padrões relacionais.