Apache CouchDB: インストール、多方向レプリケーション、オフラインファーストの実践的な同期

Database tutorial - IT technology blog
Database tutorial - IT technology blog

5分でCouchDBを起動する — まず動かして、後から理解する

最初から理論を説明するつもりはない。まずインストールして動かしてみれば、なぜ他のDBと違うのかが自然とわかってくる。

Ubuntu/Debianへのインストール

# Apache CouchDBの公式GPGキーとリポジトリを追加する
curl -L https://couchdb.apache.org/repo/keys.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/couchdb.gpg
echo "deb https://apache.jfrog.io/artifactory/couchdb-deb/ $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/couchdb.list

sudo apt update && sudo apt install -y couchdb

セットアップウィザードでモードを選択する:1ノードで動かすならstandalone、複数台構成ならclusteredを選ぶ。管理者パスワードはこのステップで設定すること — 後からやり直す機会はない。

サービスが起動していることを確認する:

sudo systemctl status couchdb
curl http://localhost:5984/
# 期待される出力:
# {"couchdb":"Welcome","version":"3.x.x", ...}

データベースとドキュメントの作成

# "myapp"という名前のデータベースを作成する
curl -X PUT http://admin:yourpassword@localhost:5984/myapp

# 最初のJSONドキュメントを追加する
curl -X POST http://admin:yourpassword@localhost:5984/myapp \
  -H "Content-Type: application/json" \
  -d '{"name": "Nguyen Van A", "role": "developer", "city": "Hanoi"}'

# データベース内の全ドキュメントを確認する
curl http://admin:yourpassword@localhost:5984/myapp/_all_docs?include_docs=true

http://localhost:5984/_utils にアクセスするとFauxtonが開く — 追加インストール不要の組み込みWeb UIだ。テスト中にドキュメントを手動で作成・確認・編集するのに非常に便利だ。


MySQL、PostgreSQL、MongoDBを使ってきた — CouchDBは何が違うのか?

複数のプロジェクトでこれら3つすべてを使ってきたが、それぞれに適した場面がある。MySQL/PostgreSQLはデータ構造が固定されていてリレーションが明確なときに最適だ。MongoDBはスキーマの柔軟性があるが、レプリケーションはprimary-secondaryが基本で — secondaryノードは読み取り専用で書き込みができない。

CouchDBが解決するのはまったく別の問題だ:ネットワークがなくてもアプリが動作し続け、接続が戻ったら同期するというケースだ。現場スタッフ向けアプリ、電波の弱いエリアのPOS、屋外の物流システム — こういった場面がCouchDBの本領だ。

CouchDBが実際に違うと感じる3つのポイント

  • 純粋なHTTP API:すべての操作はRESTで行う。専用ドライバー不要 — curlだけで始められる。
  • MVCC(Multi-Version Concurrency Control):書き込み時にロックを使わない。各ドキュメントはリビジョンを追跡する_revフィールドを持つ。読み取りが書き込みにブロックされないため、読み取りスループットが非常に高い。
  • マルチマスターレプリケーション:すべてのノードに書き込みができる。同期時にCouchDBは自動でコンフリクトを検出 — どう解決するかはあなたが決める、データベースではなく。

CouchDBのドキュメントモデル

各ドキュメントは_id(自動生成または手動指定)と_rev(リビジョンハッシュ)を持つJSONだ:

{
  "_id": "order_20260710_001",
  "_rev": "1-abc123def456",
  "customer": "Tran Thi B",
  "items": [
    {"sku": "LAP001", "qty": 2, "price": 15000000},
    {"sku": "MOU002", "qty": 1, "price": 350000}
  ],
  "total": 30350000,
  "status": "pending",
  "created_at": "2026-07-10T09:00:00Z"
}

更新するたびに_revが変わる。2つのノードがオフライン中に同じドキュメントを編集した場合、CouchDBは両方のバージョンを保持してconflictとしてマークする — マージはあなたが後で決める。


レプリケーションの設定 — CouchDBが最も得意とする部分

一方向レプリケーション(one-way)

# "myapp"データベースをサーバーAからサーバーBにコピーする
curl -X POST http://admin:pass@localhost:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{
    "source": "http://admin:pass@server-a:5984/myapp",
    "target": "http://admin:pass@server-b:5984/myapp",
    "create_target": true
  }'

継続的レプリケーション(Continuous Replication)

# "continuous": true でレプリケーションを継続実行し、1回で終了しないようにする
curl -X POST http://admin:pass@localhost:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{
    "source": "http://admin:pass@server-a:5984/myapp",
    "target": "http://admin:pass@server-b:5984/myapp",
    "continuous": true,
    "create_target": true
  }'

双方向レプリケーション(Bidirectional Replication)

2つのジョブを並行実行する — A→BとB→Aの両方向:

# A → B 方向
curl -X POST http://admin:pass@server-a:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{"source": "myapp", "target": "http://admin:pass@server-b:5984/myapp", "continuous": true}'

# B → A 方向
curl -X POST http://admin:pass@server-b:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{"source": "myapp", "target": "http://admin:pass@server-a:5984/myapp", "continuous": true}'

