sqlalchemyのORMじゃない方の機能(expression language api)

使い方忘れることが多いのでメモ。

準備

テキトウにデータを作って準備する。

テキトウにテーブル作成。

CREATE TABLE groups (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT
);
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  group_id INTEGER,
  name TEXT,
  FOREIGN KEY(group_id) REFERENCES groups(id)
);
CREATE TABLE skills (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER,
  name TEXT,
  FOREIGN KEY(user_id) REFERENCES users(id)
);

テキトウにデータを追加。

INSERT INTO groups (id, name) VALUES(NULL, 'X');
INSERT INTO groups (id, name) VALUES(NULL, 'Y');
INSERT INTO groups (id, name) VALUES(NULL, 'Z');
INSERT INTO users (id, group_id, name) VALUES(NULL, 1, 'foo');
INSERT INTO users (id, group_id, name) VALUES(NULL, 1, 'bar');
INSERT INTO users (id, group_id, name) VALUES(NULL, 2, 'boo');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 1, 'a');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 1, 'b');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 1, 'c');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 2, 'a');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 2, 'b');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 2, 'c');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 3, 'a');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 3, 'b');
INSERT INTO skills (id, user_id, name) VALUES(NULL, 3, 'c');
groups -* users -* skills

みたいな形の構造

使う

transaction気にしなければ以下の様な感じ。まじめに自分で定義したくなければmetadataのreflectを使う。

import sqlalchemy as sa


def run(url, *, echo):
    config = {"url": url}
    engine = sa.engine_from_config(config, prefix="")
    metadata = sa.MetaData(bind=engine)
    metadata.reflect(engine)
    engine.echo = echo

    with engine.connect() as conn:
        usedb(conn, metadata.tables)

def usedb(conn, tables):
    for row in conn.execute(tables["users"].select()):
        print(row)

url = "sqlite:///../src/groups.db"
run(url, echo=True)
2017-07-05 22:19:36,252 INFO sqlalchemy.engine.base.Engine SELECT users.id, users.group_id, users.name 
FROM users
2017-07-05 22:19:36,252 INFO sqlalchemy.engine.base.Engine ()
(1, 1, 'foo')
(2, 1, 'bar')
(3, 2, 'boo')

join

普通にjoinもできる。

qs = tables["users"].join(tables["skills"], tables["users"].c.id == tables["skills"].c.user_id)
for row in conn.execute(qs.select().where(tables["users"].c.id == 1)):
print(row)
(1, 1, 'foo', 1, 1, 'a')
(1, 1, 'foo', 2, 1, 'b')
(1, 1, 'foo', 3, 1, 'c')

select(field,…)

もちろん、select句を直接指定できる。

qs = tables["users"].join(tables["skills"], tables["users"].c.id == tables["skills"].c.user_id)
for row in conn.execute(
    sa.sql.select([tables["users"].c.name, tables["skills"].c.name]).select_from(qs).where(tables["users"].c.id == 1)
):
    print(row)
('foo', 'a')
('foo', 'b')
('foo', 'c')

fetch many

limitとは別に内部的なapiがbuffering的なものをサポートしていた場合にはfetchmanyが使える。

def chunked(cursor, *, n):
    while True:
        rows = cursor.fetchmany(n)
        if not rows:
            break
        yield rows


qs = tables["users"].join(tables["skills"], tables["users"].c.id == tables["skills"].c.user_id)
for rows in chunked(conn.execute(qs.select().where(tables["users"].c.id == 1)), n=2):
    print(rows)
[(1, 1, 'foo', 1, 1, 'a'), (1, 1, 'foo', 2, 1, 'b')]
[(1, 1, 'foo', 3, 1, 'c')]