|
| 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