SQLAlchemy从多对多关系中删除

时间:2016-01-21 22:12:10

标签: python sqlite flask sqlalchemy 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.

这是Post&任务模型& 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.

3 个答案:

答案 0 :(得分:0)

好的,所以我认为这里有一些可能导致你的问题。第一件事是错误信息本身。这暗示数据库认为它应该删除某些内容,但它不存在。我相信这是由您delete-all orphansingle_parent=True引起的。

这告诉sqlalchemy PostTask都有一个令人困惑的single_parent!所以我认为你需要做的就是让它发挥作用

  1. 仅在一个模型上定义关系。现在设置两种类定义关系的方式就是让你的代码吵架。我会建议这样的事情:
  2.             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. 了解您希望数据模型如何工作。任何任务都可以在任何帖子中,任何帖子都可以有任意数量的任务吗?我认为您应该通过Post own Task重新考虑数据模型。您仍然可以在不同的两者中共享任务,但是您需要清楚地了解未来的数据模型。

    2. 明确说明要删除的内容。我知道,删除任务时应该删除任务所在的每个帖子都应该删除,但对我来说没有意义。循环显示要删除的正确帖子和任务。这样您就可以更好地理解删除和更清晰的代码。

    3. <强>更新

      来自documentation

        

      这里有几种可能性:

           
          
      • 如果从Parent到Child存在关系(),但没有将特定Child链接到每个Parent的反向关系,   SQLAlchemy在删除时不会有任何意识   特别是Child对象,它需要维护“二级”表   将其链接到父级。不删除“辅助”表   发生。

      •   
      • 如果存在将特定Child链接到每个Parent的关系,假设它被称为Child.parents,默认情况下SQLAlchemy将   加载Child.parents集合以查找所有Parent对象,以及   从“辅助”表中删除每一行来建立这一行   链接。请注意,此关系不需要是bidrectional;   SQLAlchemy严格关注与之关联的每个关系()   要删除的Child对象。

      •   
      • 此处性能更高的选项是使用ON DELETE CASCADE指令和数据库使用的外键。假设   数据库支持此功能,数据库本身可以进行   自动删除“辅助”表中的行作为引用行   在“孩子”中被删除。可以指示SQLAlchemy放弃   在这种情况下使用主动加载Child.parents集合   关于relationship()的passive_deletes指令;请参阅使用被动   删除有关此内容的更多详细信息。再次注意,这些行为只是   与relationship()使用的辅助选项相关。如果处理   使用明确映射但不存在的关联表   在相关关系的次要选项中(),级联规则   可以用来代替自动删除实体以响应a   相关实体被删除 - 有关此信息,请参阅Cascades   特征

      •   

答案 1 :(得分:0)

通过设置删除级联功能,您的意思是删除tasks_posts中的记录。 这不是必需的,sql炼金术会自动完成。

一般情况下,您尝试过度配置您的关系,我建议从这样的简单设置开始:

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',这会在tasks类中自动创建Post关系。

旁注:relationship块和tasks_posts中的行末尾不需要斜杠,因为这些块自然地包含在括号中

答案 2 :(得分:0)

感谢每个人的帮助,我似乎已经弄明白了。我试图实现的想法是单个帖子中可以包含零到多个任务(用户可以一次完成多个任务)。用户可以查看单个任务中的所有帖子。如果用户决定删除任务,则该任务中的帖子保持不变。

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