Issues with add and delete rows with one to many relationship

Viewed 401

Both of the tables include unique constraints. I have been strugling with this for some time now. What should the command be for adding and deleting?

Adding by doing

movie = Movie(title="transformers", director="mb")
movie2 = Movie(title="transformers2", director="mb")
genre = Genre(category="action")
db.session.add(movie)
db.session.add(movie2)
movie.genres.append(genre)
movie2.genres.append(genre)
db.session.commit()

Gives

(sqlite3.IntegrityError) UNIQUE constraint failed: genres.category
[SQL: INSERT INTO genres (category) VALUES (?)]
[parameters: ('action',)]
(Background on this error at: http://sqlalche.me/e/14/gkpj)

Deleting by doing

db.session.delete(Movie.query.filter_by(title="t1").first())
db.session.commit()

Does not delete

Whats expected:

  • Adding a new row if the title is not existing in Movie table, add a new row if the genre is not existing in Genre table and create a link in the Association table.

  • Deleting the title from Movie table and removing the link from Association table. Do nothing for genre.

Do I need to change my model?

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

class Config:
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:?charset=utf8'
    SQLALCHEMY_DATABASE_ECHO = False
    SQLALCHEMY_TRACK_MODIFICATIONS = 'False'
    FLASK_ENV = 'test'
    DEBUG = True


db = SQLAlchemy()
conf = Config()


def create_app():
    app = Flask(__name__)
    app.config.from_object(conf)
    db.init_app(app)

    return app


class Rating(db.Model):
    __tablename__ = 'ratings'
    id = db.Column(db.Integer, db.ForeignKey("movies.id"), primary_key=True,)
    rating = db.Column(db.String(4), index=True, nullable=False,)
    movie = db.relationship("Movie", back_populates="rating",)

    def __repr__(self):
        return '{}'.format(self.rating)


Association = db.Table('association',
                       db.Column('movies_id', db.Integer,
                                 db.ForeignKey('movies.id'), index=True,),
                       db.Column(
                           'genres_id',
                           db.Integer, db.ForeignKey('genres.id'), index=True,),
                       )


class Movie(db.Model):
    __tablename__ = 'movies'
    id = db.Column(db.Integer, primary_key=True, index=True,)
    title = db.Column(db.String(80), index=True, unique=True, nullable=False,)
    director = db.Column(db.String(30), primary_key=False,
                         unique=False, nullable=False)
    rating = db.relationship(
        "Rating", uselist=False, back_populates="movie"
    )
    genres = db.relationship(
        "Genre", secondary='association', backref=db.backref('movies'),
    )

    def __repr__(self):
        return '{}'.format(self.title)


class Genre(db.Model):
    __tablename__ = 'genres'
    id = db.Column(
        db.Integer,
        primary_key=True,
        index=True,
    )
    category = db.Column(
        db.String(80),
        index=True,
        unique=True,
        nullable=False,
    )

    def __repr__(self):
        return '{}'.format(self.category)


app = create_app()
context = app.app_context()
context.push()
db.create_all()

movie = Movie(title="transformers", director="mb")
rating = Rating(rating="5", movie=movie)
genre = Genre(category="action")
db.session.add_all([movie, rating])
movie.genres.append(genre)

try:
    db.session.commit()
    print("commit successfull")
except Exception as e:
    print(f"{e}")
    db.session.rollback()

movie2 = Movie(title="transformers2", director="mb")
rating = Rating(rating="5", movie=movie2)
genre2 = Genre.query.filter_by(category="action").first()
db.session.add_all([movie2, rating])
movie2.genres.append(genre2)

try:
    db.session.commit()
    print("commit successfull")
except Exception as e:
    print(f"{e}")
    db.session.rollback()

to_delete = Movie.query.filter_by(title="transformers").first()

try:
    db.session.delete(to_delete)
    db.session.commit()
    print("deletion successfull")
except Exception as e:
    print(f"{e}")
    db.session.rollback()

context.pop()

This results in a

commit successfull
commit successfull
Dependency rule tried to blank-out primary key column 'ratings.id' on instance '<Rating at 0x2022c81bdc8>'
2 Answers

The IntegrityError occurs because there is already an action genre in the genre table. In this situation the existing genre must be fetched from the database

movie = Movie(title="transformers", director="mb")
movie2 = Movie(title="transformers2", director="mb")
genre = Genre.query.filter_by(category="action").first()
db.session.add(movie)
db.session.add(movie2)
movie.genres.append(genre)
movie2.genres.append(genre)
db.session.commit()

I can't reproduce the deletion problem in the original version of the question. However the error

Dependency rule tried to blank-out primary key column 'ratings.id' on instance '<Rating at 0x2022c81bdc8>'

Can be removed by configuring a delete cascade on the ratings relationship in the movies model.

    rating = db.relationship(
        "Rating", uselist=False, back_populates="movie",
        cascade='save-update, merge, delete',
    )

This ensures that when a movie is deleted, the associated rating is also deleted. Without it, as you have found, SQLAlchemy doesn't try to delete the associated rating, leading to the error.

If you model indeed is as simple as you write, I would use the Association Proxy to simplify such many-to-many relationship.

Your code will change in the following way:

  • add a _genre_find_or_create function:
def _genre_find_or_create(category):
    obj = Genre.query.filter_by(category=category).first()
    return obj or Genre(category=category)
  • change the Movie definition to wrap the genres

class Movie(db.Model):
    # ...

    _genres = db.relationship(
        "Genre",
        secondary="association",
        backref=db.backref("movies"),
    )
    genres = association_proxy(
        "_genres",
        "category",
        creator=_genre_find_or_create,
    )

Then you can use it as follows:

# pre-adding data to ensure the category already exists
genre = Genre(category="action")
db.session.add(genre)
db.session.commit()

# adding new data
movie = Movie(title="transformers")  # , director="mb")
movie2 = Movie(title="transformers2")  # , director="mb")
db.session.add(movie)
db.session.add(movie2)
# OLD way:
# genre = Genre(category="action")
# movie.genres.append(genre)
# movie2.genres.append(genre)
# NEW way: use just 'category'
movie.genres.append("action")
movie2.genres.append("action")

db.session.commit()


See also another question with this solution.


I still do not understand why you have issues with DELETE though. Your sample code runs correctly, producing the SQL statements below when I run it:

DELETE FROM association WHERE association.movies_id = ? AND association.genres_id = ?
DELETE FROM movies WHERE movies.id = ?

Update: deletion Given updated model, it is clear that the deletion is related to the one-to-one relationship between Movie and Rating.

The simple solution would be to add cascade="delete" to the Movie.rating relationship, which will delete the Rating related instance in situations where the Movie is deleted:

rating = db.relationship(
    "Rating",
    uselist=False,
    back_populates="movie",
    cascade="delete",  # <- **NEW**
)

Read more on Cascades

Related