破棄されたブログ

このブログは破棄されました。

複雑なクエリ生成をカプセル化したい気持ちがあった

注意: 話がまとまってない書きなぐり

複数のテーブルを結合したり、ある条件が指定された場合だけあるテーブルを結合するというコードを実装したい。

でも、それを愚直に実装しようとすると、条件分岐地獄が待ち受けていて、たちまち手に負えなくなってしまう。

そこで、クエリ生成を意図の明白なインタフェースを持つビルダー的なオブジェクトでカプセル化してやって、 生成処理が地獄絵図になるのを避けたい気持ちになった。

今回は、 SQLAlchemy を使った実装例を考えてみた。テーブル定義は以下の通り。外部キー制約すら無いので、自分で結合条件をしてしてやる必要があるつらたん。

from sqlalchemy import (
    MetaData,
    Table,
    Column,
    Integer,
    select,
    and_,
)

meta = MetaData()


parent = Table(
    'parent', meta,
    Column('id', Integer),
    Column('some_value', Integer),
    Column('key', Integer),
    Column('a_condition', Integer),
)

child_1 = Table(
    'child_1', meta,
    Column('key', Integer),
    Column('b_condition', Integer),
)

child_2 = Table(
    'child_2', meta,
    Column('key', Integer),
    Column('c_condition', Integer),
)

これテーブルから、 a_condition, b_condition, c_condition のいずれかを指定して、 指定した条件からクエリオブジェクトを組み立てるというオブジェクトを考える。

インタフェースは以下の通り

class QueryBuilder(object):
    def a_condition_is(self, value):
        pass

    def b_condition_is(self, value):
        pass

    def c_condition_is(self, value):
        pass

    def build(self):
        pass

まずは、条件が指定されるたびに、必要とするテーブルを結合して、 WHERE 句を継ぎ足す実装を考えてみた。

class QueryBuilder(object):

    def __init__(self, query=None):
        self.query = (
            select([parent.c.id])
            if query is None
            else query
        )

    def a_condition_is(self, value):
        return self.__class__(
            self.query.where(
                parent.c.a_condition == value
            ))

    def b_condition_is(self, value):
        return self.__class__(
            self.query.where(
                child_1.c.b_condition == value
            ).select_from(
                parent.join(
                    child_1,
                    child_1.c.key == parent.c.key
                )
            )
        )

    def c_condition_is(self, value):
        return self.__class__(
            self.query.where(
                parent.c.key.in_(
                    select([
                        child_2.c.key
                    ]).where(
                        child_2.c.c_condition == value
                    )
                )
            )
        )

    def build(self):
        return self.query

実装はシンプルだけど、こいつは極めて微妙なクエリを吐き出す

builder = QueryBuilder()
builder = builder.a_condition_is(2)
builder = builder.b_condition_is(7)
builder = builder.a_condition_is(3)
builder = builder.c_condition_is(13)
builder = builder.b_condition_is(5)
builder = builder.c_condition_is(11)
print(builder.build())
SELECT parent.id
FROM parent
JOIN child_1 ON child_1.key = parent.key,
                parent
JOIN child_1 ON child_1.key = parent.key
WHERE parent.a_condition = :a_condition_1
  AND child_1.b_condition = :b_condition_1
  AND parent.a_condition = :a_condition_2
  AND parent.key IN
    (SELECT child_2.key
     FROM child_2
     WHERE child_2.c_condition = :c_condition_1)
  AND child_1.b_condition = :b_condition_2
  AND parent.key IN
    (SELECT child_2.key
     FROM child_2
     WHERE child_2.c_condition = :c_condition_2)

まあよくみたらこわれているしホント話しにならない

次に、必要なテーブルと条件をためておいて、 build() のときに組み立てるパターンを考えた

class QueryBuilder(object):

    def __init__(self, required_tables=(), conditions=()):
        self.required_tables = required_tables
        self.conditions = conditions

    def append_required_table(self, table):
        required_tables = (
            self.required_tables
            if table.name in [t.name for t in self.required_tables]
            else self.required_tables + (table,)
        )

        return self.__class__(
            required_tables,
            self.conditions
        )

    def append_condition(self, condition):
        return self.__class__(
            self.required_tables,
            self.conditions + (condition,)
        )

    def a_condition_is(self, value):
        return self.append_condition(
            parent.c.a_condition == value
        )

    def b_condition_is(self, value):
        return self.append_required_table(
            child_1
        ).append_condition(
            child_1.c.b_condition == value
        )

    def c_condition_is(self, value):
        for idx, condition in enumerate(self.conditions):
            if (
                    condition.left.table == parent and
                    hasattr(condition.right, 'element') and
                    hasattr(condition.right.element, 'froms') and
                    condition.right.element.froms[0] == child_2
            ):
                new_condition = condition.operator(
                    condition.left,
                    condition.right.where(
                        child_2.c.c_condition == value
                    ))

                return self.__class__(
                    self.required_tables,
                    self.conditions[:idx] + (
                        new_condition,
                    ) + self.conditions[(idx + 1):],
                )

        return self.append_condition(
            parent.c.key.in_(
                select([
                    child_2.c.key
                ]).where(
                    child_2.c.c_condition == value
                )
            )
        )

    def build(self):
        return select([
            parent.c.id,
            parent.c.some_value,
        ]).select_from(
            reduce(
                lambda l, r: l.join(r, l.c.key == r.c.key),
                (parent,) + self.required_tables,
            )
        ).where(reduce(
            and_, self.conditions,
        ))

サブクエリに条件文を差し込むのに、無駄に複雑になってしまった。 Expression オブジェクトをごりごり置き換えてる。

その分吐き出すクエリは幾分ましになった。

SELECT parent.id,
       parent.some_value
FROM parent
JOIN child_1 ON parent.key = child_1.key
WHERE parent.a_condition = :a_condition_1
  AND child_1.b_condition = :b_condition_1
  AND parent.a_condition = :a_condition_2
  AND parent.key IN
    (SELECT child_2.key
     FROM child_2
     WHERE child_2.c_condition = :c_condition_1
       AND child_2.c_condition = :c_condition_2)
  AND child_1.b_condition = :b_condition_2

サブクエリ部分はもう一段カプセル化して、条件文を差し込みやすくしてやるといいのかもしれない。

具合が悪くなってきた

広告を非表示にする