SQLAlcyemy で repr を自動で

メモ。


SQLAlchemy でモデルクラスを定義していて、__repr__ を毎回定義するのがダルい。
かといって定義しないと、デバッグ時(主にプリントデバッグ)に中身が出ないのも困る。

# -*- coding: utf-8 -*-
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from db import Base

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    email = Column(String(120), unique=True)

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email 

    def __repr__(self): 
        # これを毎回書きたくない
        return 'User(id={0}, name={1}, email={2})'.format(
            self.id, self.name, self.email
        )

方法はないかと思ってググったら、以下がヒットした。
http://foobar.lu/wp/2013/07/05/automagic-__repr__-for-sqlalchemy-entities-with-primary-key-columns-with-declarative-base/
試してみたけど、プライマリーキーが取れるだけだった。
次にヒットしたのが、sqlalchemy - Python __repr__ and None - Stack Overflow


真似してみた。

# -*- coding: utf-8 -*-
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base


class RepresentableBase(object):
    def __repr__(self):
        """Dump all columns and value automagically.
    
        This code is copied a lot from followings.
        See also:
           - https://gist.github.com/exhuma/5935162#file-representable_base-py
           - http://stackoverflow.com/a/15929677
        """             
        #: Columns.
        columns = ', '.join([
            '{0}={1}'.format(k, repr(self.__dict__[k]))
            for k in self.__dict__.keys() if k[0] != '_'
        ])
        
        return '<{0}({1})>'.format(
            self.__class__.__name__, columns
        )

engine = create_engine('sqlite:///test.db', convert_unicode=True)
db_session = scoped_session(
    sessionmaker(autocommit=False, autoflush=False, bind=engine)
)

Base = declarative_base(cls=RepresentableBase)
Base.query = db_session.query_property()


def init_db():
    Base.metadata.create_all(bind=engine)

モデル。

# -*- coding: utf-8 -*-
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from db import Base

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    email = Column(String(120), unique=True)

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email


class UserImage(Base):
    __tablename__ = 'images'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    image = Column(String(120), unique=True)
        
    user = relationship('User', backref='images',
                        primaryjoin='User.id==UserImage.user_id',
                        lazy='joined')

    def __init__(self, user_id=None, image=None):
        self.user_id = user_id
        self.image = image

使う側。

from sqlalchemy.orm import joinedload
from db import db_session, init_db
from models import User, UserImage
        

if __name__ == '__main__':
    init_db()

    user = User(name='foo', email='foo@example.com')
    db_session.add(user)
    db_session.flush()
    print(user)
    
    image = UserImage(user_id=user.id, image='/path/to/image')
    image2 = UserImage(user_id=user.id, image='/path/to/image2')
    db_session.add_all([image, image2])
    db_session.flush()
    db_session.commit()
    
    data = db_session.query(User).options(joinedload('images')).all()
    print(data)

結果。

$ python app.py
<User(email='foo@example.com', id=1, name='foo')>
[<User(images=[<UserImage(id=1, user_id=1, image='/path/to/image')>, <UserImage(id=2, user_id=1, image='/path/to/image2')>], email='foo@example.com', id=1, name='foo')>]

ちゃんと取れた。
関連付けをしていても関連先のもちゃんと表示してくれる。