Sobre nós Guias Projetos Contactos
Админка
please wait

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:

  1. Replicação Master-Master: Sincronização bidirecional entre quaisquer nós
  2. Offline-First: PouchDB no browser sincroniza quando há ligação
  3. HTTP API: Interface RESTful, sem necessidade de drivers especiais
  4. Resolução de Conflitos: Tratamento integrado para edições concorrentes
  5. 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

  1. Modelo de documentos: Pense em documentos JSON, não em tabelas
  2. Desenhar para sincronização: Planeie antecipadamente esquemas de ID e resolução de conflitos
  3. Views para queries: Crie views para padrões de acesso comuns
  4. PouchDB para offline: Sincronização sem fricção do browser para o servidor
  5. Consistência eventual: Abrace-a, não lute contra ela
  6. 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.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive