Skip to main content
Entities in Framefox are database models that inherit from AbstractEntity, which extends SQLModel with additional functionality.

AbstractEntity Class

The AbstractEntity class is the base class for all database entities in Framefox:
from sqlmodel import SQLModel

class AbstractEntity(SQLModel):
    __abstract__ = True
Reference: framefox/core/orm/abstract_entity.py:15

Key Methods

generate_create_model()

Generates a Pydantic model for creating new entities (excludes the id field):
@classmethod
def generate_create_model(cls) -> Type[BaseModel]:
    fields = {name: (field.annotation, ...) for name, field in cls.__fields__.items() if name != "id"}
    create_model_name = f"{cls.__name__}Create"
    create_model_class = create_model(create_model_name, **fields)
    return create_model_class
Reference: framefox/core/orm/abstract_entity.py:18

get_primary_keys()

Returns the list of primary key column names:
@classmethod
def get_primary_keys(cls: Type[SQLModel]) -> List[str]:
    mapper = inspect(cls)
    primary_keys = [key.name for key in mapper.primary_key]
    return primary_keys
Reference: framefox/core/orm/abstract_entity.py:32

generate_find_model()

Generates a Pydantic model for finding entities by primary key:
@classmethod
def generate_find_model(cls) -> Type[BaseModel]:
    mapper = inspect(cls)
    primary_keys = [key.name for key in mapper.primary_key]
    fields = {name: (field.annotation, ...) for name, field in cls.__fields__.items() if name in primary_keys}
    create_model_name = f"{cls.__name__}Find"
    create_model_class = create_model(create_model_name, **fields)
    return create_model_class
Reference: framefox/core/orm/abstract_entity.py:44

generate_patch_model()

Generates a Pydantic model for partial updates with all fields optional:
@classmethod
def generate_patch_model(cls) -> Type[BaseModel]:
    fields = {name: (Optional[field.annotation], None) for name, field in cls.__fields__.items() if name not in cls.get_primary_keys()}
    patch_model_name = f"{cls.__name__}Patch"
    patch_model_class = create_model(patch_model_name, **fields)
    return patch_model_class
Reference: framefox/core/orm/abstract_entity.py:59

Creating Entity Models

Define your database models by inheriting from AbstractEntity:
from typing import Optional
from sqlmodel import Field
from framefox.core.orm.abstract_entity import AbstractEntity

class User(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=100)
    email: str = Field(unique=True, index=True)
    age: Optional[int] = None
    is_active: bool = Field(default=True)

Key Components

  • table=True: Marks this class as a database table
  • Primary Key: Use Field(primary_key=True) for the primary key
  • Optional Fields: Use Optional[Type] with default values
  • Constraints: Use Field() parameters for database constraints

Field Types and Relationships

Basic Field Types

SQLModel supports standard Python types:
from datetime import datetime
from sqlmodel import Field

class Article(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str = Field(max_length=200)
    content: str
    view_count: int = Field(default=0)
    rating: float = Field(default=0.0)
    published_at: Optional[datetime] = None
    is_published: bool = Field(default=False)

Field Constraints

Use Field() to add constraints and metadata:
class Product(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    
    # Unique constraint
    sku: str = Field(unique=True, max_length=50)
    
    # Index for faster queries
    category: str = Field(index=True)
    
    # Default values
    quantity: int = Field(default=0)
    
    # Numeric constraints
    price: float = Field(gt=0)  # Greater than 0
    discount: float = Field(ge=0, le=100)  # Between 0 and 100
    
    # String length
    description: str = Field(max_length=1000)

Relationships

Define relationships between entities using SQLModel’s relationship syntax:
from typing import Optional, List
from sqlmodel import Field, Relationship

# One-to-Many: Author has many Books
class Author(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    books: List["Book"] = Relationship(back_populates="author")

class Book(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    author_id: Optional[int] = Field(default=None, foreign_key="author.id")
    author: Optional[Author] = Relationship(back_populates="books")

# Many-to-Many: Books and Tags
class BookTagLink(AbstractEntity, table=True):
    book_id: Optional[int] = Field(default=None, foreign_key="book.id", primary_key=True)
    tag_id: Optional[int] = Field(default=None, foreign_key="tag.id", primary_key=True)

class Book(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    tags: List["Tag"] = Relationship(link_model=BookTagLink)

class Tag(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(unique=True)
    books: List[Book] = Relationship(link_model=BookTagLink)

Pydantic Validation

Since AbstractEntity extends SQLModel (which uses Pydantic), you get automatic validation:
from pydantic import validator, EmailStr
from sqlmodel import Field

class User(AbstractEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(min_length=3, max_length=100)
    email: EmailStr  # Validates email format
    age: int = Field(ge=0, le=150)  # Between 0 and 150
    
    @validator('name')
    def name_must_not_contain_numbers(cls, v):
        if any(char.isdigit() for char in v):
            raise ValueError('name must not contain numbers')
        return v
    
    @validator('age')
    def age_must_be_adult(cls, v):
        if v < 18:
            raise ValueError('user must be 18 or older')
        return v

Validation Example

try:
    # This will raise a validation error
    user = User(name="John123", email="invalid", age=15)
except ValidationError as e:
    print(e.json())
    # [
    #   {"loc": ["name"], "msg": "name must not contain numbers"},
    #   {"loc": ["email"], "msg": "value is not a valid email address"},
    #   {"loc": ["age"], "msg": "user must be 18 or older"}
    # ]

Using Generated Models

The AbstractEntity provides methods to generate helper models:
# Create model (for POST requests)
UserCreate = User.generate_create_model()
user_data = UserCreate(name="Alice", email="[email protected]")

# Find model (for primary key lookup)
UserFind = User.generate_find_model()
user_find = UserFind(id=1)

# Patch model (for partial updates)
UserPatch = User.generate_patch_model()
user_update = UserPatch(email="[email protected]")  # Only update email

Best Practices

  1. Always use Optional for nullable fields: Be explicit about which fields can be None
  2. Set table=True: Don’t forget to mark entities as tables
  3. Use Field constraints: Leverage Field() for validation and database constraints
  4. Index frequently queried fields: Add index=True to fields used in WHERE clauses
  5. Validate at the model level: Use Pydantic validators for business logic validation
  6. Document relationships: Use clear relationship names with back_populates

Next Steps

Repositories

Learn how to query and persist entities

Query Builder

Build complex queries

Build docs developers (and LLMs) love