Include nested and related, Many-to-Many
The same as usual includes. Here’s an example with an association object.
Example (sources here):
Prepare models and schemas
Define SQLAlchemy models
Parent model
models/parent.py
:
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from examples.api_for_sqlalchemy.extensions.sqlalchemy import Base
from examples.api_for_sqlalchemy.utils.sqlalchemy.base_model_mixin import BaseModelMixin
class Parent(Base, BaseModelMixin):
__tablename__ = "left_table_parents"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
children = relationship(
"ParentToChildAssociation",
back_populates="parent",
)
Child model
models/child.py
:
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from examples.api_for_sqlalchemy.extensions.sqlalchemy import Base
from examples.api_for_sqlalchemy.utils.sqlalchemy.base_model_mixin import BaseModelMixin
class Child(Base, BaseModelMixin):
__tablename__ = "right_table_children"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
parents = relationship(
"ParentToChildAssociation",
back_populates="child",
)
Parent to Child Association model
models/parent_child_association.py
:
from sqlalchemy import Column, ForeignKey, Index, Integer, String
from sqlalchemy.orm import relationship
from examples.api_for_sqlalchemy.extensions.sqlalchemy import Base
from examples.api_for_sqlalchemy.utils.sqlalchemy.base_model_mixin import BaseModelMixin
class ParentToChildAssociation(Base, BaseModelMixin):
__table_args__ = (
# JSON:API requires `id` field on any model,
# so we can't create a composite PK here
# that's why we need to create this index
Index(
"ix_parent_child_association_unique",
"parent_left_id",
"child_right_id",
unique=True,
),
)
__tablename__ = "parent_to_child_association_table"
id = Column(Integer, primary_key=True, autoincrement=True)
parent_left_id = Column(
ForeignKey("left_table_parents.id"),
nullable=False,
)
child_right_id = Column(
ForeignKey("right_table_children.id"),
nullable=False,
)
extra_data = Column(String(50))
parent = relationship(
"Parent",
back_populates="children",
# primaryjoin="ParentToChildAssociation.parent_left_id == Parent.id",
)
child = relationship(
"Child",
back_populates="parents",
# primaryjoin="ParentToChildAssociation.child_right_id == Child.id",
)
Define pydantic schemas
Parent Schema
schemas/parent.py
:
from typing import TYPE_CHECKING, List
from fastapi_jsonapi.schema_base import BaseModel, Field, RelationshipInfo
if TYPE_CHECKING:
from .parent_child_association import ParentToChildAssociationSchema
class ParentBaseSchema(BaseModel):
"""Parent base schema."""
class Config:
orm_mode = True
name: str
children: List["ParentToChildAssociationSchema"] = Field(
default=None,
relationship=RelationshipInfo(
resource_type="parent_child_association",
many=True,
),
)
class ParentPatchSchema(ParentBaseSchema):
"""Parent PATCH schema."""
class ParentInSchema(ParentBaseSchema):
"""Parent input schema."""
class ParentSchema(ParentInSchema):
"""Parent item schema."""
id: int
Child Schema
schemas/child.py
:
from typing import TYPE_CHECKING, List
from fastapi_jsonapi.schema_base import BaseModel, Field, RelationshipInfo
if TYPE_CHECKING:
from .parent_child_association import ParentToChildAssociationSchema
class ChildBaseSchema(BaseModel):
"""Child base schema."""
class Config:
orm_mode = True
name: str
parents: List["ParentToChildAssociationSchema"] = Field(
default=None,
relationship=RelationshipInfo(
resource_type="parent_child_association",
many=True,
),
)
class ChildPatchSchema(ChildBaseSchema):
"""Child PATCH schema."""
class ChildInSchema(ChildBaseSchema):
"""Child input schema."""
class ChildSchema(ChildInSchema):
"""Child item schema."""
id: int
Parent to Child Association Schema
schemas/parent_child_association.py
:
from typing import TYPE_CHECKING
from fastapi_jsonapi.schema_base import BaseModel, Field, RelationshipInfo
if TYPE_CHECKING:
from .child import ChildSchema
from .parent import ParentSchema
class ParentToChildAssociationSchema(BaseModel):
id: int
extra_data: str
parent: "ParentSchema" = Field(
default=None,
relationship=RelationshipInfo(
resource_type="parent",
),
)
child: "ChildSchema" = Field(
default=None,
relationship=RelationshipInfo(
resource_type="child",
),
)
Define view classes
Base Views
api/base.py
:
from typing import ClassVar, Dict
from fastapi import Depends
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from examples.api_for_sqlalchemy.extensions.sqlalchemy import Connector
from fastapi_jsonapi.data_layers.sqla_orm import SqlalchemyDataLayer
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase
class SessionDependency(BaseModel):
session: AsyncSession = Depends(Connector.get_session)
class Config:
arbitrary_types_allowed = True
def handler(view: ViewBase, dto: SessionDependency) -> Dict:
return {"session": dto.session}
class DetailViewBase(DetailViewBaseGeneric):
"""
Generic view base (detail)
"""
data_layer_cls = SqlalchemyDataLayer
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=handler,
),
}
class ListViewBase(ListViewBaseGeneric):
"""
Generic view base (list)
"""
data_layer_cls = SqlalchemyDataLayer
method_dependencies: ClassVar = {
HTTPMethod.ALL: HTTPMethodConfig(
dependencies=SessionDependency,
prepare_data_layer_kwargs=handler,
),
}
List Parent objects with Children through an Association object
Request:
GET /parents?include=children%2Cchildren.child HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{
"attributes": {
"name": "parent_1"
},
"id": "1",
"relationships": {
"children": {
"data": [
{
"id": "1",
"type": "parent_child_association"
},
{
"id": "3",
"type": "parent_child_association"
}
]
}
},
"type": "parent"
},
{
"attributes": {
"name": "parent_2"
},
"id": "2",
"relationships": {
"children": {
"data": [
{
"id": "2",
"type": "parent_child_association"
},
{
"id": "4",
"type": "parent_child_association"
},
{
"id": "5",
"type": "parent_child_association"
}
]
}
},
"type": "parent"
},
{
"attributes": {
"name": "parent_3"
},
"id": "3",
"relationships": {
"children": {
"data": []
}
},
"type": "parent"
}
],
"included": [
{
"attributes": {
"name": "child_1"
},
"id": "1",
"type": "child"
},
{
"attributes": {
"name": "child_2"
},
"id": "2",
"type": "child"
},
{
"attributes": {
"name": "child_3"
},
"id": "3",
"type": "child"
},
{
"attributes": {
"extra_data": "assoc_p1c1_extra"
},
"id": "1",
"relationships": {
"child": {
"data": {
"id": "1",
"type": "child"
}
}
},
"type": "parent_child_association"
},
{
"attributes": {
"extra_data": "assoc_p2c1_extra"
},
"id": "2",
"relationships": {
"child": {
"data": {
"id": "1",
"type": "child"
}
}
},
"type": "parent_child_association"
},
{
"attributes": {
"extra_data": "assoc_p1c2_extra"
},
"id": "3",
"relationships": {
"child": {
"data": {
"id": "2",
"type": "child"
}
}
},
"type": "parent_child_association"
},
{
"attributes": {
"extra_data": "assoc_p2c2_extra"
},
"id": "4",
"relationships": {
"child": {
"data": {
"id": "2",
"type": "child"
}
}
},
"type": "parent_child_association"
},
{
"attributes": {
"extra_data": "assoc_p2c3_extra"
},
"id": "5",
"relationships": {
"child": {
"data": {
"id": "3",
"type": "child"
}
}
},
"type": "parent_child_association"
}
],
"jsonapi": {
"version": "1.0"
},
"meta": {
"count": 3,
"totalPages": 1
}
}