Metadata-Version: 2.4
Name: django-rw-router
Version: 0.0.1
Summary: Django database read-write separation router with transaction-aware and consistent hash routing support
Author: huoyinghui
Requires-Python: >=3.8
Requires-Dist: django>=3.2
Requires-Dist: uhashring>=2.1
Provides-Extra: dev
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest-django>=4.5; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# Django Read-Write Router

Django database read-write separation router with transaction-aware and consistent hash routing support.

## Features

- **Basic Read-Write Separation**: Route writes to primary, reads to replicas
- **Transaction-Aware Routing**: Automatically route reads to primary when in a transaction
- **Consistent Hash Routing**: Same user always reads from same replica (monotonic reads)
- **Request Context Tracking**: Automatic context management via middleware
- **Primary-Only QuerySet**: Force specific queries to use primary database

## Installation

```bash
pip install django-rw-router
```

Or with uv:

```bash
uv add django-rw-router
```

## Quick Start

### 1. Configure Databases

```python
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'USER': 'user',
        'PASSWORD': 'password',
        'HOST': 'primary.db.example.com',
        'PORT': '5432',
    },
    'readonly': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'USER': 'readonly_user',
        'PASSWORD': 'password',
        'HOST': 'replica.db.example.com',
        'PORT': '5432',
    },
}
```

### 2. Add Router

```python
# settings.py
DATABASE_ROUTERS = [
    'django_rw_router.routers.TransactionPrimaryReplicaRouter',
]
```

### 3. Add Middleware (Optional)

```python
# settings.py
MIDDLEWARE = [
    'django_rw_router.middleware.RequestContextMiddleware',
    # ... other middleware
]
```

## Router Options

### PrimaryReplicaRouter

Basic read-write separation. Writes go to primary, reads are randomly distributed among replicas.

```python
DATABASE_ROUTERS = [
    'django_rw_router.routers.PrimaryReplicaRouter',
]
```

### TransactionPrimaryReplicaRouter (Recommended)

Extends basic router with transaction awareness. Reads inside a transaction go to primary.

```python
DATABASE_ROUTERS = [
    'django_rw_router.routers.TransactionPrimaryReplicaRouter',
]
```

### HashPrimaryReplicaRouter

Uses consistent hashing based on `user_id` from RequestContext. Same user always reads from same replica.

```python
DATABASE_ROUTERS = [
    'django_rw_router.routers.hash.HashPrimaryReplicaRouter',
]
```

## Configuration

### Database Aliases

```python
# settings.py

# Read replica aliases (single or list)
DJANGO_RW_ROUTER_READ_DBS = ['readonly', 'readonly2']

# Write database alias
DJANGO_RW_ROUTER_WRITE_DB = 'default'

# Hash ring virtual nodes (for HashPrimaryReplicaRouter)
DJANGO_RW_ROUTER_HASH_VIRTUAL_NODES = 40
```

### Middleware Configuration

```python
# settings.py

# How to extract user_id from request (nested attributes supported)
DJANGO_RW_ROUTER_USER_ID_ATTR = 'user.id'  # Default

# How to extract request_id from request
DJANGO_RW_ROUTER_REQUEST_ID_ATTR = 'id'  # Default
```

### QuerySet Configuration

```python
# settings.py

# Force all PrimaryQuerySet reads to use primary
DJANGO_RW_ROUTER_QUERYSET_USING_ENABLE = True

# Enable @use_primary_db decorator for specific methods
DJANGO_RW_ROUTER_METHOD_USING_ENABLE = True
```

## Usage Examples

### Basic Usage

```python
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=100)

# Writes go to 'default', reads go to 'readonly'
book = Book.objects.create(title="Django Guide")  # Write to primary
books = Book.objects.all()  # Read from replica
```

### Transaction-Aware Reads

```python
from django.db import transaction

with transaction.atomic():
    book = Book.objects.create(title="New Book")
    # This read goes to PRIMARY (not replica) because we're in a transaction
    fresh_book = Book.objects.get(id=book.id)
```

### Using PrimaryManager for Critical Queries

```python
from django.db import models
from django_rw_router.managers import PrimaryManager

class ImportantModel(models.Model):
    objects = PrimaryManager()

# When DJANGO_RW_ROUTER_QUERYSET_USING_ENABLE=True,
# all reads through this manager go to primary
obj = ImportantModel.objects.first()
```

### Manual Database Selection

You can always override the router:

```python
# Force read from primary
obj = Book.objects.using('default').first()

# Force write to replica (not recommended)
book.save(using='readonly')
```

## How It Works

1. **Write Operations**: Always routed to the primary database (`default`)
2. **Read Operations**:
   - Outside transaction: Routed to random read replica
   - Inside transaction: Routed to primary (prevents stale reads)
3. **Hash Routing**: `user_id` is hashed to consistently select the same replica
4. **Context Tracking**: Middleware sets/clears request context automatically

## Requirements

- Python >= 3.8
- Django >= 3.2
- uhashring >= 2.1

## License

MIT License

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