設定が完了すると、変更があるたびに2台のサーバーが自動で同期する — 片方がオフラインから復帰した後も含めて。追加の設定は不要だ。


同期後のコンフリクト処理

コンフリクトは思っているよりも頻繁に発生する — 特に従業員が3Gの不安定なエリアでアプリを使い、2時間オフラインになってから同期する場合に。CouchDBはコンテンツを自動でマージしない — どのリビジョンを採用するかはあなたが決める。

import requests

base = "http://admin:yourpassword@localhost:5984"

# コンフリクトが発生しているドキュメントを検索する
def find_conflicts(db):
    url = f"{base}/{db}/_all_docs?include_docs=true&conflicts=true"
    docs = requests.get(url).json()["rows"]
    return [row["doc"] for row in docs if "_conflicts" in row.get("doc", {})]

# コンフリクトしているリビジョンを取得する
def get_conflict_revs(db, doc_id, conflict_revs):
    results = []
    for rev in conflict_revs:
        url = f"{base}/{db}/{doc_id}?rev={rev}"
        results.append(requests.get(url).json())
    return results

# 負けたリビジョンを削除する(残したいリビジョンを保持する)
def resolve_conflict(db, doc_id, losing_rev):
    url = f"{base}/{db}/{doc_id}?rev={losing_rev}"
    resp = requests.delete(url)
    print(f"Deleted losing rev: {resp.status_code}")

# 使用例
conflicts = find_conflicts("myapp")
for doc in conflicts:
    print(f"Conflict on: {doc['_id']}")
    revs = get_conflict_revs("myapp", doc["_id"], doc["_conflicts"])
    # 現在のリビジョン(winning)を保持し、コンフリクトしているリビジョンを削除する
    for rev in doc["_conflicts"]:
        resolve_conflict("myapp", doc["_id"], rev)

マージのロジックはビジネスロジックに依存する:注文データならcreated_atで最新を採用し、アクティビティログなら両方を保持する必要がある。CouchDBは仕組みを提供する — ルールはあなたが書く。


CouchDBを実際に使う前に知っておくべき4つのこと

1. クライアント側でPouchDBと組み合わせる — 完全なオフラインファーストのコンビ

PouchDBはブラウザやReact Nativeで直接動作するJavaScriptライブラリ(~46KB gzip圧縮後)だ。APIはCouchDBに似ており、ネットワークが復帰すると自動で双方向同期する:

// Webアプリ/React Nativeアプリ内
const db = new PouchDB('local_myapp');
const remoteDB = new PouchDB('http://admin:pass@yourserver:5984/myapp');

// 双方向同期を有効にし、ネットワーク復帰時に自動再接続する
db.sync(remoteDB, { live: true, retry: true })
  .on('change', info => console.log('Synced:', info))
  .on('error', err => console.error('Sync error:', err));

ユーザーはオフラインのまま普通に入力できる。ネットワークが戻ると、PouchDBが自動でプッシュとプルを行う — 追加のコードは不要だ。

2. インデックスを作成してクエリを高速化する

デフォルトのCouchDBはビュー(MapReduce)を使う — 書くのが面倒だ。CouchDB 2.x以降はMango Queryが使えて、はるかに簡単だ:

# "status"と"created_at"フィールドにインデックスを作成する
curl -X POST http://admin:pass@localhost:5984/myapp/_index \
  -H "Content-Type: application/json" \
  -d '{"index": {"fields": ["status", "created_at"]}, "name": "status-index"}'

# Mango構文でクエリを実行する
curl -X POST http://admin:pass@localhost:5984/myapp/_find \
  -H "Content-Type: application/json" \
  -d '{
    "selector": {"status": "pending"},
    "sort": [{"created_at": "desc"}],
    "limit": 20
  }'

3. CouchDBを使わない方がいいケースは?

  • 複数テーブルの複雑なJOINが必要な場合 → PostgreSQLを使う。
  • 時系列データやメトリクス → TimescaleDBかInfluxDBを使う。
  • 高速キャッシュ → Redisを使う。
  • CouchDBが適しているのは:ドキュメント型データ、オフラインファーストが必要、分散ロックを気にせずマルチマスターが必要な場合。

4. ブラウザからPouchDBを使う場合はCORSを有効にする

curl -X PUT http://admin:pass@localhost:5984/_node/nonode@nohost/_config/httpd/enable_cors \
  -d '"true"'
curl -X PUT http://admin:pass@localhost:5984/_node/nonode@nohost/_config/cors/origins \
  -d '"*"'
curl -X PUT http://admin:pass@localhost:5984/_node/nonode@nohost/_config/cors/methods \
  -d '"GET,PUT,POST,HEAD,DELETE"'

本番環境では"*"を特定のドメインに変更すること。開発中は"*"のままで便利だが、実際のデプロイ時はそのままにしないこと。

CouchDBはすべての問題に対応するデータベースではない。しかし、アプリがオフライン時でも動作して後で同期する必要がある場合、または複数のノードが同時に書き込む必要があって分散ロックに悩みたくない場合 — それをワークアラウンドなしでそのまま解決してくれる。

Share: