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.