Relations (Rel)¶
Helpers em strider/relations.py para colunas de chave estrangeira e relationship() do SQLAlchemy com convenções próximas ao Django.
Dois formatos de string (não confundir)¶
| Uso | Formato | Exemplo | Onde |
|---|---|---|---|
| Coluna FK SQL | tabela.coluna (≥ um ponto; com schema, usam-se os dois últimos segmentos como tabela e coluna) |
"users.id", "public.accounts.id" |
Rel.foreign_key |
Alvo de relationship |
app_label.ModelName (regex: um ponto, identificadores válidos) |
"core.User" |
many_to_one, one_to_many, one_to_one, many_to_many |
app_label: pasta do app emsrc/apps(ex.:core,strategies).ModelName: nome da classe no módulosrc.apps.<app_label>.models.- Resolução:
importlib.import_module("src.apps.<app_label>.models")egetattr(..., ModelName).
Erros na resolução do modelo (relationship)¶
ValueError:targetnão coincide com o padrãoapp_label.ModelName.ImportError: módulosrc.apps.<app_label>.modelsnão importável.AttributeError: classe não existe nesse módulo.
Validação estrita (sys.exit(0))¶
Se usar foreign_keys=[...] como lista de nomes de atributos (strings) em many_to_one / one_to_many / one_to_one, o framework instala um descriptor que chama _validate_relationship_target. Se target não for app_label.ModelName, imprime-se uma mensagem em stderr e o processo termina com código 0 (falha explícita em desenvolvimento).
Rel.foreign_key¶
Rel.foreign_key(
target: str, # "tabela.coluna"
*,
nullable: bool = False,
ondelete: str = "CASCADE",
index: bool = True,
type_: str | None = None, # None = inferir int / uuid / bigint
)
type_:None(padrão) infere o tipo SQLAlchemy a partir de umModelcujo__tablename__seja a tabela referenciada (PK ou coluna). O modelo alvo deve estar já definido quando a classe que declara a FK é construída (ordem de declaração / imports). Se não for encontrado, assume-se inteiro.- Valores explícitos:
"int","uuid","bigint"sobrescrevem a inferência. ondeletetípicos:CASCADE,SET NULL(combinar comnullable=True),RESTRICT,NO ACTION.
from uuid import UUID
from sqlalchemy.orm import Mapped
from strider import Model, Field
from strider.relations import Rel
class Post(Model):
__tablename__ = "posts"
author_id: Mapped[int] = Rel.foreign_key("users.id")
workspace_id: Mapped[UUID] = Rel.foreign_key("workspaces.id") # inferência UUID se o modelo workspaces tiver PK UUID
category_id: Mapped[int | None] = Rel.foreign_key(
"categories.id",
nullable=True,
ondelete="SET NULL",
)
Rel.many_to_one / Rel.belongs_to¶
Lado N de N:1. target = "app_label.ModelName".
Rel.many_to_one(
target: str,
*,
back_populates: str | None = None,
backref: str | None = None,
lazy: str = "selectin",
foreign_keys: list[str] | None = None,
uselist: bool = False,
)
Rel.one_to_many / Rel.has_many¶
Lado 1 de 1:N.
Rel.one_to_many(
target: str,
*,
back_populates: str | None = None,
backref: str | None = None,
lazy: str = "selectin",
foreign_keys: list[str] | None = None,
cascade: str = "all, delete-orphan",
passive_deletes: bool = True,
order_by: str | None = None,
)
Rel.one_to_one / Rel.has_one¶
1:1; no lado que possui a FK, use unique=True na coluna (ou equivalente) para garantir unicidade.
Rel.many_to_many¶
Rel.many_to_many(
target: str,
*,
secondary: str | Table,
back_populates: str | None = None,
backref: str | None = None,
lazy: str = "selectin",
cascade: str = "all",
passive_deletes: bool = True,
order_by: str | None = None,
)
secondary: nome da tabela (string) ou objetoTable. Se a tabela foi criada comAssociationTable.createe está em cache, o nome pode resolver para essa instância.
Rel.self_referential¶
Relação na mesma classe. foreign_keys e remote_side são nomes de atributos Python na classe (ex.: "parent_id", "id"), não strings qualificadas com o nome da classe.
class Category(Model):
__tablename__ = "categories"
id: Mapped[int] = Field.pk()
parent_id: Mapped[int | None] = Rel.foreign_key(
"categories.id",
nullable=True,
ondelete="SET NULL",
)
parent: Mapped["Category | None"] = Rel.self_referential(
back_populates="children",
remote_side="id",
uselist=False,
)
children: Mapped[list["Category"]] = Rel.self_referential(
back_populates="parent",
foreign_keys="parent_id",
)
AssociationTable¶
Cria tabelas de junção para N:N com duas FKs inteiras por defeito. Para PKs UUID/bigint nas entidades, construa a Table manualmente com os tipos correctos.
post_tags = AssociationTable.create(
"post_tags",
left=("post_id", "posts.id"),
right=("tag_id", "tags.id"),
)
get("post_tags")— obtém tabela em cache.clear_cache()— limpa o cache (testes).
clear_model_cache¶
Limpa cache interno de importação de models em relations.py (uso principal: testes).
Lazy loading¶
| Valor | Comportamento |
|---|---|
"selectin" |
SELECT IN separado (recomendado, padrão) |
"joined" |
JOIN na mesma query |
"subquery" |
Subquery |
"select" |
Lazy clássico |
"raise" / "noload" |
Conforme SQLAlchemy |
Cascade (SQLAlchemy)¶
| Valor | Comportamento |
|---|---|
"all" |
Operações em cascata amplas |
"save-update" |
Propaga save/update |
"merge" |
Propaga merge |
"delete" |
Propaga delete |
"delete-orphan" |
Remove órfãos |
"all, delete-orphan" |
Comum em 1:N (padrão em one_to_many) |
ON DELETE (FK / foreign_key)¶
| Valor | Comportamento |
|---|---|
"CASCADE" |
Apaga filhos quando o pai é apagado (padrão) |
"SET NULL" |
Põe FK a NULL (requer coluna anulável) |
"RESTRICT" / "NO ACTION" |
Impede ou delega ao motor |
Exemplo completo (targets app_label.ModelName)¶
from strider import Model, Field
from strider.relations import Rel
from sqlalchemy.orm import Mapped
class User(Model):
__tablename__ = "users"
id: Mapped[int] = Field.pk()
posts: Mapped[list["Post"]] = Rel.one_to_many(
"core.Post",
back_populates="author",
)
class Post(Model):
__tablename__ = "posts"
id: Mapped[int] = Field.pk()
author_id: Mapped[int] = Rel.foreign_key("users.id")
author: Mapped["User"] = Rel.many_to_one(
"core.User",
back_populates="posts",
)
(Ajuste core ao app_label real do projecto.)
Eager loading em queries¶
user = await User.objects.using(db).select_related("posts").get(id=1)
post = await Post.objects.using(db).select_related("author").get(id=1)