How to create and associate another model during creation in flask-sqlalchemy?

时间:2015-09-14 16:09:01

标签: python flask flask-sqlalchemy

I have a one-to-many relationship and I'd like to create and associate the related model to the model being created at creation time.

Here I have User and Item models. When a user is first created, the user should get a default item automatically:

class User(Model):
    id = Column(Integer, primary_key=True)
    name = Column(String, default="", unique=True)
    items = relationship('Item', backref='user', lazy='dynamic')

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        self.items.append(...)    # <---- Add default item here?

class Item(Model):
    id = Column(Integer, primary_key=True)
    name = Column(String, default="")
    userid = Column(Integer,  ForeignKey('user.id'))

But I'm not sure how to append the new Item in User.__init__()? Is it something like:

# in User __init__()
item = Item(name="default item")
self.items.append(item)
session.add(item)

But then I"m unclear what happens to the transaction if something fails on either model, and whether I need to call session.add(item) or if appending the item to user automatically does this.

ie, what happens to the transaction when I create a user and this throws an error because name is not unique. Will the item still be created?

def doit():
    myuser = User(name='bob')   # <---- Say "bob" is already in DB
    session.add(myuser)
    session.commit()

What is the valid way of creating a default Item for User at user creation time?

1 个答案:

答案 0 :(得分:1)

解决这些类型问题的最佳方法是创建一些测试代码。我发现sqlite内存数据库中的原型设计工作得非常好。

因此,如果关系配置正确 - 就像您的情况一样 - sqlalchemy将找出关键关系并自动设置外键。

但是看到相信,我添加了一些示例代码,这些代码具有您所描述的场景的单元测试。 在每次测试之前,我都会删除并重新创建所有表,以确保模式是干净的。

import logging
logging.basicConfig()

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()


from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship


class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String, default="", unique=True)
    items = relationship('Item', backref='user', lazy='dynamic')

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        self.items.append(Item(name='DefaultItem'))


class Item(Base):
    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    name = Column(String, default="", unique=True)  # Unique so we can create a failing test
    user_id = Column(Integer,  ForeignKey('user.id'))


from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker
from contextlib import contextmanager


engine = create_engine('sqlite://', echo=True)
Session = sessionmaker(bind=engine)


@contextmanager
def session_scope():
    """Provide a transactional scope around a series of operations."""
    session = Session()
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()


import unittest


class Test(unittest.TestCase):
    def setUp(self):
        Base.metadata.create_all(engine)  # Reset

    def tearDown(self):
        Base.metadata.drop_all(engine)  # Reset

    def test_sanity(self):
        with session_scope() as session:
            self.assertEqual(session.query(User).all(), [])
            self.assertEqual(session.query(Item).all(), [])

    def test_item_automatically_added(self):
        with session_scope() as session:
            user = User(name='John')
            session.add(user)

        with session_scope() as session:
            user = session.query(User).filter(User.name == 'John').one()
            self.assertIsNotNone(user.items.filter(Item.name == 'DefaultItem').one())

    def test_duplicate_item_causes_user_save_to_fail(self):
        with session_scope() as session:
            item = Item(name='DefaultItem')
            session.add(item)

        from sqlalchemy.exc import IntegrityError

        with self.assertRaises(IntegrityError):
            with session_scope() as session:
                user = User(name='John')
                session.add(user)


if __name__ == '__main__':
    unittest.main()

所以这些测试涵盖了除一个以外的所有场景。如果用户创建失败,则测试将失败。我无法想出一个简单的方法来做这个测试,但我确实检查了sql echo输出

INFO:sqlalchemy.engine.base.Engine:INSERT INTO user (name) VALUES (?)
2015-09-15 20:55:49,834 INFO sqlalchemy.engine.base.Engine INSERT INTO user (name) VALUES (?)
INFO:sqlalchemy.engine.base.Engine:('John',)
INFO:sqlalchemy.engine.base.Engine:INSERT INTO item (name, user_id) VALUES (?, ?)
2015-09-15 20:55:49,834 INFO sqlalchemy.engine.base.Engine ('John',)
INFO:sqlalchemy.engine.base.Engine:('DefaultItem', 1)
2015-09-15 20:55:49,834 INFO sqlalchemy.engine.base.Engine INSERT INTO item (name, user_id) VALUES (?, ?)

用户总是在项目之前插入,所以无论如何我们都无法获得悬空物品。为了更加确定这一点,你可以使item.user_id外键不可为空。