簡體   English   中英

從多對多關系中刪除 SQLAlchemy

[英]SQLAlchemy DELETE from many-to-many relationship

(我正在使用 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.

這是 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'))\
        )

這是視圖函數:

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

我假設問題是我錯誤地實施:

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

好的,所以我認為這里發生的一些事情可能會導致您的問題。 首先是錯誤消息本身。 這意味着數據庫認為它應該刪除某些內容,但它並不存在。 我相信這是由您的delete-all orphansingle_parent=True

這告訴 sqlalchemy PostTask都有一個令人困惑的 single_parent ! 所以我相信你需要做的是讓這個工作

  1. 僅在一個模型上定義關系。 您現在使用兩個定義關系的類來設置它的方式正在使您的代碼變得混亂。 我會建議這樣的事情:
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'))
  1. 弄清楚您希望數據模型如何工作。 任何任務都可以在任何帖子中,任何帖子可以有任意數量的任務? 我認為您可能應該通過發布自己的任務來重新考慮數據模型。 您仍然可以在不同的兩者中共享任務,但您需要清楚地了解未來的數據模型。

  2. 明確說明您要刪除的內容。 我知道在刪除任務時應該刪除任務所在的每個帖子對您來說可能是有意義的,但對我來說這沒有意義。 循環遍歷要刪除的正確帖子和任務。 通過這種方式,您將對刪除和更干凈的代碼有更好的理解。

更新:

文檔

這里有幾種可能:

  • 如果從Parent到Child有一個relationship(),但不存在將一個特定的Child鏈接到每個Parent的反向關系,SQLAlchemy將不會意識到在刪除這個特定的Child對象時,它需要維護“secondary ” 將其鏈接到父級的表。 不會刪除“輔助”表。

  • 如果存在將特定 Child 鏈接到每個 Parent 的關系,假設它稱為 Child.parents,默認情況下 SQLAlchemy 將加載 Child.parents 集合以定位所有 Parent 對象,並從建立的“輔助”表中刪除每一行這個鏈接。 請注意,這種關系不需要是雙向的; SQLAlchemy 嚴格查看與被刪除的子對象關聯的每個關系()。

  • 此處性能更高的選項是對數據庫使用的外鍵使用 ON DELETE CASCADE 指令。 假設數據庫支持此功能,則可以使數據庫本身在刪除“子”表中的引用行時自動刪除“輔助”表中的行。 在這種情況下,可以使用關於關系()的passive_deletes 指令指示SQLAlchemy 放棄主動加載Child.parents 集合; 有關更多詳細信息,請參閱使用被動刪除。 再次注意,這些行為僅與與 relationship() 一起使用的次要選項相關。 如果處理顯式映射且不存在於相關關系()的次要選項中的關聯表,則可以使用級聯規則自動刪除實體以響應相關實體被刪除 - 有關此功能的信息,請參閱級聯.

多虧了大家的幫助,我似乎已經想通了。 我試圖實現的想法是一個帖子可以包含零到多個任務(用戶可以一次完成多個任務)。 用戶可以在單個任務中查看所有帖子。 如果用戶決定刪除任務,則該任務中的帖子保持不變。

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

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(24))
    description = db.Column(String(64))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

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

看起來通過設置刪除級聯功能,您的意思是從tasks_posts刪除記錄。 這不是必需的,sql alchemy 會自動完成。

一般來說,您嘗試過度配置您的關系,我建議從這樣的簡單設置開始:

class Post(ModelBase):
    __tablename__ = 'posts'
    id = Column(Integer, primary_key=True)
    body = Column(Text)
    user_id = Column(Integer, ForeignKey('users.id'))


class Task(ModelBase):
    __tablename__ = 'tasks'
    id = Column(Integer, primary_key=True)
    title = Column(String(24))
    description = Column(String(64))
    user_id = Column(Integer, ForeignKey('users.id'))
    posts = relationship(
        'Post', 
         secondary='tasks_posts', 
         backref='tasks')

正如在評論中已經提到的, backref僅在其中一張表中需要。 上面我為帖子指定backref='tasks' ,這會自動在Post類中創建tasks關系。

旁注:您不需要在relationship塊和tasks_posts的行尾使用斜杠,因為這些塊自然地包含在括號中

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM