SQLAlchemy で独自の Composite Value を作る


Composite Value とは、 PofEAA にあるパターンで、Entity が持っている属性となるシンプルな Value Object を、その Entity がマップされるテーブルの複数のカラムとしてマップすることです。

似たパターンで Serialized LOB という、オブジェクトを JSON 等にシリアライズして BLOB や CLOB に入れてしまうパターンも有ります。 Composite Value は Serialized LOB に比べると、 SQL から使えるというメリットが有ります。

SQLAlchemy で Composite Value をする例は、本家のドキュメントにあります。
[Composite Column Types](http://docs.sqlalchemy.org/en/rel_0_9/orm/mapper_config.html#mapper-composite)

ですが、この例ではオペレータを定義していないので、等値比較しかできません。
`comparator_factory` を使うと、SQLの生成部分もカスタマイズできるのですが、その部分のサンプルが分かれていたので、一緒にするサンプルを作りました。

こちらのサンプルのほうが、 named tuple を使って Value Object を作ってるので楽をできています。

```python:composite_example.py
# -*- coding: utf-8 -*-
from __future__ import division, print_function, absolute_import

from collections import namedtuple
from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.properties import CompositeProperty
from sqlalchemy.orm import composite, sessionmaker, configure_mapper

# ORM tutorial よりコピペ
Base = declarative_base()
engine = create_engine('sqlite:///:memory:', echo=True)
Session = sessionmaker(engine)


# 今回の Value Object となる Range 型。 start と end　がある。
class Range(namedtuple('Range', 'start end')):

    # このメソッドはタプルでマップされてる値を返すんだけど、
    # namedtuple はタプルなので self を返すだけで十分
    def __composite_values__(self):
        return self

    # こっちはただのメソッド。インスタンスメンバ経由で使う
    def includes(self, value):
        return self.start <= value < self.end


# こちらが SQL を生成する方
class RangeComparator(CompositeProperty.Comparator):
    # SQL を生成するメソッド.  Value Object のメソッドと使い方を合わせよう
    def includes(self, value):
        # マップされてるカラムを取り出す。この部分はコピペ.
        start, end = self.__clause_element__().clauses
        # and_() を使って SQL を生成.
        return and_(start <= value, value < end)


# ヘルパ関数
def range_composite(start, end):
    return composite(Range, start, end, comparator_factory=RangeComparator)


class MyTable(Base):
    __tablename__ = 'mytable'
    id = Column(Integer, primary_key=True)
    foo_start = Column(Integer)
    foo_end = Column(Integer)
    foo_range = range_composite(foo_start, foo_end)

    def __repr__(self):
        return "MyTable(foo_start={0.foo_start!r}, foo_end={0.foo_end!r}".format(self)


# このサンプルでは不要. だけど、複雑なプロジェクトではこれをやっておかないと
# 複数のクラスをまたがるマッピングが完成してなくてSQL生成に失敗するケースがあるので、
# 全モデルを定義した後にやっておこう。
# configure_mappers()


print("Create tables")
Base.metadata.create_all(engine)

session = Session()
print("Insert test data")
session.add(MyTable(foo_start=10, foo_end=100))
session.add(MyTable(foo_start=100, foo_end=200))
session.add(MyTable(foo_start=1, foo_end=10))
session.commit()

print("Select using filter")
# RangeComparator.includes() を使って filter 部分を構築できる
values = session.query(MyTable).filter(MyTable.foo_range.includes(42)).all()
print("values:", values)

# もちろん、 Range.includes() は普通にインスタンスで使える
v = values[0]
print("test")
print(9, v.foo_range.includes(9))
print(10, v.foo_range.includes(10))
print(99, v.foo_range.includes(99))
print(100, v.foo_range.includes(100))
```

出力はこうなります。

```
Create tables
2014-01-15 22:59:15,334 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("mytable")
2014-01-15 22:59:15,334 INFO sqlalchemy.engine.base.Engine ()
2014-01-15 22:59:15,335 INFO sqlalchemy.engine.base.Engine
CREATE TABLE mytable (
	id INTEGER NOT NULL,
	foo_start INTEGER,
	foo_end INTEGER,
	PRIMARY KEY (id)
)


2014-01-15 22:59:15,335 INFO sqlalchemy.engine.base.Engine ()
2014-01-15 22:59:15,335 INFO sqlalchemy.engine.base.Engine COMMIT
Insert test data
2014-01-15 22:59:15,336 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2014-01-15 22:59:15,337 INFO sqlalchemy.engine.base.Engine INSERT INTO mytable (foo_start, foo_end) VALUES (?, ?)
2014-01-15 22:59:15,337 INFO sqlalchemy.engine.base.Engine (10, 100)
2014-01-15 22:59:15,337 INFO sqlalchemy.engine.base.Engine INSERT INTO mytable (foo_start, foo_end) VALUES (?, ?)
2014-01-15 22:59:15,337 INFO sqlalchemy.engine.base.Engine (100, 200)
2014-01-15 22:59:15,338 INFO sqlalchemy.engine.base.Engine INSERT INTO mytable (foo_start, foo_end) VALUES (?, ?)
2014-01-15 22:59:15,338 INFO sqlalchemy.engine.base.Engine (1, 10)
2014-01-15 22:59:15,338 INFO sqlalchemy.engine.base.Engine COMMIT
Select using filter
2014-01-15 22:59:15,339 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2014-01-15 22:59:15,340 INFO sqlalchemy.engine.base.Engine SELECT mytable.id AS mytable_id, mytable.foo_start AS mytable_foo_start, mytable.foo_end AS mytable_foo_end
FROM mytable
WHERE mytable.foo_start <= ? AND mytable.foo_end > ?
2014-01-15 22:59:15,340 INFO sqlalchemy.engine.base.Engine (42, 42)
values: [MyTable(foo_start=10, foo_end=100]
test
9 False
10 True
99 True
100 False
```