# Mongoengine Read-Write Router

A MongoDB read-write separation solution for mongoengine ODM, inspired by the [django-rw-router](https://github.com/pydtools/django-rw-router) architecture.

This library routes write operations to the primary database and read operations to replica set members, with configurable consistency strategies.

## Features

- **Automatic read-write routing** - Writes go to primary, reads go to replicas
- **Multiple consistency strategies** - random, transaction-aware, consistent hashing
- **Read-after-write consistency** - Ensures you see your own writes
- **Request context tracking** - Thread-safe context using `contextvars`
- **Web framework integration** - Middleware for FastAPI, Flask, and Django
- **Force primary reads** - Explicit control when you need fresh data
- **Write operation tracking** - Automatic context updates on writes

## Installation

```bash
pip install mongoengine-rw-router
```

## Configure 配置方式

支持三种配置方式，按场景选择：

| 方式 | 场景 | 说明 |
|------|------|------|
| `configure()` | 手动模式 | 用户自行 `connect()`，仅配置路由 |
| `configure_from_url()` | 单 URL 副本集 | 阿里云等，同一 URL + 不同 read_preference |
| `configure_from_hosts()` | 主从分离 | 主库与从库使用不同连接串 |

### 方式 1：configure_from_url（单 URL，推荐）

适用于副本集单连接串（含 replicaSet、authSource、多节点、用户名密码）：

```python
from mongoengine import Document, StringField
from mongoengine_rw_router import RwDocumentMixin, configure_from_url

# 阿里云 / 自建副本集等
host = "mongodb://root:xx@node1:3717,node2:3717/?replicaSet=mgset-xxx&authSource=admin"
configure_from_url(host=host, db="myapp")

class User(RwDocumentMixin, Document):
    name = StringField()
    meta = {"db_alias": "default"}

user = User.objects.first()  # 读从库
user.save()  # 写主库
```

### 方式 2：configure_from_hosts（主从分离 URL）

主库与从库使用不同连接串时：

```python
from mongoengine_rw_router import configure_from_hosts

configure_from_hosts(
    primary_host="mongodb://primary:27017",
    replica_hosts=["mongodb://replica1:27017", "mongodb://replica2:27017"],
    db="myapp",
)
```

### 方式 3：configure（手动模式）

用户自行调用 `mongoengine.connect()`：

```python
from mongoengine import connect
from pymongo.read_preferences import ReadPreference
from mongoengine_rw_router import configure

connect(db="myapp", alias="default", host="mongodb://primary:27017")
connect(db="myapp", alias="replica1", host="mongodb://replica1:27017",
        read_preference=ReadPreference.SECONDARY_PREFERRED)

configure(primary_alias="default", replica_aliases=["replica1"])
```

## Strategy Options

| 策略 | 配置 | 一致性 |
|------|------|--------|
| `random` | 基础读写分离 | 最终一致 (Eventual Consistency) |
| `transaction_aware` | 事务内 + 写后读主库 | Read Your Writes |
| `consistent_hash` | 同一 user_id 固定从库 | Monotonic Reads / Consistent Prefix |

## 一致性场景 (DDIA)

与 [django-rw-router](https://github.com/pydtools/django-rw-router) 一致，支持 DDIA 第四章复制滞后导致的四种读一致性问题：

| 场景 | 含义 | 推荐配置 |
|------|------|----------|
| **Read Your Writes** | 写入后立即读可见 | `strategy="transaction_aware"` + `read_after_write_consistency=True` + `transaction_read_primary=True` |
| **Monotonic Reads** | 同一用户不出现时光倒流 | `strategy="consistent_hash"` + 中间件注入 `user_id` |
| **Consistent Prefix** | 有序读取不出现乱序 | `strategy="consistent_hash"`（同 Monotonic Reads） |
| **Eventual Consistency** | 接受复制滞后 | `strategy="random"` |

### 配置示例

```python
# Read Your Writes（推荐，默认）
configure(
    primary_alias="default",
    replica_aliases=["replica1", "replica2"],
    strategy="transaction_aware",
    read_after_write_consistency=True,
    transaction_read_primary=True,
)

# Monotonic Reads / Consistent Prefix（需中间件注入 user_id）
configure(
    strategy="consistent_hash",
    hash_virtual_nodes=40,
)
app.add_middleware(MongoRouterMiddleware, user_id_attr="user.id")

# Eventual Consistency
configure(strategy="random")
```

### 强制主库读

```python
# 方式 1：primary_queryset
user = User.primary_queryset().first()

# 方式 2：force_primary 链式调用
user = User.objects.force_primary().first()

# 方式 3：force_primary 上下文
from mongoengine_rw_router import force_primary
with force_primary():
    user = User.objects.first()
```

## License

MIT

## Credits

Inspired by [django-rw-router](https://github.com/pydtools/django-rw-router)
