Models¶
FastEdgy models are based on Edgy ORM and provide a powerful, type-safe way to define your database schema and interact with data.
Basic model definition¶
from fastedgy.models.base import BaseModel
from fastedgy.orm import fields
class Product(BaseModel):
name = fields.CharField(max_length=255)
description = fields.TextField()
price = fields.DecimalField(max_digits=10, decimal_places=2)
is_active = fields.BooleanField(default=True)
class Meta:
tablename = "products"
Base model features¶
All FastEdgy models inherit from the base model, which provides:
- Automatic ID: Primary key
idfield (auto-increment integer) - Query managers:
.queryfor workspace-scoped queries,.global_queryfor all data - FastEdgy integration: Automatic service injection and context management
- Timestamps: Optional
created_atandupdated_atfields
Model operations¶
Create¶
product = Product(name="Laptop", description="High-performance laptop", price=999.99)
await product.save()
Query¶
# Get by ID
product = await Product.query.get(id=1)
# Filter
products = await Product.query.filter(is_active=True).all()
# Complex queries
expensive_products = await Product.query.filter(price__gte=500, is_active=True).order_by("-price").all()
Update¶
product.price = 899.99
await product.save()
# Bulk update
await Product.query.filter(is_active=False).update(is_active=True)
Delete¶
Relationships between models¶
class Category(BaseModel):
name = fields.CharField(max_length=100)
class Meta:
tablename = "categories"
class Product(BaseModel):
name = fields.CharField(max_length=255)
category = fields.ForeignKey(Category, on_delete="CASCADE")
tags = fields.ManyToMany("Tag", related_name="products")
class Meta:
tablename = "products"
class Tag(BaseModel):
name = fields.CharField(max_length=50)
class Meta:
tablename = "tags"
Model configuration¶
class Product(BaseModel):
name = fields.CharField(max_length=255)
class Meta:
tablename = "products"
# Optional configurations
abstract = False # True for abstract base models
registry = None # Auto-set by FastEdgy
# Database table options
indexes = [
fields.Index(fields=["name"]),
]
constraints = [fields.UniqueConstraint(fields=["name"], name="unique_product_name")]
Read-only fields¶
Mark a field read_only=True when it must never be set from an API input — a platform role, an immutable owner, a value derived server-side:
class WorkspaceUser(BaseModel):
workspace = fields.ForeignKey(Workspace, read_only=True)
user = fields.ForeignKey(User, read_only=True)
role = fields.CharChoiceField(choices=Role, default=Role.member)
Read-only fields are excluded from the generated input schemas, so no POST, PATCH or sync payload can write them. Every regular write path drops a read-only column: Model(field=...) at construction, attribute assignment before save(), save(values={...}), update(...) on an instance or on a queryset. A read-only foreign key is guarded at the API boundary only: save() writes the relation the instance holds, whether given to the constructor, assigned, or read with the row. This makes read-only columns safe by default, but it also means server code cannot set them the usual way.
A stale copy writes its read-only relations back
A full save() writes every read-only foreign key the instance holds, changed or not. An instance read before another request moved one (a membership re-pointed to another workspace, an owner transferred) puts the old value back when it is saved. Write only what changed with save(values={...}), or read the row again before saving.
Setting read-only fields from code¶
Use apply_readonly_values(...), the explicit code-side escape hatch on BaseModel. It stages the values and the next save() persists them:
membership = WorkspaceUser().apply_readonly_values({"workspace": workspace, "user": user})
membership.role = Role.admin
await membership.save()
- The staged values also apply to the in-memory instance immediately (so
membership.workspacereads back). - They are consumed by the next
save()and then cleared — call it again for a later write. - An unknown field name raises
ValueError.
This keeps the field immutable at the API boundary while leaving it fully writable from trusted code (creation factories, CLI commands, data seeding).
Setting read-only fields through a queryset¶
update() on a queryset drops the read-only fields it is handed. The queryset has the same apply_readonly_values(...): it returns a queryset carrying the values, and every update() it runs writes them along with its own arguments:
from fastedgy.orm.filter import R
await WorkspaceUser.query.filter(R("user", "=", user_id)).apply_readonly_values({"is_default": False}).update()
- The values survive further chaining (
.apply_readonly_values(...).filter(...)). - Only
update()reads them:create(),get_or_create()and the writes of an instance ignore them. - Like any queryset write, the update is not announced by
@realtime_model. - An unknown field name raises
ValueError.
Automatic API integration¶
Integrate with FastEdgy's API route generator:
from fastedgy.api_route_model import api_route_model
@api_route_model()
class Product(BaseModel):
name = fields.CharField(max_length=255)
price = fields.DecimalField(max_digits=10, decimal_places=2)
class Meta:
tablename = "products"
# Automatically generates:
# GET /api/products/ - List products
# POST /api/products/ - Create product
# GET /api/products/{id} - Get product
# PATCH /api/products/{id} - Update product
# DELETE /api/products/{id} - Delete product
Advanced capabilities¶
- Signals: Pre/post save, delete hooks (see Edgy documentation)
- Custom managers: Define custom query managers
- Model inheritance: Abstract base models and multi-table inheritance
- Database functions: Use SQL functions in queries
- Raw SQL: Execute custom SQL when needed
Best practices¶
- Use descriptive model and field names
- Add appropriate constraints and indexes
- Keep models focused (single responsibility)
- Use relationships to normalize data
- Leverage base model features for consistency
For more advanced ORM features, see the Edgy ORM documentation.