LoginSignup
7
6

More than 5 years have passed since last update.

SQLAlchemy で独自の Composite Value を作る

Last updated at Posted at 2014-01-15

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
7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6