Composite Value とは、 PofEAA にあるパターンで、Entity が持っている属性となるシンプルな Value Object を、その Entity がマップされるテーブルの複数のカラムとしてマップすることです。
似たパターンで Serialized LOB という、オブジェクトを JSON 等にシリアライズして BLOB や CLOB に入れてしまうパターンも有ります。 Composite Value は Serialized LOB に比べると、 SQL から使えるというメリットが有ります。
SQLAlchemy で Composite Value をする例は、本家のドキュメントにあります。
Composite Column Types
ですが、この例ではオペレータを定義していないので、等値比較しかできません。
comparator_factory
を使うと、SQLの生成部分もカスタマイズできるのですが、その部分のサンプルが分かれていたので、一緒にするサンプルを作りました。
こちらのサンプルのほうが、 named tuple を使って Value Object を作ってるので楽をできています。
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