Annotating Queries¶
Annotating queries in Edgy presents some unique challenges due to the use of hash functions with prefix (select_related) paths for generating stable join names. However, if you primarily use subqueries, these issues are less likely to arise.
Annotating a Child with Parent Columns¶
When working with nested relationships, you might need to access parent columns from a child element. The reference_select
function addresses this, functioning similarly to Django's F()
function but with broader applicability.
import sqlalchemy
import edgy
models = edgy.Registry(database=...)
class User(edgy.Model):
name = edgy.CharField(max_length=100)
class Meta:
registry = models
class Profile(edgy.Model):
user = edgy.fields.OneToOne(User, related_name="profile")
name = edgy.CharField(max_length=100)
class Meta:
registry = models
for profile in await Profile.query.select_related("profile").reference_select(
{"user": {"profile_name": "name"}}
):
assert profile.user.profile_name == profile.name
# up to one level you can leave out the select_related()
# you can also reference columns in case you use them of the main table or explicitly provided via extra_select
for profile in await Profile.query.reference_select(
{"user": {"profile_name": Profile.table.c.name}}
):
assert profile.user.profile_name == profile.name
Explanation:
- The
reference_select
function allows you to include columns from related tables (parents) in your query results. - In this example, it's used to bring parent columns into the child's result set, making parent data accessible alongside child data.
import sqlalchemy
import edgy
models = edgy.Registry(database=...)
class User(edgy.StrictModel):
name = edgy.CharField(max_length=100)
profile_name = edgy.fields.PlaceholderField(null=True)
class Meta:
registry = models
class Profile(edgy.Model):
user = edgy.fields.OneToOne(User, related_name="profile")
name = edgy.CharField(max_length=100)
class Meta:
registry = models
for profile in await Profile.query.select_related("profile").reference_select(
{"user": {"profile_name": "name"}}
):
assert profile.user.profile_name == profile.name
# up to one level you can leave out the select_related()
for profile in await Profile.query.reference_select({"user": {"profile_name": "name"}}):
assert profile.user.profile_name == profile.name
Explanation:
- When using
StrictModel
, every attribute in the query result must correspond to a defined field. PlaceHolderField(null=True)
is used to reserve attribute names for assignment, ensuring that the query results can be correctly mapped to the model's fields.- This is crucial for maintaining type safety and preventing unexpected errors when working with strictly defined models.
Warning
With StrictModel
, ensure that all query result names match defined fields. Use PlaceHolderField(null=True)
to reserve attribute names for assignment.
Annotating and embed_parent
¶
reference_select
executes before embedding, enabling the addition of parent attributes to grandchild elements.
import sqlalchemy
import edgy
models = edgy.Registry(database=...)
class User(edgy.Model):
name = edgy.CharField(max_length=100)
class Meta:
registry = models
class Profile(edgy.Model):
user = edgy.fields.OneToOne(User, related_name="profile")
name = edgy.CharField(max_length=100)
profile = edgy.fields.OneToOne(
"SuperProfile", related_name="profile", embed_parent=("user", "normal_profile")
)
class Meta:
registry = models
class SuperProfile(edgy.Model):
name = edgy.CharField(max_length=100)
class Meta:
registry = models
for profile in await SuperProfile.query.all():
user = (
await profile.profile.select_related("user")
.reference_select({"user": {"profile_name": "name"}})
.get()
)
assert isinstance(user, User)
assert user.normal_profile.name == user.profile_name
Explanation:
- The
embed_parent
feature allows you to include parent data when fetching related children. - By using
reference_select
before embedding, you can augment the data available to the grandchild with additional attributes from the parent. - This is particularly useful for deeply nested relationships where you need to access data from multiple levels.
Annotating Child Attributes to the Parent¶
reference_select
operates from the perspective of the main query, allowing child attributes to be set via reference_select({"user": {"user_name", "user__name"}})
on the child.
Alternatively, you can manually use the hash_tablekey
helper function (though this is not recommended).
import sqlalchemy
import edgy
from edgy.core.utils.db import hash_tablekey
models = edgy.Registry(database=...)
class User(edgy.Model):
name = edgy.CharField(max_length=100)
class Meta:
registry = models
class Profile(edgy.Model):
user = edgy.fields.OneToOne(User, related_name="profile")
name = edgy.CharField(max_length=100)
class Meta:
registry = models
for profile in await Profile.query.select_related("user").reference_select(
{"user_name": sqlalchemy.text(f"user__name")}
):
assert profile.user_name == profile.user.name
# manual way
join_table_key = hash_tablekey(tablekey=User.table.key, prefix="user")
for profile in await Profile.query.select_related("user").reference_select(
{"user_name": sqlalchemy.text(f"{join_table_key}_name")}
):
assert profile.user_name == profile.user.name
Explanation:
reference_select
allows you to pull data not only from parent tables to children, but also from children to parents.- The dictionary passed to
reference_select
specifies the relationship and the fields to be included. - In this example, child attributes (e.g.,
user_name
,user__name
) are being added to the parent's result set. - Using
hash_tablekey
manually is possible but generally discouraged due to its complexity and potential for errors.
Annotating and Subqueries¶
Beyond referencing child-parent structures, you can reference arbitrary select statements using extra_select
.
import sqlalchemy
from sqlalchemy import func
import edgy
models = edgy.Registry(database=...)
class User(edgy.Model):
name = edgy.CharField(max_length=100)
class Meta:
registry = models
class Profile(edgy.Model):
user = edgy.fields.OneToOne(User, related_name="profile")
name = edgy.CharField(max_length=100)
class Meta:
registry = models
for profile in await Profile.query.extra_select(
func.count()
.select()
.select_from((await User.query.as_select()).subquery())
.label("total_number")
).reference_select({"total_number": "total_number"}):
assert profile.total_number == 10
# or manually
for profile in await Profile.query.extra_select(
sqlalchemy.select(func.count(User.table.c.id).label("total_number")).subquery()
).reference_select({"total_number": "total_number"}):
assert profile.total_number >= 0
Explanation:
extra_select
allows you to include custom SQL expressions or subqueries in your query results.- You can add multiple
extra_select
entries, each with a unique label to prevent collisions. - This provides flexibility to include calculated values or data from other tables that are not directly related through model relationships.
- The example shows how to add a subquery that counts the number of posts associated with each user.
Furthermore, you can reference Common Table Expressions (CTEs) by knowing their name or providing the column.
Explanation:
- CTEs, like subqueries, can be used to add complex data to your queries.
- By referencing a CTE's name or providing the necessary column information, you can include the results of the CTE in your query.
- This allows for more advanced data manipulation and aggregation within your queries.
- This is very useful for optimizing complex queries by breaking them down into simpler, reusable parts.