Relations¶
Here is a more complicated version of simple awokado usage. Awokado provides you with the possibility to easily build relations between entities.
Let’s take the Authors-Books one-to-many relation, for example.
Firstly, we need models:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #models.py
import sqlalchemy as sa
from awokado.db import DATABASE_URL
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Book(BaseModel):
__tablename__ = "books"
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
author_id = sa.Column(
sa.Integer,
sa.ForeignKey("authors.id", onupdate="CASCADE", ondelete="SET NULL"),
index=True,
)
description = sa.Column(sa.Text)
title = sa.Column(sa.Text)
class Author(BaseModel):
__tablename__ = "authors"
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
first_name = sa.Column(sa.Text, nullable=False)
last_name = sa.Column(sa.Text, nullable=False)
e = create_engine(DATABASE_URL)
Base.metadata.create_all(e)
|
Secondly, we write resources for each entity connected with their models.
Bind Book to Author using the ToOne awokado custom_field. Resource argument is the name field of Meta class in Author resource we’re connecting to, model_field argument is the field in Book model where Author unique identifier is stored.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #resources.py
import sqlalchemy as sa
from awokado import custom_fields
from awokado.consts import CREATE, READ, UPDATE
from awokado.resource import BaseResource
from awokado.utils import ReadContext
from marshmallow import fields
import models as m
class BookResource(BaseResource):
class Meta:
model = m.Book
name = "book"
methods = (CREATE, READ, UPDATE)
id = fields.Int(model_field=m.Book.id)
title = fields.String(model_field=m.Book.title, required=True)
description = fields.String(model_field=m.Book.description)
author = custom_fields.ToOne(
resource="author", model_field=m.Book.author_id
)
|
The continuation of building the connection is in the Author resource. Here we define another end of connection by the ToMany field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class AuthorResource(Resource):
class Meta:
model = m.Author
name = "author"
methods = (CREATE, READ, UPDATE)
select_from = sa.outerjoin(
m.Author, m.Book, m.Author.id == m.Book.author_id
)
id = fields.Int(model_field=m.Author.id)
books = custom_fields.ToMany(
fields.Int(),
resource="book",
model_field=m.Book.id,
description="Authors Books",
)
books_count = fields.Int(
dump_only=True, model_field=sa.func.count(m.Book.id)
)
name = fields.String(
model_field=sa.func.concat(
m.Author.first_name, " ", m.Author.last_name
),
dump_only=True,
)
last_name = fields.String(
model_field=m.Author.last_name, required=True, load_only=True
)
first_name = fields.String(
model_field=m.Author.first_name, required=True, load_only=True
)
|
So finally here are the methods where we add logic for getting connected entities.
Pay attention, that methods should be named with following template - “get_by_<name>_ids”, where “<name>” is Meta class name option in resource you connecting to.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | #BookResource
def get_by_author_ids(
self, session, ctx: ReadContext, field: sa.Column = None
):
authors = sa.func.array_remove(
sa.func.array_agg(m.Author.id), None
).label("authors")
q = (
sa.select(
[
m.Book.id.label("id"),
m.Book.title.label("title"),
m.Book.description.label("description"),
authors,
]
)
.select_from(
sa.outerjoin(m.Book, m.Author, m.Author.id == m.Book.author_id)
)
.where(m.Book.author_id.in_(ctx.obj_ids))
.group_by(m.Book.id)
)
result = session.execute(q).fetchall()
serialized_objs = self.dump(result, many=True)
return serialized_objs
#AuthorResource
def get_by_book_ids(
self, session, ctx: ReadContext, field: sa.Column = None
):
books_count = self.fields.get("books_count").metadata["model_field"]
q = (
sa.select(
[
m.Author.id.label("id"),
self.fields.get("name")
.metadata["model_field"]
.label("name"),
books_count.label("books_count"),
]
)
.select_from(
sa.outerjoin(m.Author, m.Book, m.Author.id == m.Book.author_id)
)
.where(m.Book.id.in_(ctx.obj_ids))
.group_by(m.Author.id)
)
result = session.execute(q).fetchall()
serialized_objs = self.dump(result, many=True)
return serialized_objs
|
Add routes, so resources can handle requests:
1 2 3 4 5 | app = falcon.API()
api.add_route("/v1/author/", AuthorResource())
api.add_route("/v1/author/{resource_id}", AuthorResource())
api.add_route("/v1/book/", BookResource())
api.add_route("/v1/book/{resource_id}", BookResource())
|
Test it using curl in terminal.
Create entities using following curl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | curl localhost:8000/v1/author --data-binary '{"author":{"last_name": "B","first_name": "Sier"}}' --compressed -v | python -m json.tool
{
"author": [
{
"books": [],
"books_count": 0,
"id": 1,
"name": "Sier B"
}
]
}
curl localhost:8000/v1/book --data-binary '{"book":{"title":"some_title","description":"some_description", "author":"1"}}' --compressed -v | python -m json.tool
{
"book": [
{
"author": 1,
"description": "some_description",
"id": 1,
"title": "some_title"
}
]
}
|
And then, with read request see what you’ve got:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | curl localhost:8000/v1/author?include=books | python -m json.tool
{
"meta": {
"total": 1
},
"payload": {
"author": [
{
"books": [
1
],
"books_count": 1,
"id": 1,
"name": "Sier B"
}
],
"book": [
{
"description": "some_description",
"id": 1,
"title": "some_title"
}
]
}
}
curl localhost:8000/v1/book?include=author | python -m json.tool
{
"meta": {
"total": 1
},
"payload": {
"author": [
{
"books_count": 1,
"id": 1,
"name": "Sier B"
}
],
"book": [
{
"author": 1,
"description": "some_description",
"id": 1,
"title": "some_title"
}
]
}
}
|