bpmappers 便利!

今日は bpmappers というライブラリが、すごく便利で感動したという事を書く

良くJSONを返すようなAPIを作るときに、オブジェクトの必要な部分だけを、辞書に変換してからJSONにするなんて事をすると思う。オブジェクトのプロパティが、数値やテキストなどリテラルなものであれば良いが、別のオブジェクトだったりすると、そもそもオブジェクトから辞書への変換は面倒くさそうだ。

これをよしなにやってくれるのがbpmappersである。詳しくはドキュメントを見てもらうとして、簡単な例を紹介しようと思う。

前提

まず SQLAlchemyで下記のようなモデルがあるとする。Entry と Comment は 1:N の関係にある。

# declare models 
class Entry(Base): 
    __tablename__ = 'entries'  
    id = Column(Integer, primary_key=True)   
    text = Column(String)  
    created_at = Column(DateTime, default=datetime.now, nullable=False) 

    comments = relationship('Comment') 

class Comment(Base):
    __tablename__ = 'comments'
    id = Column(Integer, primary_key=True)
    entry_id = Column(Integer, ForeignKey('entries.id'), nullable=False)
    text = Column(String)
    created_at = Column(DateTime, default=datetime.now, nullable=False)

このように定義すると「Entry.comments」は下記のようにCommentオブジェクトのリストとして取得できる。

# select data 
entries = Entry.query.all() 
for entry in entries:
   for comment in entry.comments: 
        print ' comment.id: ' + str(comment.id)
        print ' comment.text: ' + comment.text
        print ' comment.created_at: ' + str(comment.created_at)

この入れ子になったオブジェクト構成をbpmappersを使って簡単にJSONにしてみる。

Mapperの定義

# declare mappers
class CommentMapper(Mapper):
    id = RawField()
    entry_id = RawField()
    text = RawField()
    created_at = RawField(callback=lambda x:x.strftime('%Y-%m-%d %H:%M:%S'))
                                                                            
class EntryMapper(Mapper):
    id = RawField() 
    text = RawField() 
    created_at = RawField(callback=lambda x:x.strftime('%Y-%m-%d %H:%M:%S'))
 
    comments = ListDelegateField(CommentMapper) 

#convert to json  
print json.dumps(EntryMapper(Entry.query.all()).as_dict(), indent=2)

これで終わり。 Mapperを定義するだけ。 あとはas_dict()でよしなに辞書にしてくれる。実行結果はこんな感じ

{
  "created_at": "2012-02-08 00:06:12", 
  "text": "entry text", 
  "id": 1, 
  "comments": [
    {
      "created_at": "2012-02-08 00:06:12", 
      "text": "comment1 text", 
      "entry_id": 1, 
      "id": 1
    }, 
    {
      "created_at": "2012-02-08 00:06:12", 
      "text": "comment2 text", 
      "entry_id": 1, 
      "id": 2
    }
  ]
}

Mapperクラスを継承して、必要なフィールドを定義するだけで目的が達成できてしまった。RawFieldはオブジェクトが持つ同名のプロパティをそのままセットしてくれる。callbackも呼べるので適宜データを加工した上でセットすることも可能。

ポイント は ListDelegateField 。 リストの各要素に対するマッピング処理を別のMapperクラスに委譲できる。この例でいうと、commentsの各要素であるCommentオブジェクトのマッピング処理は、CommentMapperに任せてる。

まとめ

駆け足で説明したけど、まとめるとこんな感じ

  • Mapperを定義するだけで、オブジェクト自身のデータをゴニョゴニョしなくていいのは大変嬉しい。
  • DelegateFieldがある事で、ネストしたデータ構造でも安心して使える。
  • Modelクラスと1 : 1 になるように掛けるので、ごちゃごちゃしない

以上おわり。

おまけ

コードの断片だと分かりにくいと思うので、サンプルコードをまるっと貼付けておく。

#!/usr/bin/env python
#-*- coding:utf8 -*-

import json 
from datetime import datetime 

from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base

from bpmappers import Mapper, RawField, ListDelegateField 

# create base 
engine = create_engine('sqlite://', convert_unicode=True, echo=False) 
db_session = scoped_session(sessionmaker(autocommit=False,  
                            autoflush=False,
                            bind=engine)) 
Base = declarative_base() 
Base.query = db_session.query_property()

# declare models
class Entry(Base):
    __tablename__ = 'entries' 
    id = Column(Integer, primary_key=True) 
    text = Column(String) 
    created_at = Column(DateTime, default=datetime.now, nullable=False)

    comments = relationship('Comment')

class Comment(Base): 
    __tablename__ = 'comments' 
    id = Column(Integer, primary_key=True)  
    entry_id = Column(Integer, ForeignKey('entries.id'), nullable=False)  
    text = Column(String) 
    created_at = Column(DateTime, default=datetime.now, nullable=False) 

# declare mappers
class CommentMapper(Mapper): 
    id = RawField() 
    entry_id = RawField()
    text = RawField() 
    created_at = RawField(callback=lambda x:x.strftime('%Y-%m-%d %H:%M:%S'))

class EntryMapper(Mapper): 
    id = RawField() 
    text = RawField() 
    created_at = RawField(callback=lambda x:x.strftime('%Y-%m-%d %H:%M:%S'))
    comments = ListDelegateField(CommentMapper)

# insert data 
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)

db_session.add(Entry(text='entry text'))
db_session.commit()
db_session.add(Comment(entry_id=1, text='comment1 text')) 
db_session.add(Comment(entry_id=1, text='comment2 text')) 
db_session.commit()

# select data
print '-' * 10 
entries = Entry.query.all() 
for entry in entries: 
    print 'entry.id: ' + str(entry.id)  
    print 'entry.text: ' + entry.text 
    print 'entry.creted_at: ' + str(entry.created_at) 
    for comment in entry.comments: 
        print ' ' + ('-' * 9)
        print ' comment.id: ' + str(comment.id)  
        print ' comment.text: ' + comment.text
        print ' comment.created_at: ' + str(comment.created_at)

# convert to json
print '-' * 10
print json.dumps(EntryMapper(entries).as_dict(), indent=2)