Custom Database Models
DDD CQRS
Protean auto-generates database models for every aggregate and entity, and for every projection stored in a database rather than a cache. Custom database models let you override the default storage schema when you need adapter-specific tuning, custom table names, Elasticsearch analyzers, or a different model for each database type.
When to use custom models
Most applications don't need custom models. Use them when:
- You need to override the table or collection name
- You need adapter-specific field types (e.g., Elasticsearch
Textwith a custom analyzer) - One aggregate runs on different database types in different deployments (e.g., PostgreSQL in one, Elasticsearch in another), and each needs its own model
- You need partial field mapping (persist only a subset of fields)
If your fields map 1:1 to standard database types, the auto-generated model is sufficient.
Defining a custom model
Subclass BaseDatabaseModel and register it with part_of:
from protean.core.database_model import BaseDatabaseModel
@domain.aggregate
class Product:
name = String(required=True)
description = Text()
price = Float()
class ProductModel(BaseDatabaseModel):
pass # Empty -- just override the schema name
domain.register(ProductModel, part_of=Product, schema_name="products")
Overriding field types
Map aggregate fields to adapter-specific types:
from elasticsearch.dsl import Keyword
from elasticsearch.dsl import Text as ESText
class ProductSearchModel(BaseDatabaseModel):
name = Keyword() # Exact match, no analysis
description = ESText(analyzer="standard") # Full-text search
domain.register(
ProductSearchModel,
part_of=Product,
database="elasticsearch",
)
Partial field mapping
A model can map fewer fields than the aggregate. Protean auto-generates the mapping for each field the model leaves out:
from elasticsearch.dsl import Text as ESText
@domain.aggregate
class Article:
title = String(required=True)
body = Text()
category = String()
class ArticleSearchModel(BaseDatabaseModel):
body = ESText(analyzer="english") # Override only this field
# title and category use the default mapping
domain.register(ArticleSearchModel, part_of=Article)
Registration options
| Option | Type | Description |
|---|---|---|
part_of |
class or str | Required. The aggregate, entity or projection this model maps to. A string names the element and is resolved when domain.init() runs, so the model can be declared before its target. |
schema_name |
str | Override the storage table/collection name |
database |
str | The database type this model applies to: memory, sqlite, postgresql, mysql, mssql or elasticsearch. This is not a provider name. It has no default: without it, the model is used for any database type that has no model of its own. |
class ProductReportingModel(BaseDatabaseModel):
pass
domain.register(
ProductReportingModel,
part_of=Product,
schema_name="product_reports",
database="postgresql",
)
Multi-database deployment
Register multiple models for the same aggregate, each targeting a different database type:
@domain.aggregate
class Customer:
name = String(required=True)
email = String()
class CustomerWriteModel(BaseDatabaseModel):
pass
class CustomerSearchModel(BaseDatabaseModel):
name = Keyword()
domain.register(
CustomerWriteModel,
part_of=Customer,
database="postgresql",
schema_name="customers",
)
domain.register(
CustomerSearchModel,
part_of=Customer,
database="elasticsearch",
schema_name="customer_index",
)
An aggregate is stored in one provider, set with its provider option. Its
repository uses the model whose database matches that provider's database
type. For example, Customer above uses CustomerSearchModel on Elasticsearch
and CustomerWriteModel on PostgreSQL. The aggregate is not written to both.
Validation rules
- Model fields must be a subset of the aggregate's fields. Defining
a field that doesn't exist on the aggregate raises
IncorrectUsageError. - A model can have fewer fields than the aggregate (partial mapping).
- A model cannot add fields not present on the aggregate.
See also
- Adapters Reference: Provider-specific configuration and field types.
- Custom Databases: Building your own database adapter.