Skip to content

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 id field (auto-increment integer)
  • Query managers: .query for workspace-scoped queries, .global_query for all data
  • FastEdgy integration: Automatic service injection and context management
  • Timestamps: Optional created_at and updated_at fields

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

await product.delete()

# Bulk delete
await Product.query.filter(price__lt=10).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.workspace reads 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.