Python コードから PostgreSQL に可変な SQL 文を投げてみた

アイキャッチ

こんにちは、みかみです。

はじめに

やりたいこと

  • Python コードと SQL 文を分離したい
  • テーブル名や指定条件などを動的にしてクエリを発行したい
  • Python から可変な SQL を投げるにはどんな方法(ライブラリ)があるのか知りたい

確認環境

  • OS:Windows10(Mac VMware Fusion)
  • Python:3.6.0
  • PostgreSQL:9.6.1(環境変数に PGPASSWORD 設定済み)

確認用データ準備

id と name カラムを持つ test テーブルを作成し、データを insert しました。

postgres=# create table test(id int, name varchar(20));
CREATE TABLE
postgres=# \d test
        テーブル "public.test"
  列  |          型           | 修飾語
------+-----------------------+--------
 id   | integer               |
 name | character varying(20) |


postgres=# insert into test values(1, 'yuki'), (2, 'ruka'), (3, 'uri');
INSERT 0 3

test テーブルに、id=1~3 の 3レコードが入りました。

postgres=# select * from test;
 id | name
----+------
  1 | yuki
  2 | ruka
  3 | uri
(3 行)

やってみた

案1. psql コマンドオプションでバインド変数を指定する

バインド変数入りの SQL を書いた外部ファイルを実行します。

取得元テーブル名と where 句で指定する id の値を、バインド変数(:変数名)で指定しています。

select * from :table where id = :id;

Pythonコードから、subprocess モジュールで psql コマンドを実行。

→ psql コマンドのオプション(-v)でバインド変数を指定して、ファイルに書いてある SQL を実行します。

import subprocess

file_name = 'select.sql'

id = 1
table = 'test'

cmd = 'psql -d postgres -h localhost -p 5432 -U postgres -f {} -v table={} -v id={}'.format(file_name, table, id)

result = subprocess.Popen(cmd, stdout=subprocess.PIPE,shell=True).communicate()[0]
print(result.decode('sjis'))

実行してみると、ちゃんと id=1 のレコードだけが取れました。

 id | name 
----+------
  1 | yuki
(1 行)

案2. Psycopg2 でバインドパラメータを指定する

Python の PostgreSQL アクセスライブラリ psycopg2 の execute メソッドで、SQL実行時にバインドパラメータを指定します。

先ほどと同じ SQL ファイル(↓)を読み込みます。

select * from :table where id = :id;

psycopg2 の execute ではバインドパラメータを %(変数名)s で指定する必要があるので、ファイルから読み込んだクエリの :変数名 を replace してから実行します。

import psycopg2

file_name = 'select.sql'

with open(file_name, 'r') as f:
    query = f.read()
query = query.replace(':table', '%(table)s').replace(':id', '%(id)s')

url = 'postgresql://postgres:password@localhost:5432/postgres'

table = 'test'
id = 1

try:
    conn = psycopg2.connect(url)
    with conn.cursor() as cursor:
        cursor.execute(query % {'table':table, 'id':id})
        print(cursor.fetchall())
except Exception as e:
    print(e)
finally:
    conn.close()

実行すると、ちゃんとパラメータが適用されて、指定した条件のレコードだけが取れました。

[(1, 'yuki')]

案3. SQLAlchemy で ORM を使う

Python の ORM ライブラリ sqlalchemy で、対象テーブルをマッピングして、指定条件のレコードを取得します。

まずは、テーブルクラスを定義してマッピングします。

from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
class Test(Base):
    __tablename__ = 'test'
    id = Column(Integer, primary_key=True)
    name = Column(String(20))

セッションを作成して、先ほどマッピングしたテーブルに対するクエリを実行します。

取得するレコードの条件は filter メソッドで指定します。

from sqlalchemy.orm.session import sessionmaker

url = 'postgresql://postgres:password@localhost:5432/postgres'
engine = create_engine(url)
session = sessionmaker(bind=engine)()

id = 1
result = session.query(Test).filter(Test.id == id).all()
for row in result:
    print(row.id, row.name)

session.close()

実行してみると、ちゃんと指定条件の結果が返ってきました。

1 yuki


なお、sqlalchemy の session.execute() でも、2. の psycopg2 と同様に SQL 文を指定して実行でき、バインド変数は通常の SQLと同じ :変数名 で指定します。

%(変数名)s 形式への replace が不要なので、外部 SQL ファイル読み込んでそのまま実行できるかと思ったのですが、コンパイルされたクエリのテーブル名にシングルクォートがついてしまい、エラーになってしまいました。。

"'test'"またはその近辺で構文エラー
LINE 1: select * from 'test' where id = 1
                      ^

おわりに(所感、わかったこと)

  • テーブル名やカラム名を可変にしないならば、SQLAlchemy でバインド変数指定がよさげ。
  • ちょっと作業用のスクリプト的な位置づけならば、psql コマンドで外部ファイル実行が一番シンプル?
  • ORM 使うのはちょっとやりすぎ感。。(アプリを作りたいわけではないので。
  • Flask(Python の アプリフレームワーク)の ORM は SQLAlchemy らしい。

参考