从多对多关系中删除 SQLAlchemy

KA0*_*A01 7 python sqlite sqlalchemy flask flask-sqlalchemy

(我正在使用 SQLAlchemy、SQLite3、Flask-SQLAlchemy、Flask 和 Python)

我正在实施一个待办事项列表提要,用户可以在其中创建帖子 ( class Post) 并将任务 ( class Task)附加到每个帖子。每个任务可以有多个帖子。每个帖子可以有多个任务。我在使用 SQLAlchemy 并从表中删除时遇到问题。有趣的是:

  • 当用户删除其中有零个帖子的任务( task.posts.count() == 0) 时,从数据库中删除成功
  • 当用户删除包含一个或多个帖子的任务( task.posts.count() > 0) 时,从数据库中删除会引发错误。

这是错误:

sqlalchemy.exc.InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. 
To begin a new transaction with this Session, first issue Session.rollback().
Original exception was: DELETE statement on table 'tasks_posts' expected to delete 1 row(s); Only 0 were matched.
Run Code Online (Sandbox Code Playgroud)

这是 Post & Task Models & tasks_posts 表:

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    tasks = db.relationship('Task', secondary='tasks_posts', \
            backref=db.backref('post', lazy='joined'), \
            lazy='dynamic', cascade='all, delete-orphan', \
            single_parent=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True) 
    title = db.Column(db.String(24))
    description = db.Column(db.String(64))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    posts = db.relationship('Post', secondary='tasks_posts', \
            backref=db.backref('task', lazy='joined'), \
            lazy='dynamic', cascade='all, delete-orphan', \
            single_parent=True)

tasks_posts = db.Table('tasks_posts',\
        db.Column('task_id', db.Integer, db.ForeignKey('tasks.id')),\
        db.Column('post_id', db.Integer, db.ForeignKey('posts.id'))\
        )
Run Code Online (Sandbox Code Playgroud)

这是视图函数:

@main.route('/edit-task/delete/<int:id>', methods=['GET', 'POST'])
def delete_task(id):
    task = Task.query.get_or_404(id)
    db.session.delete(task)
    db.session.commit()
    return redirect(url_for('.user', username=current_user.username))
Run Code Online (Sandbox Code Playgroud)

我假设问题是我错误地实施:

  • SQLAlchemy 的“级联”功能
  • 多对多关系
  • 或视图函数

这是堆栈跟踪:

File "...venv/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__
    return self.wsgi_app(environ, start_response)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1473, in full_dispatch_request
    rv = self.preprocess_request()
  File ".../venv/lib/python2.7/site-packages/flask/app.py", line 1666, in preprocess_request
    rv = func()
  File ".../app/auth/views.py", line 12, in before_request
    if current_user.is_authenticated:
  File ".../venv/lib/python2.7/site-packages/werkzeug/local.py", line 342, in __getattr__
    return getattr(self._get_current_object(), name)
  File ".../venv/lib/python2.7/site-packages/werkzeug/local.py", line 301, in _get_current_object
    return self.__local()
  File ".../venv/lib/python2.7/site-packages/flask_login.py", line 47, in <lambda>
    current_user = LocalProxy(lambda: _get_user())
  File ".../venv/lib/python2.7/site-packages/flask_login.py", line 858, in _get_user
    current_app.login_manager._load_user()
  File ".../venv/lib/python2.7/site-packages/flask_login.py", line 389, in _load_user
    return self.reload_user()
  File ".../venv/lib/python2.7/site-packages/flask_login.py", line 351, in reload_user
    user = self.user_callback(user_id)
  File ".../app/models.py", line 235, in load_user
    return User.query.get(int(user_id))
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 829, in get
    return self._get_impl(ident, loading.load_on_ident)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 853, in _get_impl
    self.session, key, attributes.PASSIVE_OFF)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/loading.py", line 152, in get_from_identity
    state._load_expired(state, passive)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/state.py", line 474, in _load_expired
    self.manager.deferred_scalar_loader(self, toload)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/loading.py", line 664, in load_scalar_attributes
    only_load_props=attribute_names)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/loading.py", line 219, in load_on_ident
    return q.one()
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 2528, in one
    ret = list(self)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 2571, in __iter__
    return self._execute_and_instances(context)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 2584, in _execute_and_instances
    close_with_result=True)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/query.py", line 2575, in _connection_from_session
    **kw)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 893, in connection
    execution_options=execution_options)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 898, in _connection_for_bind
    engine, execution_options)
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 313, in _connection_for_bind
    self._assert_active()
  File ".../venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 214, in _assert_active
    % self._rollback_exception
InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: DELETE statement on table 'tasks_posts' expected to delete 1 row(s); Only 0 were matched.
Run Code Online (Sandbox Code Playgroud)

pec*_*rin 1

好的,所以我认为这里发生的一些事情可能会导致您的问题。首先是错误消息本身。这意味着数据库认为它应该删除某些内容,但它不在那里。我相信这是由你delete-all orphansingle_parent=True引起的。

\n\n

这告诉 sqlalchemyPostTask一个 single_parent,这令人困惑!所以我相信你需要做的就是让它发挥作用

\n\n
    \n
  1. 仅在一个模型上定义关系。现在使用两个定义关系的类来设置它的方式正在使您的代码混乱。我会建议这样的事情:
  2. \n
\n\n
class Post(db.Model):\n    __tablename__ = \'posts\'\n    id = db.Column(db.Integer, primary_key=True)\n    body = db.Column(db.Text)\n    tasks = db.relationship(\'Task\', secondary=\'tasks_posts\', \\\n                            backref=db.backref(\'post\', lazy=\'joined\'), \\\n                            lazy=\'dynamic\', cascade=\'all, delete-orphan\', \\\n                            single_parent=True)\n    user_id = db.Column(db.Integer, db.ForeignKey(\'users.id\'))\n\nclass Task(db.Model):\n    __tablename__ = \'tasks\'\n    id = db.Column(db.Integer, primary_key=True)\n    title = db.Column(db.String(24))\n    description = db.Column(db.String(64))\n    user_id = db.Column(db.Integer, db.ForeignKey(\'users.id\'))\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  1. 弄清楚您希望数据模型如何工作。任何任务可以在任何帖子中,并且任何帖子可以有任意数量的任务?我认为您也许应该通过发布自己的任务来重新考虑数据模型。您仍然可以在不同的两者中共享任务,但您需要清楚地了解未来的数据模型。

  2. \n
  3. 明确您要删除的内容。我知道,当删除任务时,任务所在的每个帖子都应该被删除,这对您来说可能是有意义的,但对我来说,这没有意义。循环浏览要删除的正确帖子和任务。这样你会对删除和更干净的代码有更好的理解。

  4. \n
\n\n

更新:

\n\n

文档中中:

\n\n
\n

这里有几种可能性:

\n\n
    \n
  • 如果存在从父级到子级的关系(),但没有将特定子级链接到每个父级的反向关系,\n SQLAlchemy 将不会意识到在删除此\n 特定子级对象时,它需要维护将其链接到父级的 \xe2\x80\x9csecondary\xe2\x80\x9d 表\n。不会删除 \xe2\x80\x9csecondary\xe2\x80\x9d 表。\n

  • \n
  • 如果存在将特定 Child 链接到每个 Parent 的关系,假设它\xe2\x80\x99 称为 Child.parents,则 SQLAlchemy 默认情况下将\n 加载到 Child.parents 集合中以查找所有 Parent 对象,并\n 删除建立此链接的 \xe2\x80\x9csecondary\xe2\x80\x9d 表中的每一行。请注意,此关系不需要是双向关系;\n SQLAlchemy 严格查看与要删除的子对象关联的每个关系()。

  • \n
  • 这里性能更高的选项是将 ON DELETE CASCADE 指令与数据库使用的外键一起使用。假设数据库支持此功能,则可以让数据库本身自动删除 \xe2\x80\x9csecondary\xe2\x80\x9d 表中的行,以引用 \xe2\x80\x9cchild\xe2 中的行\n \x80\x9d 被删除。在这种情况下,可以使用关系()上的passive_deletes指令指示SQLAlchemy放弃在Child.parents集合中主动加载;有关详细信息,请参阅使用被动\n 删除。再次注意,这些行为仅与关系()使用的辅助选项相关。如果处理\n 显式映射且不存在\n 相关关系() 的辅助选项\n 中的关联表,则可以使用级联规则\n 来自动删除实体,以响应删除的相关实体\n - 有关此功能的信息,请参阅级联。\n

  • \n
\n
\n