cocuh's note

type(あうとぷっと) -> 駄文

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錬金術だ…:;(∩´_`∩);: