Reference ForeignKey¶
This is so special and unique to Edgy and rarely seen (if ever) that deserves its own page in the documentation!
What is a Reference ForeignKey¶
Well for start it is not a normal ForeignKey. The reason why calling RefForeignKey it is because of its own unique type of functionality and what it can provide when it comes to insert records in the database.
This object does not create any foreign key in the database for you, mostly because this type literally does not exist. Instead is some sort of a mapper that can coexist inside your model declaration and help you with some automated tasks.
Warning
The RefForeignKey its only used for insertion of records and not for updates. Be very careful not to create duplicates and make those normal mistakes.
As mentioned above, RefForeignKey
will always create (even on save()
) records, it won't
update if they exist.
Brief explanation¶
In a nutshell, to use the RefForeignKey you will need to use a ModelRef.
The ModelRef is a special Edgy object that will make sure you can interact with the model declared and perform the operations.
Now, what is this useful? Let us imagine the following scenario:
Scenario example¶
You want to create a blog or anything that has users
and posts
. Something like this:
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
name: str = edgy.CharField(max_length=255)
class Meta:
registry = models
class Post(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
Quite simple so far. Now the normal way of creating users
and posts
would be 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")
Simple, right? What if there was another way of doing this? This is where the RefForeignKey gets in.
RefForeignKey¶
A RefForeignKey is internally interpreted as a list of the model declared in the ModelRef.
How to import it:
from edgy import RefForeignKey
Or
from edgy.core.db.fields import RefForeignKey
When using the RefForeignKey
it make it mandatory to populate the to
with a ModelRef
type
of object or it will raise a ModelReferenceError
.
Parameters¶
- to - To which ModelRef it should point.
-
null - If the RefForeignKey should allow nulls when an instance of your model is created.
Warning
This is for when an instance is created, not saved, which means it will run the normal Pydantic validations upon the creation of the object.
ModelRef¶
This is another special type of object unique to Edgy. It is what allows you to interact with the RefForeignKey and use it properly.
from edgy import ModelRef
Or
from edgy.core.db.models import ModelRef
The ModelRef
when creating and declaring it makes it mandatory to populate the __related_name__
attribute or else it won't know what to do and it will raise a ModelReferenceError
. This is good and
means you can't miss it even if you wanted to.
The __related_name__
attribute should point to a Relation (reverse side of ForeignKey or ManyToMany relation).
The ModelRef
is a special type from the Pydantic BaseModel
which means you can take advantage
of everything that Pydantic can do for you, for example the field_validator
or model_validator
or anything you could normally use with a normal Pydantic model.
Attention¶
You need to be careful when declaring the fields of the ModelRef
because that will be used
against the __related_name__
declared. If the model on the reverse end of the relationship has constraints
, uniques
and so on
you will need to respect it when you are about to insert in the database.
It is also not possible to cross multiple models (except the through model in ManyToMany).
Declaring a ModelRef¶
When creating a ModelRef
, as mentioned before, you need to declare the __related_name__
field pointing
to the Relation you want that reference to be.
Let us be honest, would just creating the __related_name__
be enough for what we want to achieve? No.
In the ModelRef
you must also specify the fields you want to have upon the instantiation of
that model.
Let us see an example how to declare the ModelRef for a specific model.
from datetime import datetime
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class Post(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
comment: str = edgy.TextField()
created_at: datetime = edgy.DateTimeField(auto_now_add=True)
class Meta:
registry = models
First we have a model already created which is the database table representation as per normal design, then we can create a model reference for that same model.
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 if you want to have everything in one place.
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):
id: int = edgy.IntegerField(primary_key=True)
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
Another way of thinking what fields should I put in the ModelRef is:
What minimum fields would I need to create a object of type X using the ModelRef?
This usually means, you should put at least the not null fields of the model you are referencing.
How to use¶
Well, now that we talked about the RefForeignKey and the ModelRef, it is time to see exactly how to use both in your models and to take advantage.
Do you remember the scenario above? If not, no worries, let us see it again.
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
name: str = edgy.CharField(max_length=255)
class Meta:
registry = models
class Post(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
In the scenario above we also showed how to insert and associate the posts with the user but now it is time to use the RefForeignKey instead.
What do we needed:
- The ModelRef object.
- The RefForeignKey field (Optionally, you can pass ModelRef instances also as positional argument).
Now it is time to readapt the scenario example to adopt the 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):
id: int = edgy.IntegerField(primary_key=True)
name: str = edgy.CharField(max_length=255)
posts: List["Post"] = edgy.RefForeignKey(PostRef)
class Meta:
registry = models
class Post(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
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"}],
)
That is it, you simply declare the ModelRef created for the Post
model and pass it
to the posts
of the User
model inside the RefForeignKey. In our example, the posts
is not null.
Note
As mentioned before, the RefForeignKey does not create a field in the database. This is for internal Edgy model purposes only.
More structured¶
The previous example has everything in one place but 99% of times you will want to have the references
somewhere else and just import them. A dedicated references.py
file for instance.
With this idea in mind, now it kinda makes a bit more sense doesn't it? Something like this:
from edgy import ModelRef
class PostRef(ModelRef):
__related_name__ = "posts_set"
comment: str
And the models with the 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):
id: int = edgy.IntegerField(primary_key=True)
name: str = edgy.CharField(max_length=255)
posts: List["Post"] = edgy.RefForeignKey(PostRef)
class Meta:
registry = models
class Post(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
user: User = edgy.ForeignKey(User)
comment: str = edgy.TextField()
class Meta:
registry = models
Here an example using the 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):
id: int = edgy.IntegerField(primary_key=True)
name: str = edgy.CharField(max_length=255)
class Meta:
registry = models
class Post(edgy.Model):
id: int = edgy.IntegerField(primary_key=True)
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 the results¶
Now that we refactored the code to have the ModelRef we will also readapt the way we insert in the database 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 the 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 will now will make sure that creates all the proper objects and associated IDs in the corresponding
order, first the user
followed by the post
and associates that user with the created post
automatically.
Ok, this is great and practical sure but coding wise, it is also very similar to the original way, right? Yes and no.
What if we were to apply the ModelRef and the RefForeignKey in a proper API call? Now, that would be interesting to see wouldn't it?
Using in API¶
As per almost everything in the documentation, Edgy will use Esmerald as an example. Let us see the advantage of using this new approach directly there and enjoy.
You can see the RefForeignKey as some sort of nested object.
The beauty of RefForeignKey is the automatic conversion of dicts, so it is interoperable with many APIs.
Declare the models, views and ModelRef¶
Let us create the models, views and ModelRef for our /create
API to use.
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)
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
See that we are adding some extra information in the response of our /create
API just to make
sure you can then check the results accordingly.
Making the API call¶
Now that we have everything in place, its time to create a user
and at the same time create some
posts
directly.
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)
Now this is a beauty, isn't it? Now we can see the advantage of having the ModelRef. The API call it is so much cleaner and simple and nested that one API makes it all.
The response
The if you check the response, you should see something similar to this.
{
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description",
"comment": "A COMMENT",
"total_posts": 4,
}
Remember adding the comment
and total_posts
? Well this is why, just to confirm the total inserted
and the comment of the first inserted,
Errors¶
As per normal Pydantic validations, if you send the wrong payload, it will raise the corresponding errors, for example:
{
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description"
}
This will raise a ValidationError
as the posts
are not null, as expected and you should
have something similar to this as response:
{
"type": "missing",
"loc": ["posts"],
"msg": "Field required",
"input": {
"name": "Edgy",
"email": "edgy@esmerald.dev",
"language": "EN",
"description": "A description",
},
}
Sending the wrong type¶
The RefForeignKey is always expecting a list to be sent, if you try to send the wrong
type, it will raise a ValidationError
, something similar to this:
If we have sent a dictionary instead of a list
{
"type": "list_type",
"loc": ["posts"],
"msg": "Input should be a valid list",
"input": {"comment": "A comment"},
}
Conclusion¶
This is an extensive document just for one field type but it deserves as it is complex and allows you to simplify a lot your code when you want to insert records in the database all in one go.