Models¶
Define your data structure using Python classes that map directly to PostgreSQL tables.
What is a Model?¶
A model is a Python class that represents a table in your database. Each model:
- Defines what data you want to store (fields like title, email, price)
- Maps to a PostgreSQL table automatically
- Provides methods to create, read, update, and delete records
- Validates data before saving
Think of a model as a blueprint: it describes what a "Post" or "User" looks like, and Aksara handles all the database work.
from aksara import Model, fields
class Article(Model):
"""A blog article stored in the 'articles' table."""
title = fields.String(max_length=200)
content = fields.Text()
published = fields.Boolean(default=False)
created_at = fields.DateTime(auto_now_add=True)
What this creates:
PostgreSQL table: articles
┌──────────────┬─────────────────┬─────────────┐
│ Column │ Type │ Constraints │
├──────────────┼─────────────────┼─────────────┤
│ id │ UUID │ PRIMARY KEY │
│ title │ VARCHAR(200) │ NOT NULL │
│ content │ TEXT │ NOT NULL │
│ published │ BOOLEAN │ DEFAULT false│
│ created_at │ TIMESTAMPTZ │ AUTO │
└──────────────┴─────────────────┴─────────────┘
Creating a Model¶
Step 1: Import the Base Class and Fields¶
Model— The base class all your models inherit fromfields— Contains all field types (String, Integer, Boolean, etc.)
Step 2: Define Your Class¶
class Task(Model):
"""A task that users can complete."""
title = fields.String(max_length=200)
description = fields.Text(nullable=True)
completed = fields.Boolean(default=False)
due_date = fields.DateTime(nullable=True)
Step 3: Create the Table¶
Run migrations to create the actual PostgreSQL table:
What You Get Automatically¶
Primary Key (id)¶
Every model automatically gets a UUID primary key. You don't need to define it:
class User(Model):
email = fields.Email(unique=True)
# 'id' is automatically added as a UUID
user = await User.objects.create(email="alice@example.com")
print(user.id) # UUID('550e8400-e29b-41d4-a716-446655440000')
Why UUID instead of auto-increment?
- Globally unique (safe for distributed systems)
- Can be generated client-side
- No sequential pattern (better security)
- PostgreSQL-native with
gen_random_uuid()
Table Name¶
The table name is derived from your class name:
| Model Name | Table Name |
|---|---|
Article |
articles |
User |
users |
Category |
categories |
UserProfile |
user_profiles |
Override the table name if needed:
class Article(Model):
__tablename__ = "blog_posts" # Custom table name
title = fields.String(max_length=200)
Timestamps¶
Add automatic timestamps with auto_now and auto_now_add:
class Article(Model):
title = fields.String(max_length=200)
# Set once when created, never changes
created_at = fields.DateTime(auto_now_add=True)
# Updated every time you save
updated_at = fields.DateTime(auto_now=True)
Working with Models¶
Creating Records¶
# Method 1: Create and save in one step
article = await Article.objects.create(
title="Hello World",
content="My first article!",
published=True,
)
# Method 2: Create an instance, then save
article = Article(
title="Hello World",
content="My first article!",
)
await article.save()
Reading Records¶
# Get one record by ID
article = await Article.objects.get(id=article_id)
# Get one record by any field
article = await Article.objects.get(title="Hello World")
# Get all records
articles = await Article.objects.all()
# Filter records
published = await Article.objects.filter(published=True)
Updating Records¶
# Method 1: Modify and save
article = await Article.objects.get(id=article_id)
article.title = "New Title"
await article.save()
# Method 2: Update multiple at once
await Article.objects.filter(published=False).update(published=True)
Deleting Records¶
# Delete one record
article = await Article.objects.get(id=article_id)
await article.delete()
# Delete multiple records
await Article.objects.filter(published=False).delete()
Field Types¶
Here's a quick reference of common fields:
Text Fields¶
class Example(Model):
# Short text with max length
name = fields.String(max_length=100)
# Unlimited text
bio = fields.Text()
# Email with validation
email = fields.Email(unique=True)
# URL with validation
website = fields.URL(nullable=True)
Numeric Fields¶
class Product(Model):
# Whole numbers
quantity = fields.Integer(default=0)
# Decimal numbers (for money)
price = fields.Decimal(max_digits=10, decimal_places=2)
# Floating point (for calculations)
rating = fields.Float(nullable=True)
Date and Time¶
class Event(Model):
# Date only (no time)
date = fields.Date()
# Time only (no date)
start_time = fields.Time()
# Full timestamp with timezone
created_at = fields.DateTime(auto_now_add=True)
Other Types¶
class Settings(Model):
# True/False
is_active = fields.Boolean(default=True)
# JSON data (dictionaries, lists)
preferences = fields.JSON(default=dict)
# PostgreSQL arrays
tags = fields.Array(base_type="text", default=list)
# UUID
external_id = fields.UUID(nullable=True)
See Fields Reference for complete documentation.
Field Options¶
All fields accept these common options:
| Option | What It Does | Example |
|---|---|---|
nullable |
Allow NULL values | fields.String(nullable=True) |
default |
Default value | fields.Boolean(default=False) |
unique |
Ensure uniqueness | fields.Email(unique=True) |
db_index |
Create database index | fields.String(db_index=True) |
primary_key |
Mark as primary key | fields.UUID(primary_key=True) |
Examples¶
class User(Model):
# Required, must be unique
email = fields.Email(unique=True)
# Optional (can be empty)
phone = fields.String(max_length=20, nullable=True)
# Has a default value
is_active = fields.Boolean(default=True)
# Indexed for fast lookups
username = fields.String(max_length=50, unique=True, db_index=True)
Model Meta Options¶
Configure model-wide settings using the Meta class:
class Article(Model):
title = fields.String(max_length=200)
created_at = fields.DateTime(auto_now_add=True)
class Meta:
# Custom table name
table_name = "blog_articles"
# Default ordering (newest first)
ordering = ["-created_at"]
# Database indexes for performance
indexes = [
("title",), # Single column index
("created_at", "title"), # Composite index
]
# Unique together constraints
unique_together = [
("author_id", "slug"), # Same author can't have duplicate slugs
]
# App label for admin grouping
app_label = "blog"
Meta Options Reference¶
| Option | What It Does | Example |
|---|---|---|
table_name |
Custom database table name | "blog_posts" |
ordering |
Default sort order | ["-created_at"] |
indexes |
Database indexes | [("field1", "field2")] |
unique_together |
Multi-column uniqueness | [("user_id", "slug")] |
app_label |
Group in admin | "blog" |
AI Metadata¶
Aksara models can include metadata that helps AI agents understand your data:
class Product(Model):
name = fields.String(
max_length=200,
ai_description="The product's display name shown to customers",
)
price = fields.Decimal(
max_digits=10,
decimal_places=2,
ai_description="Price in USD, excluding tax",
)
internal_cost = fields.Decimal(
max_digits=10,
decimal_places=2,
ai_sensitive=True, # Hide from AI context
)
class Meta:
ai_description = "Products available for purchase in the store"
| Option | What It Does |
|---|---|
ai_description |
Human-readable description for AI |
ai_sensitive |
Hide field from AI context |
ai_agent_writable |
Allow/prevent AI from modifying |
Relationships¶
Models can reference other models:
from aksara import Model, fields, CASCADE
class Author(Model):
name = fields.String(max_length=100)
class Post(Model):
title = fields.String(max_length=200)
# Many posts can have one author
author = fields.ForeignKey(Author, on_delete=CASCADE, related_name="posts")
See Relations for complete documentation.
Complete Example¶
from aksara import Model, fields, CASCADE
from decimal import Decimal
class Category(Model):
"""Product categories like Electronics, Clothing, etc."""
name = fields.String(max_length=100, unique=True)
slug = fields.String(max_length=100, unique=True)
description = fields.Text(nullable=True)
class Meta:
ordering = ["name"]
app_label = "store"
class Product(Model):
"""Items available for purchase."""
# Basic info
name = fields.String(max_length=200)
slug = fields.String(max_length=200, unique=True)
description = fields.Text()
# Pricing
price = fields.Decimal(max_digits=10, decimal_places=2)
sale_price = fields.Decimal(max_digits=10, decimal_places=2, nullable=True)
# Inventory
sku = fields.String(max_length=50, unique=True)
stock_quantity = fields.Integer(default=0)
# Status
is_active = fields.Boolean(default=True)
is_featured = fields.Boolean(default=False)
# Relationships
category = fields.ForeignKey(
Category,
on_delete=CASCADE,
related_name="products",
)
# Metadata
tags = fields.Array(base_type="text", default=list)
attributes = fields.JSON(default=dict) # e.g., {"color": "red", "size": "M"}
# Timestamps
created_at = fields.DateTime(auto_now_add=True)
updated_at = fields.DateTime(auto_now=True)
class Meta:
ordering = ["-created_at"]
app_label = "store"
indexes = [
("category_id", "is_active"),
("sku",),
]
# Using the models
async def example():
# Create a category
electronics = await Category.objects.create(
name="Electronics",
slug="electronics",
description="Gadgets and devices",
)
# Create a product
phone = await Product.objects.create(
name="Smartphone X",
slug="smartphone-x",
description="Latest smartphone with amazing features",
price=Decimal("999.99"),
sku="PHONE-001",
stock_quantity=100,
category=electronics,
tags=["smartphone", "mobile", "5g"],
attributes={"color": "black", "storage": "256GB"},
)
# Query products
active_products = await Product.objects.filter(
is_active=True,
stock_quantity__gt=0,
).order_by("-created_at")
# Access relationship
for product in active_products:
category = await product.category
print(f"{product.name} in {category.name}")
Related Documentation¶
- Fields — All field types and options
- Querying — Filter, sort, and retrieve data
- Relations — ForeignKey, ManyToMany, OneToOne
- Migrations — Create and update tables