Skip to content

Commit

Permalink
Merge pull request #100 from tecladocode/jose/cou-160-rest-delete-err…
Browse files Browse the repository at this point in the history
…ors-due-to-not-null
  • Loading branch information
jslvtr authored Dec 7, 2022
2 parents d5d9827 + 683659d commit cb95115
Show file tree
Hide file tree
Showing 26 changed files with 459 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Delete models with relationships
description: Tell SQLAlchemy what to do with related models when you delete the parent.
---

# Delete models with relationships using cascades

When you delete a model that has a relationship to other models that still exist, the default behavior in SQLAlchemy with PostgreSQL is to raise an error. This is because SQLAlchemy does not want to allow you to accidentally delete data that is still being used by other models.

Let's say you have a `Store 1` that has two items, `Item 1` and `Item 2`. If you try to delete Store 1 without first deleting Item 1 and Item 2, SQLAlchemy will raise an error because the items are still related to the store.

This means the items have a **Foreign Key** that references the store you're trying to delete. If the store actually was deleted, the items have a store ID that references something that doesn't exist.

To fix this, you can use a feature called "cascading deletes". Cascading deletes allow you to specify that when a model is deleted, any related models should also be deleted automatically.

SQLAlchemy makes it easy to add cascades to our models, here's how you might do that!

```python title="models/store.py"
from db import db


class StoreModel(db.Model):
__tablename__ = "stores"

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)

# highlight-start
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic", cascade="all, delete")
# highlight-end
```

Remember that `StoreModel` and `ItemModel` have a one-to-many relationship, where each store can have multiple items, and each item belongs to a single store.

The `cascade="all,delete"` argument in the `relationship()` call for the `StoreModel.items` attribute specifies that when a store is deleted, all of its related items should also be deleted.

If you add a `cascade` on the relationship in the `ItemModel`, then when an item is deleted, its related store should also be deleted. This is not what we want, so we won't add a cascade to `ItemModel`.

With this code in place, if you try to delete a store that still has items, the items will be deleted automatically along with the store. This will allow you to delete the store without having to delete the items individually.

For more information, I strongly recommend reading [the official documentation](https://docs.sqlalchemy.org/en/20/orm/cascades.html#delete)! There are also other cascade options you can pass in depending on what you want to happen to related models when the parent changes or is deleted.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLASK_APP=app
FLASK_DEBUG=True
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from flask import Flask
from flask_smorest import Api

from db import db

import models

from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint


def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)

with app.app_context():
db.create_all()

api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)

return app
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from models.item import ItemModel
from models.store import StoreModel
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from db import db


class ItemModel(db.Model):
__tablename__ = "items"

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)

store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from db import db


class StoreModel(db.Model):
__tablename__ = "stores"

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)

items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
flask
flask-smorest
flask-sqlalchemy
python-dotenv
marshmallow
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError

from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema

blp = Blueprint("Items", "items", description="Operations on items")


@blp.route("/item/<string:item_id>")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item

def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}

@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)

if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)

db.session.add(item)
db.session.commit()

return item


@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()

@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)

try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")

return item
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError

from db import db
from models import StoreModel
from schemas import StoreSchema


blp = Blueprint("Stores", "stores", description="Operations on stores")


@blp.route("/store/<string:store_id>")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store

def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200


@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()

@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")

return store
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from marshmallow import Schema, fields


class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)


class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()


class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()


class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLASK_APP=app
FLASK_DEBUG=True
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from flask import Flask
from flask_smorest import Api

from db import db

import models

from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint


def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)

with app.app_context():
db.create_all()

api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)

return app
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from db import db


class ItemModel(db.Model):
__tablename__ = "items"

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)

store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from db import db


class StoreModel(db.Model):
__tablename__ = "stores"

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)

items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
flask
flask-smorest
python-dotenv
marshmallow
Loading

1 comment on commit cb95115

@vercel
Copy link

@vercel vercel bot commented on cb95115 Dec 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.