Skip to content

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:

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

Create a model reference:

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:

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

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:

references.py
from edgy import ModelRef


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

Models with 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

Using 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 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

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

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.