Skip to content

Commit aa299a2

Browse files
committed
✨ (Many-to-many) Add SQLAlchemy section with many-to-many relationships
1 parent 0b98f3b commit aa299a2

File tree

25 files changed

+1302
-0
lines changed

25 files changed

+1302
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
title: Changes in this section
3+
description: In this section we add Tags to our Stores, and link these to Items using a many-to-many relationship.
4+
---
5+
6+
# Changes in this section
7+
8+
It's common for online stores to use "tags" to group items and to be able to search for them a bit more easily.
9+
10+
For example, an item "Chair" could be tagged with "Furniture" and "Office".
11+
12+
Another item, "Laptop", could be tagged with "Tech" and "Office".
13+
14+
So one item can be associated with many tags, and one tag can be associated with many items.
15+
16+
This is a many-to-many relationship, which is bit trickier to implement than the one-to-many we've already implemented between Items and Stores.
17+
18+
## When you have many stores
19+
20+
We want to add one more constraint to tags, however. That is that if we have many stores, it's possible each store wants to use different tags. So the tags we create will be unique to each store.
21+
22+
This means that tags will have:
23+
24+
- A many-to-one relationship with stores
25+
- A many-to-many relationship with items
26+
27+
Here's a diagram to illustrate what this looks like:
28+
29+
![ER database model showing relationships](./assets/db_model.drawio.png)
30+
31+
## New API endpoints to be added
32+
33+
In this section we will add all the Tag endpoints:
34+
35+
36+
| Method | Endpoint | Description |
37+
| -------- | ----------------------- | ------------------------------------------------------- |
38+
| `GET` | `/stores/{id}/tags` | Get a list of tags in a store. |
39+
| `POST` | `/stores/{id}/tags` | Create a new tag. |
40+
| `POST` | `/items/{id}/tags/{id}` | Link an item in a store with a tag from the same store. |
41+
| `DELETE` | `/items/{id}/tags/{id}` | Unlink a tag from an item. |
42+
| `GET` | `/tags/{id}` | Get information about a tag given its unique id. |
43+
| `DELETE` | `/tags/{id}` | Delete a tag, which must have no associated items. |
113 KB
Loading
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: One-to-many relationships review
3+
description: A super-quick look at creating the Tag model and setting up the one-to-many relationship with Stores.
4+
---
5+
6+
# One-to-many relationship between Tag and Store
7+
8+
Since we've already learned how to set up one-to-many relationships with SQLAlchemy when we looked at Items and Stores, let's go quickly in this section.
9+
10+
## The SQLAlchemy models
11+
12+
import Tabs from '@theme/Tabs';
13+
import TabItem from '@theme/TabItem';
14+
15+
<div className="codeTabContainer">
16+
<Tabs>
17+
<TabItem value="tag" label="models/tag.py" default>
18+
19+
```python title="models/tag.py"
20+
from db import db
21+
22+
23+
class TagModel(db.Model):
24+
__tablename__ = "tags"
25+
26+
id = db.Column(db.Integer, primary_key=True)
27+
name = db.Column(db.String(80), unique=True, nullable=False)
28+
store_id = db.Column(db.String(), db.ForeignKey("stores.id"), nullable=False)
29+
30+
store = db.relationship("StoreModel", back_populates="tags")
31+
```
32+
33+
</TabItem>
34+
<TabItem value="store" label="models/store.py">
35+
36+
```python title="models/store.py"
37+
from db import db
38+
39+
40+
class StoreModel(db.Model):
41+
__tablename__ = "stores"
42+
43+
id = db.Column(db.Integer, primary_key=True)
44+
name = db.Column(db.String(80), unique=True, nullable=False)
45+
46+
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
47+
# highlight-start
48+
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
49+
# highlight-end
50+
```
51+
52+
</TabItem>
53+
</Tabs>
54+
</div>
55+
56+
## The marshmallow schemas
57+
58+
These are the new schemas we'll add. Note that none of the tag schemas have any notion of "items". We'll add those to the schemas when we construct the many-to-many relationship.
59+
60+
In the `StoreSchema` we add a new list field for the nested `PlainTagSchema`, just as it has with `PlainItemSchema`.
61+
62+
```python title="schemas.py"
63+
class PlainTagSchema(Schema):
64+
id = fields.Int(dump_only=True)
65+
name = fields.Str()
66+
67+
68+
class StoreSchema(PlainStoreSchema):
69+
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
70+
# highlight-start
71+
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
72+
# highlight-end
73+
74+
75+
class TagSchema(PlainTagSchema):
76+
store_id = fields.Int(load_only=True)
77+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
78+
```
79+
80+
## The API endpoints
81+
82+
Let's add the Tag endpoints that aren't related to Items:
83+
84+
85+
| Method | Endpoint | Description |
86+
| ---------- | ----------------------- | ------------------------------------------------------- |
87+
|`GET` | `/stores/{id}/tags` | Get a list of tags in a store. |
88+
|`POST` | `/stores/{id}/tags` | Create a new tag. |
89+
|`POST` | `/items/{id}/tags/{id}` | Link an item in a store with a tag from the same store. |
90+
|`DELETE` | `/items/{id}/tags/{id}` | Unlink a tag from an item. |
91+
|`GET` | `/tags/{id}` | Get information about a tag given its unique id. |
92+
|`DELETE` | `/tags/{id}` | Delete a tag, which must have no associated items. |
93+
94+
Here's the code we need to write to add these endpoints:
95+
96+
```python title="resources/tag.py"
97+
from flask.views import MethodView
98+
from flask_smorest import Blueprint, abort
99+
from sqlalchemy.exc import SQLAlchemyError
100+
101+
from db import db
102+
from models import TagModel, StoreModel
103+
from schemas import TagSchema
104+
105+
blp = Blueprint("Tags", "tags", description="Operations on tags")
106+
107+
108+
@blp.route("/stores/<string:store_id>/tags")
109+
class TagsInStore(MethodView):
110+
@blp.response(200, TagSchema(many=True))
111+
def get(self, store_id):
112+
store = StoreModel.query.get_or_404(store_id)
113+
114+
return store.tags.all()
115+
116+
@blp.arguments(TagSchema)
117+
@blp.response(201, TagSchema)
118+
def post(self, tag_data, store_id):
119+
if TagModel.query.filter(TagModel.store_id == store_id).first():
120+
abort(400, message="A tag with that name already exists in that store.")
121+
122+
tag = TagModel(**tag_data, store_id=store_id)
123+
124+
try:
125+
db.session.add(tag)
126+
db.session.commit()
127+
except SQLAlchemyError as e:
128+
abort(
129+
500,
130+
message=str(e),
131+
)
132+
133+
return tag
134+
135+
136+
@blp.route("/tags/<string:tag_id>")
137+
class Tag(MethodView):
138+
@blp.response(200, TagSchema)
139+
def get(self, tag_id):
140+
tag = TagModel.query.get_or_404(tag_id)
141+
return tag
142+
143+
```

0 commit comments

Comments
 (0)