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)