添加和删除具有一对多关系的行的问题
Issues with add and delete rows with one to many relationship
两个 table 都包含唯一约束。我已经为此苦苦挣扎了一段时间。添加和删除命令应该是什么?
边做边加
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()
给予
(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)
边做边删
db.session.delete(Movie.query.filter_by(title="t1").first())
db.session.commit()
不删除
预期结果:
如果电影 table 中不存在该标题,则添加一个新行;如果流派 table 中不存在该类型,则添加一个新行并创建一个 link 在协会 table.
正在从电影 table 中删除标题并从关联 table 中删除 link。什么都不做。
我需要更换模型吗?
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()
这导致
commit successfull
commit successfull
Dependency rule tried to blank-out primary key column 'ratings.id' on instance '<Rating at 0x2022c81bdc8>'
出现 IntegrityError
是因为类型 table 中已经有一个动作类型。在这种情况下,必须从数据库中获取现有类型
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()
我无法重现原版问题中的删除问题。然而错误
Dependency rule tried to blank-out primary key column 'ratings.id' on instance '<Rating at 0x2022c81bdc8>'
可以通过对电影模型中的评级关系配置删除 cascade 来删除。
rating = db.relationship(
"Rating", uselist=False, back_populates="movie",
cascade='save-update, merge, delete',
)
这样可以确保在删除电影时,相关的评级也会被删除。没有它,正如您所发现的,SQLAlchemy 不会尝试删除相关的评级,从而导致错误。
如果你的模型确实像你写的那么简单,我会用Association Proxy来简化这种多对多的关系。
您的代码将按以下方式更改:
- 添加一个
_genre_find_or_create
函数:
def _genre_find_or_create(category):
obj = Genre.query.filter_by(category=category).first()
return obj or Genre(category=category)
- 将
Movie
定义更改为 wrap 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,
)
那么就可以这样使用了:
# 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()
另请参阅另一个 question 使用此解决方案。
不过,我还是不明白为什么您对 DELETE
有疑问。您的示例代码 运行s 正确,当我 运行 时生成下面的 SQL 语句:
DELETE FROM association WHERE association.movies_id = ? AND association.genres_id = ?
DELETE FROM movies WHERE movies.id = ?
更新:删除
鉴于更新后的模型,很明显删除与 Movie
和 Rating
.
之间的一对一关系有关
简单的解决方案是将 cascade="delete"
添加到 Movie.rating
关系中,这将在删除 Movie
的情况下删除 Rating
相关实例:
rating = db.relationship(
"Rating",
uselist=False,
back_populates="movie",
cascade="delete", # <- **NEW**
)
阅读更多关于Cascades
两个 table 都包含唯一约束。我已经为此苦苦挣扎了一段时间。添加和删除命令应该是什么?
边做边加
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()
给予
(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)
边做边删
db.session.delete(Movie.query.filter_by(title="t1").first())
db.session.commit()
不删除
预期结果:
如果电影 table 中不存在该标题,则添加一个新行;如果流派 table 中不存在该类型,则添加一个新行并创建一个 link 在协会 table.
正在从电影 table 中删除标题并从关联 table 中删除 link。什么都不做。
我需要更换模型吗?
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()
这导致
commit successfull
commit successfull
Dependency rule tried to blank-out primary key column 'ratings.id' on instance '<Rating at 0x2022c81bdc8>'
出现 IntegrityError
是因为类型 table 中已经有一个动作类型。在这种情况下,必须从数据库中获取现有类型
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()
我无法重现原版问题中的删除问题。然而错误
Dependency rule tried to blank-out primary key column 'ratings.id' on instance '<Rating at 0x2022c81bdc8>'
可以通过对电影模型中的评级关系配置删除 cascade 来删除。
rating = db.relationship(
"Rating", uselist=False, back_populates="movie",
cascade='save-update, merge, delete',
)
这样可以确保在删除电影时,相关的评级也会被删除。没有它,正如您所发现的,SQLAlchemy 不会尝试删除相关的评级,从而导致错误。
如果你的模型确实像你写的那么简单,我会用Association Proxy来简化这种多对多的关系。
您的代码将按以下方式更改:
- 添加一个
_genre_find_or_create
函数:
def _genre_find_or_create(category):
obj = Genre.query.filter_by(category=category).first()
return obj or Genre(category=category)
- 将
Movie
定义更改为 wrapgenres
class Movie(db.Model):
# ...
_genres = db.relationship(
"Genre",
secondary="association",
backref=db.backref("movies"),
)
genres = association_proxy(
"_genres",
"category",
creator=_genre_find_or_create,
)
那么就可以这样使用了:
# 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()
另请参阅另一个 question 使用此解决方案。
不过,我还是不明白为什么您对 DELETE
有疑问。您的示例代码 运行s 正确,当我 运行 时生成下面的 SQL 语句:
DELETE FROM association WHERE association.movies_id = ? AND association.genres_id = ?
DELETE FROM movies WHERE movies.id = ?
更新:删除
鉴于更新后的模型,很明显删除与 Movie
和 Rating
.
简单的解决方案是将 cascade="delete"
添加到 Movie.rating
关系中,这将在删除 Movie
的情况下删除 Rating
相关实例:
rating = db.relationship(
"Rating",
uselist=False,
back_populates="movie",
cascade="delete", # <- **NEW**
)
阅读更多关于Cascades