SQLAlchemyの初期化でハマったこと
sqlalchemyでschemaを宣言して、initで代入した時の挙動でちょっと躓いたのでめもです。
たとえば、todo管理ツールを作るとして、UesrとTaskを作ります。 Taskの順番をpositionとして保持しようとしたときに以下のようなコードにしたとします。
# coding=utf-8 from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import( Column, Integer, String, create_engine, ForeignKey, ) from sqlalchemy.orm import ( scoped_session, sessionmaker, relationship, ) Base = declarative_base() class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String) def __init__(self, name): self.name = name class Task(Base): __tablename__ = 'tasks' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String) user_id = Column(Integer, ForeignKey('users.id'), nullable=False) user = relationship(User, cascade='delete', backref='tasks') position = Column(Integer, nullable=False) # ここが重要 def __init__(self, user, name): self.user = user self.name = name self.position = len(user.tasks) # ここが悪さしてる def main(): # engine,sessionの宣言とdbの初期化 engine = create_engine('sqlite://', echo=True) Session = scoped_session(sessionmaker(bind=engine)) Base.metadata.create_all(engine) # userを作って user = User('E.HOBA') Session.add(user) Session.commit() # taskを作る task = Task(user, name='Hyper Operating System') Session.add(task) Session.commit() # テスト print('name: %s'%task.name) print('user: %s'%task.user.name) print('posotion: %d'%task.position) if __name__ == '__main__': main()
一見うごきそうですが、こんなエラーになります。
sqlalchemy.exc.IntegrityError: (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occuring prematurely) (IntegrityError) NOT NULL constraint failed: tasks.position u'INSERT INTO tasks (name, user_id, position) VALUES (?, ?, ?)' ('Hyper Operating System', 1, None)
なんかpositionがnullable=False
なのがいけないようです。
解決策
nullable=Falseを外してもうごきますが、Taskのinitをこんなふうに先に計算しておくようにすると治せます。
class Task(Base): def __init__(self, user, name): position = len(user.tasks) ## ←ここ! self.user = user self.name = name self.position = position
name: Hyper Operating System user: E.HOBA posotion: 0
たぶん、sqlalchemyの錬金術により、関数呼び出しなどがない部分は先に作って→必要な部分は呼び出しとしている様子で、それによりnullableに引っかかってしまうようです。
最初のコードのnullable=True
にしてみると最初のpositionが1となり、最初に作ってからlen(user.tasks)
を呼び出しているのがわかります。
name: Hyper Operating System user: E.HOBA posotion: 1
ちなみに
nullable=False
のままで下みたいなことをすると問題なくいけます。
case01
class Task(Base): def __init__(self, user, name): print(user.tasks) ## ←ここと self.user = user self.name = name self.position = len(user.tasks) ## ←OK!!!
case02
__init__
内の最初の方に関数呼び出しや代入があれば大丈夫なのかなと思ってこうしてみるとエラーになります。
class Task(Base): def __init__(self, user, name): f = lambda:0 # 意味のない代入 f() # 意味のない関数呼び出し self.user = user self.name = name self.position = len(user.tasks) ## →エラー
case03
やっぱりDBへの参照が行われるかどうかがキモなのかなとおもってこうすると
class Task(Base): def __init__(self, user, name): print(user.name) # 意味のない参照 self.user = user self.name = name self.position = len(user.tasks) ## →エラー
うーむ…たぶんsqlalchemy内でキャッシュをしてるのかな…
sqlalchemy錬金術だ…:;(∩´_`∩);: