Reference ForeignKey (RefForeignKey)¶
The Reference ForeignKey (RefForeignKey) is a unique feature in Edgy that simplifies the creation of related records.
What is a Reference ForeignKey?¶
Unlike a standard ForeignKey, a RefForeignKey does not create a foreign key constraint in the database. Instead, it acts as a mapper that facilitates automated record insertion.
Warning
RefForeignKey is only used for inserting records, not updating them. Exercise caution to avoid creating duplicates.
RefForeignKey always creates new records, even on save(), rather than updating existing ones.
Brief Explanation¶
To use RefForeignKey, you'll need a ModelRef.
ModelRef is an Edgy object that enables interaction with the declared model and performs operations.
Scenario Example
Consider a blog with users and posts:
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
class Meta:
registry = models
class Post(edgy.Model):
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
Typically, you'd create users and posts like this:
# Create the user
user = await User.query.create(name="Edgy")
# Create posts and associate with the user
await Post.query.create(user=user, comment="A comment")
await Post.query.create(user=user, comment="Another comment")
await Post.query.create(user=user, comment="A third comment")
RefForeignKey offers an alternative approach.
RefForeignKey¶
RefForeignKey is internally treated as a list of the model declared in ModelRef.
Import it:
from edgy import RefForeignKey
Or
from edgy.core.db.fields import RefForeignKey
RefForeignKey requires the to parameter to be a ModelRef object; otherwise, it raises a ModelReferenceError.
Parameters¶
- to: The ModelRef to point to.
-
null: Whether to allow nulls when creating a model instance.
Warning
This applies during instance creation, not saving. It performs Pydantic validations.
ModelRef¶
ModelRef is a special Edgy object for interacting with RefForeignKey.
from edgy import ModelRef
Or
from edgy.core.db.models import ModelRef
ModelRef requires the __related_name__ attribute to be populated; otherwise, it raises a ModelReferenceError.
__related_name__ should point to a Relation (reverse side of ForeignKey or ManyToMany relation).
ModelRef is a Pydantic BaseModel, allowing you to use Pydantic features like field_validator and model_validator.
Attention¶
When declaring ModelRef fields, ensure they align with the __related_name__ model's constraints and uniques.
You cannot cross multiple models (except the through model in ManyToMany).
Declaring a ModelRef¶
Declare the __related_name__ field and specify the fields for instantiation.
Example:
from datetime import datetime
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class Post(edgy.Model):
comment: str = edgy.TextField()
created_at: datetime = edgy.DateTimeField(auto_now_add=True)
class Meta:
registry = models
Create a model reference:
from datetime import datetime
from edgy import Database, ModelRef, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class PostRef(ModelRef):
__related_name__ = "posts_set"
comment: str
created_at: datetime
Or:
from datetime import datetime
import edgy
from edgy import Database, ModelRef, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class Post(edgy.Model):
comment: str = edgy.TextField()
created_at: datetime = edgy.DateTimeField(auto_now_add=True)
class Meta:
registry = models
class PostRef(ModelRef):
__related_name__ = "posts_set"
comment: str
created_at: datetime
Include at least the non-null fields of the referenced model.
How to Use¶
Combine RefForeignKey and ModelRef in your models.
Scenario Example (Revisited)
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
class Meta:
registry = models
class Post(edgy.Model):
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
Use RefForeignKey instead:
In a Nutshell¶
from typing import List
import edgy
from edgy import Database, ModelRef, Registry, run_sync
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class PostRef(ModelRef):
__related_name__ = "posts_set"
comment: str
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
posts: List["Post"] = edgy.RefForeignKey(PostRef)
class Meta:
registry = models
class Post(edgy.Model):
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
# now we do things like
run_sync(
User.query.create(
PostRef(comment="foo"),
PostRef(comment="bar"),
),
name="edgy",
posts=[{"comment": "I am a dict"}],
)
Declare the ModelRef for the Post model and pass it to the posts field of the User model.
Note
RefForeignKey does not create a database field. It's for internal Edgy model purposes.
More Structured¶
Separate references into a references.py file:
from edgy import ModelRef
class PostRef(ModelRef):
__related_name__ = "posts_set"
comment: str
Models with imports:
from typing import List
import edgy
from edgy import Database, Registry
from .references import PostRef
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
posts: List["Post"] = edgy.RefForeignKey(PostRef)
class Meta:
registry = models
class Post(edgy.Model):
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
Using ModelRefs without RefForeignKey:
from typing import List
import edgy
from edgy import Database, Registry, run_sync
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class PostRef(ModelRef):
__related_name__ = "posts_set"
comment: str
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
class Meta:
registry = models
class Post(edgy.Model):
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
# This time completely without a RefForeignKey
run_sync(
User.query.create(
PostRef(comment="foo"),
PostRef(comment="bar"),
)
)
Writing Results¶
Adapt the insertion method from the scenario:
Old Way:
# Create the user
user = await User.query.create(name="Edgy")
# Create posts and associate with the user
await Post.query.create(user=user, comment="A comment")
await Post.query.create(user=user, comment="Another comment")
await Post.query.create(user=user, comment="A third comment")
Using ModelRef:
# Create the posts using PostRef model
post1 = PostRef(comment="A comment")
post2 = PostRef(comment="Another comment")
post3 = PostRef(comment="A third comment")
# Create the usee with all the posts
await User.query.create(name="Edgy", posts=[post1, post2, post3])
# or positional (Note: because posts has not null=True, we need still to provide the argument)
await User.query.create(post1, post2, post3, name="Edgy", posts=[])
This ensures proper object creation and association.
Using in API¶
Use RefForeignKey as a nested object in your API.
Declare Models, Views, and ModelRef¶
from esmerald import Esmerald, Gateway, post
from pydantic import field_validator
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class PostRef(edgy.ModelRef):
__related_name__ = "posts_set"
comment: str
@field_validator("comment", mode="before")
def validate_comment(cls, comment: str) -> str:
"""
We want to store the comments as everything uppercase.
"""
comment = comment.upper()
return comment
class User(edgy.Model):
id: int = edgy.IntegerField(primary_key=True, autoincrement=True)
name: str = edgy.CharField(max_length=100)
email: str = edgy.EmailField(max_length=100)
language: str = edgy.CharField(max_length=200, null=True)
description: str = edgy.TextField(max_length=5000, null=True)
posts: PostRef = edgy.RefForeignKey(PostRef)
class Meta:
registry = models
class Post(edgy.Model):
user = edgy.ForeignKey("User")
comment = edgy.CharField(max_length=255)
class Meta:
registry = models
@post("/create")
async def create_user(data: User) -> User:
"""
We want to create a user and update the return model
with the total posts created for that same user and the
comment generated.
"""
user = await data.save()
posts = await Post.query.filter(user=user)
return_user = user.model_dump(exclude={"posts"})
return_user["total_posts"] = len(posts)
return_user["comment"] = posts[0].comment
return return_user
def app():
app = models.asgi(
Esmerald(
routes=[Gateway(handler=create_user)],
)
)
return app
Making the API Call¶
import httpx
data = {
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description",
"posts": [
{"comment": "First comment"},
{"comment": "Second comment"},
{"comment": "Third comment"},
{"comment": "Fourth comment"},
],
}
# Make the API call to create the user with some posts
# This will also create the posts and associate them with the user
# All the posts will be in uppercase as per `field_validator` in the ModelRef.
response = httpx.post("/create", json=data)
Response:
{
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description",
"comment": "A COMMENT",
"total_posts": 4,
}
Errors¶
Pydantic validations apply:
{
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description"
}
Response:
{
"type": "missing",
"loc": ["posts"],
"msg": "Field required",
"input": {
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description",
},
}
Wrong Type¶
RefForeignKey expects a list:
{
"type": "item_type",
"loc": ["posts"],
"msg": "Input should be a valid list",
"input": {"comment": "A comment"},
}
Conclusion¶
RefForeignKey and ModelRef simplify database record insertion, especially in APIs.