Skip to content

Commit 81c4c5a

Browse files
authored
Merge pull request #51 from tecladocode/jose/cou-103-write-sqlalchemy-many-to-many-seection
2 parents 0b98f3b + 22842eb commit 81c4c5a

File tree

98 files changed

+2562
-17
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+2562
-17
lines changed

docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class PlainStoreSchema(Schema):
107107

108108
class ItemSchema(PlainItemSchema):
109109
store_id = fields.Int(required=True, load_only=True)
110-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
110+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
111111

112112

113113
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):

docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):
1414

1515
class ItemSchema(PlainItemSchema):
1616
store_id = fields.Int(required=True, load_only=True)
17-
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
17+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
1818

1919

2020
class ItemUpdateSchema(Schema):
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. |
Loading
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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+
- [x] Set metadata above
7+
- [x] Start writing!
8+
- [x] Create `start` folder
9+
- [x] Create `end` folder
10+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
11+
12+
# One-to-many relationship between Tag and Store
13+
14+
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.
15+
16+
## The SQLAlchemy models
17+
18+
import Tabs from '@theme/Tabs';
19+
import TabItem from '@theme/TabItem';
20+
21+
<div className="codeTabContainer">
22+
<Tabs>
23+
<TabItem value="tag" label="models/tag.py" default>
24+
25+
```python title="models/tag.py"
26+
from db import db
27+
28+
29+
class TagModel(db.Model):
30+
__tablename__ = "tags"
31+
32+
id = db.Column(db.Integer, primary_key=True)
33+
name = db.Column(db.String(80), unique=True, nullable=False)
34+
store_id = db.Column(db.String(), db.ForeignKey("stores.id"), nullable=False)
35+
36+
store = db.relationship("StoreModel", back_populates="tags")
37+
```
38+
39+
</TabItem>
40+
<TabItem value="store" label="models/store.py">
41+
42+
```python title="models/store.py"
43+
from db import db
44+
45+
46+
class StoreModel(db.Model):
47+
__tablename__ = "stores"
48+
49+
id = db.Column(db.Integer, primary_key=True)
50+
name = db.Column(db.String(80), unique=True, nullable=False)
51+
52+
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
53+
# highlight-start
54+
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
55+
# highlight-end
56+
```
57+
58+
</TabItem>
59+
</Tabs>
60+
</div>
61+
62+
## The marshmallow schemas
63+
64+
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.
65+
66+
In the `StoreSchema` we add a new list field for the nested `PlainTagSchema`, just as it has with `PlainItemSchema`.
67+
68+
```python title="schemas.py"
69+
class PlainTagSchema(Schema):
70+
id = fields.Int(dump_only=True)
71+
name = fields.Str()
72+
73+
74+
class StoreSchema(PlainStoreSchema):
75+
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
76+
# highlight-start
77+
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
78+
# highlight-end
79+
80+
81+
class TagSchema(PlainTagSchema):
82+
store_id = fields.Int(load_only=True)
83+
store = fields.Nested(PlainStoreSchema(), dump_only=True)
84+
```
85+
86+
## The API endpoints
87+
88+
Let's add the Tag endpoints that aren't related to Items:
89+
90+
91+
| Method | Endpoint | Description |
92+
| ---------- | ----------------------- | ------------------------------------------------------- |
93+
|`GET` | `/stores/{id}/tags` | Get a list of tags in a store. |
94+
|`POST` | `/stores/{id}/tags` | Create a new tag. |
95+
|`POST` | `/items/{id}/tags/{id}` | Link an item in a store with a tag from the same store. |
96+
|`DELETE` | `/items/{id}/tags/{id}` | Unlink a tag from an item. |
97+
|`GET` | `/tags/{id}` | Get information about a tag given its unique id. |
98+
|`DELETE` | `/tags/{id}` | Delete a tag, which must have no associated items. |
99+
100+
Here's the code we need to write to add these endpoints:
101+
102+
```python title="resources/tag.py"
103+
from flask.views import MethodView
104+
from flask_smorest import Blueprint, abort
105+
from sqlalchemy.exc import SQLAlchemyError
106+
107+
from db import db
108+
from models import TagModel, StoreModel
109+
from schemas import TagSchema
110+
111+
blp = Blueprint("Tags", "tags", description="Operations on tags")
112+
113+
114+
@blp.route("/stores/<string:store_id>/tags")
115+
class TagsInStore(MethodView):
116+
@blp.response(200, TagSchema(many=True))
117+
def get(self, store_id):
118+
store = StoreModel.query.get_or_404(store_id)
119+
120+
return store.tags.all()
121+
122+
@blp.arguments(TagSchema)
123+
@blp.response(201, TagSchema)
124+
def post(self, tag_data, store_id):
125+
if TagModel.query.filter(TagModel.store_id == store_id).first():
126+
abort(400, message="A tag with that name already exists in that store.")
127+
128+
tag = TagModel(**tag_data, store_id=store_id)
129+
130+
try:
131+
db.session.add(tag)
132+
db.session.commit()
133+
except SQLAlchemyError as e:
134+
abort(
135+
500,
136+
message=str(e),
137+
)
138+
139+
return tag
140+
141+
142+
@blp.route("/tags/<string:tag_id>")
143+
class Tag(MethodView):
144+
@blp.response(200, TagSchema)
145+
def get(self, tag_id):
146+
tag = TagModel.query.get_or_404(tag_id)
147+
return tag
148+
```
149+
150+
## Register the Tag blueprint in `app.py`
151+
152+
Finally, we need to remember to import the blueprint and register it!
153+
154+
```python title="app.py"
155+
from flask import Flask
156+
from flask_smorest import Api
157+
158+
import models
159+
160+
from db import db
161+
from resources.item import blp as ItemBlueprint
162+
from resources.store import blp as StoreBlueprint
163+
# highlight-start
164+
from resources.tag import blp as TagBlueprint
165+
# highlight-end
166+
167+
168+
def create_app(db_url=None):
169+
app = Flask(__name__)
170+
app.config["API_TITLE"] = "Stores REST API"
171+
app.config["API_VERSION"] = "v1"
172+
app.config["OPENAPI_VERSION"] = "3.0.3"
173+
app.config["OPENAPI_URL_PREFIX"] = "/"
174+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
175+
app.config[
176+
"OPENAPI_SWAGGER_UI_URL"
177+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
178+
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
179+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
180+
app.config["PROPAGATE_EXCEPTIONS"] = True
181+
db.init_app(app)
182+
api = Api(app)
183+
184+
@app.before_first_request
185+
def create_tables():
186+
db.create_all()
187+
188+
api.register_blueprint(ItemBlueprint)
189+
api.register_blueprint(StoreBlueprint)
190+
# highlight-start
191+
api.register_blueprint(TagBlueprint)
192+
# highlight-end
193+
194+
return app
195+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_ENV=development
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM python:3.10
2+
EXPOSE 5000
3+
WORKDIR /app
4+
COPY ./requirements.txt requirements.txt
5+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
6+
COPY . .
7+
CMD ["flask", "run", "--host", "0.0.0.0"]

0 commit comments

Comments
 (0)