Skip to content

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):
    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

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.

The original 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):
    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.

The 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 if you want to have everything in one place.

The model reference
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

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):
    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

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:

  1. The ModelRef object.
  2. 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):
    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"}],
)

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:

references.py
from edgy import ModelRef


class PostRef(ModelRef):
    __related_name__ = "posts_set"
    comment: str

And the models with the imports.

models.py
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

Here an example using the ModelRefs without RefForeignKey:

models.py
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 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.

app.py
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

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.