Takayuki Shimizukawa
@shimizukawa (清水川)
 (株)ビープラウド 取締役 / IT Architect
 IT勉強会支援サービス connpass
 オンライン学習サービス PyQ
 一般社団法人PyCon JP Association 理事
 PyCon JP 年次イベントの見守り
 Python Boot Camp 主催
 Sphinx コミッター(休業中)
おまえ誰よ / Who are you
22020/8/28
執筆・翻訳に関わったPython書籍
2020/8/28 3
2018/2/282018/2/26
翻訳・監訳
補章執筆
進行管理
6章: デプロイ
9章: ドキュメント
2018/6/12
3章: package
9章: package
(7章)ドキュメント
2017/10/20
1章: Sphinxとは
付録A: reStructuredTextリファレン
ス
第17刷
自走プログラマー(2020年2月27日)
2020/8/28 4
本書には
「プログラミング入門者が
中級者にランクアップ」
するのに必要な
「自走するための知識」を
「120のベストプラクティス」
にまとめました。
NEW
Django ORMをうまく使いこなすには、このスライドで紹介する事の他に
も
色々知っておくべきことがあります。
書籍『自走プログラマー』では、以下のトピックを紹介しています。
 58: DBのスキーママイグレーションとデータマイグレーションを分け
る
 59: データマイグレーションはロールバックも実装する
 60: Django ORMでどんなSQLが発行されているか気にしよう
 61: ORMのN+1問題を回避しよう
 62: SQLから逆算してDjango ORMを組み立てる
『自走プログラマー』とDjango ORM
2020/8/28 5
62020/8/28
 DjangoのORMがあればSQLが分からなくても大丈夫
(´・ω・) … 大丈夫...
 SQLとDjango ORM、用語が違って難しい
(´・ω・) SQLのgroup byはORMではannotate、とかね
 SQLは書けるけど、DjangoのORMは難しい
(´・ω・) SQLからORMクエリを逆算するのが難しいよね
 Django+SQLAlchemy ってシンプルじゃないよね?
(´・ω・`) そうかも
この資料のターゲット
72020/8/28
 Django ORM と SQLAlchemy の併用をお勧めするスライドです
 SQL の SELECT クエリのみ扱います
 INSERT/UPDATE/DELETE は Django ORM に統一でよいかと
 トランザクションなど色々考える事が増えるため
このスライドの前提
2020/8/28 8
1. Django ORMで表現が難しいSQLの例
2. SQLを直接書いてDjangoで実行する例
3. SQLAlchemyの紹介と簡単な実行例
4. SQLAlchemyでテーブル別名JOINを組み立ててDjangoに組み込む
5. SQLAlchemyとDjangoの連携に Aldjemy を使う
6. 複雑なSQLをSQLAlchemyで組み立てる
7. まとめ と 補足
アジェンダ
2020/8/28 9
(´?ω?`)Djangoでどう書くの?
102020/8/28
クエリで欲しい結果のイメージ
 personごとに、ある2つの年(year1, year2)の成績を一覧化したい
前提: ER図と欲しいデータ
2020/8/28 11
name year1_grade year2_grade
===== =========== ===========
Ham 4 5
Spam 4 2
Egg None 4
class Person(models.Model):
class Meta:
db_table = 'person'
name = models.CharField('名前', max_length=255)
class Grade(models.Model):
class Meta:
db_table = 'grade'
year = models.IntegerField('年度')
person = models.ForeignKey(Person, on_delete=models.CASCADE)
seiseki = models.IntegerField('成績', validators=[
MinValueValidator(1),
MaxValueValidator(5)
])
Django ORMの定義例
2020/8/28 12
 シンプルなクエリを書きやすい
 バージョンアップにつれて、複雑なクエリも可能になりつつある
 サブクエリや、後述する別名JOINなど
 SQLとORMでレイヤーが異なるため用語が異なる
 WHERE ではなく filter
 SELECT する項目を選択するには values
 SELECT する項目を追加するには annotate
 GROUP BY は aggregate
 INNER JOIN / LEFT OUTER JOIN は自動判別
DjangoのORMは ...
2020/8/28 13
>>> qs = Person.objects.filter(grade__year=2019) .values('name', 'grade__seiseki')
SELECT person.name, grade.seiseki
FROM person
INNER JOIN grade ON (person.id = grade.person_id)
WHERE grade.year = 2019
 INNER JOIN / LEFT OUTER JOIN
 暗黙的に可能、明示的に指定できない
 (´・ω・) このINNER JOIN を LEFT OUTER JOINに変えるにはどう書くの?
 外部キー制約が定義されていないModelのJOIN
 extrasを使えば INNER JOIN に近いクエリは可能
 ModelにForeignObjectを定義すれば、通常のクエリ実装でJOINが可能
 1つのテーブルを複数回JOIN
 FilteredRelationを使えば可能
 セルフJOIN
 不可能(裏技を駆使すれば可能)
Django ORMで表現が難しいSQL
2020/8/28 14
INNER JOIN / OUTER JOINの指定は暗黙的に可能
INNER JOIN
LEFT OUTER JOIN
.
Django ORMで表現が難しいSQL: JOIN
2020/8/28 15
>>> qs = Person.objects.filter(grade__year=2019).values('name', 'grade__seiseki')
SELECT person.name, grade.seiseki
FROM person
INNER JOIN grade ON (person.id = grade.person_id)
WHERE grade.year = %s
filter()で行を絞り込むと
通常 INNER JOIN になり
NULLが除外される
filter()で行を絞り込むと
通常 INNER JOIN になり
NULLが除外される
>>> qs = Person.objects.filter(Q(grade__year=2019)|Q(grade__isnull=True)) .values(
'name', 'grade__seiseki')
SELECT person.name, grade.seiseki
FROM person
LEFT OUTER JOIN grade ON (person.id = grade.person_id)
WHERE (grade.year = 2019 OR grade.id IS NULL)
NULLを含むfilter()指定で、
LEFT OUTER JOIN になる
外部キー制約が定義されていないModelのJOIN
extrasを使えば、内部結合(INNER JOIN相当)は可能
ただし INNER JOIN と異なり、gradeに値がないpersonは除外される。
LEFT OUTER JOIN は表現できない。
そして、コードの理解が難しくなるので書きたくない...
Django ORMで表現が難しいSQL: FKなし1
2020/8/28 16
# GradeモデルにForeignKeyを定義していない場合
>>> qs = Person.objects.extra(
... tables=['grade'],
... where=['grade.person_id=person.id', 'grade.year=2019']
... ).extra(select={'seiseki': 'grade.seiseki'})
SELECT (grade.seiseki) AS seiseki, person.id, person.name
FROM person, grade
WHERE (grade.person_id=person.id)
AND (grade.year=2019)
extra は最終手段、将来廃止予定
― QuerySet API reference より
(´・ω・`)色々な事情が
ありましてね・・・
「関連」をModelに設定すれば、普通のORMコードでJOIN可能になる
外部キー制約(ForeignKey)を定義できない状況(マイグレーションでDB
に反映されては困る時)の回避策として使える。
extraで頑張るより良い。
だが、ドキュメントに記載されていない (´・ω・`)
Django ORMで表現が難しいSQL: FKなし2
2020/8/28 17
明示的な関連のない2つのモデルを
JOINする方法は?
― Django Issue #29551 より
class Grade(models.Model):
person_id = models.IntegerField('Person')
person = ForeignObject(
Person,
models.CASCADE,
from_fields=['person_id'],
to_fields=['id']
)
テーブルを別の名前でJOINするのは可能。
Django 2.0 で導入された FilteredRelation を使う
Django ORMで表現が難しいSQL: 別名JOIN1
2020/8/28 18
>>> qs = Person.objects.annotate(
... g=FilteredRelation('grade'),
... ).filter(
... Q(g__year=2019) | Q(g__isnull=True)
... ).values('name', 'g__seiseki')
SELECT person.name, g.seiseki
FROM person
LEFT OUTER JOIN grade g ON (person.id = g.person_id)
WHERE (g.year = 2019 OR g.id IS NULL)
※ ただし JOIN には ForeignKey が必
要FilteredRelation を annotate() に指定すること
で、JOIN の ON句を指定できます。
― QuerySet API reference より
1つのテーブルを別の名前で複数回 JOINするのも可能。
Django ORMで表現が難しいSQL: 別名JOIN2
2020/8/28 19
>>> qs = Person.objects.annotate(
... g1=FilteredRelation('grade'),
... g2=FilteredRelation('grade'),
... ).filter(
... Q(g1__year=2019) | Q(g1__isnull=True),
... Q(g2__year=2020) | Q(g2__isnull=True),
... ).values('name', 'g1__seiseki', 'g2__seiseki')
SELECT person.name, g1.seiseki, T3.seiseki
FROM person
LEFT OUTER JOIN grade g1 ON (person.id = g1.person_id)
LEFT OUTER JOIN grade T3 ON (person.id = T3.person_id)
WHERE (
(g1.year = 2019 OR g1.id IS NULL) AND
(T3.year = 2020 OR T3.id IS NULL))
2つめの名前指定
は無視される
FilteredRelationの本来の使い方は、ON句で絞り込みを指定する。
絞り込んでからJOINされるため、SQLの実行効率が良い。
Django ORMで表現が難しいSQL: 別名JOIN3
2020/8/28 20
>>> qs = Person.objects.annotate(
... g1=FilteredRelation('grade', condition=Q(grade__year=2019)),
... g2=FilteredRelation('grade', condition=Q(grade__year=2020)),
... ).values('name', 'g1__seiseki', 'g2__seiseki')
SELECT person.name, g1.seiseki, T3.seiseki
FROM person
LEFT OUTER JOIN grade g1 ON ((person.id = g1.person_id) AND (g1.year = 2019))
LEFT OUTER JOIN grade T3 ON ((person.id = T3.person_id) AND (T3.year = 2020))
例: Grade.seiseki の毎年の変化をPersonごとに行いたい
 FK定義なしでのセルフJOINは、通常の方法では不可能
 裏技(!)を駆使すれば可能らしい
 https://stackoverflow.com/questions/1578362/self-join-with-django-orm
 ForeignObjectを使えば可能かもしれない
 今のところうまくいってません
 ドキュメントにないクラスを使うのは避けたい
Django ORMで表現が難しいSQL: セルフJOIN
2020/8/28 21
ここまでのまとめ
Django ORMで表現が難しいSQL
2020/8/28 22
 SQLを知ってる人は
 SQLをDjango ORMに翻訳している感じ
 Django ORMで思い通りのSQLを発行するのは意外と難しい
 SQLに不慣れな人は
 目隠しして(SQLを見ずに)データを取り出そうとする感じ
 Django ORMで思い通りのデータを得るのは意外と難しい
Django ORMで表現が難しいSQL: まとめ
2020/8/28 23
― 『自走プログラマ-』
"60: Django ORMでどんなSQLが発行されているか気にしよう" より
残念ながら、ORMは「SQLを知らなくても使える便利な仕組み」ではあ
りません。 簡単なクエリであればSQLを確認する必要はなく、多くの要
件は簡単なクエリの発行で済むかもしれません。 だからといって、ORM
がどんなSQLを発行しているか気にしないままでいると、落とし穴には
まってしまいます。
(´・ω・`)やっちゃう?
242020/8/28
欲しいデータ: personごとに、2つの年(year1, year2)の成績を一覧化
実行効率の良い、生々しいSQL
生SQLを書いて実行しよう
2020/8/28 25
SELECT
p.name AS name,
g1.seiseki AS grade1,
g2.seiseki AS grade2
FROM person AS p
LEFT OUTER JOIN grade AS g1 ON ((p.id = g1.person_id) AND (g1.year = 2018))
LEFT OUTER JOIN grade AS g2 ON ((p.id = g2.person_id) AND (g2.year = 2019))
name year1_grade year2_grade
===== =========== ===========
Ham 4 5
Spam 4 2
Egg None 4
生SQLをrawメソッドで実行する
NG!!
 文字列操作でSQLに値を埋め込むと、SQLインジェクションの原因に!
生SQLの落とし穴
2020/8/28 26
>>> year1, year2 = 2019, 2020
>>> sql = """
SELECT
p.id,
p.name,
g1.seiseki AS grade1,
g2.seiseki AS grade2
FROM person AS p
LEFT OUTER JOIN grade AS g1 ON ((p.id = g1.person_id) AND (g1.year = %s))
LEFT OUTER JOIN grade AS g2 ON ((p.id = g2.person_id) AND (g2.year = %s))
""".format(year1, year2)
>>> qs = Person.objects.raw(sql)
>>> for raw in qs:
... print(f"{p.name}, {p.grade1}, {p.grade2}")
プレースホルダ (%s) を使い、ドライバ側で値をエスケープ処理する
課題
 動的な条件追加などは文字列操作しかない(WHERE句追加など)
パラメータのエスケープ処理
2020/8/28 27
>>> year1, year2 = 2019, 2020
>>> sql = """
SELECT
p.id,
p.name,
g1.seiseki AS grade1,
g2.seiseki AS grade2
FROM person AS p
LEFT OUTER JOIN grade AS g1 ON ((p.id = g1.person_id) AND (g1.year = %s))
LEFT OUTER JOIN grade AS g2 ON ((p.id = g2.person_id) AND (g2.year = %s))
"""
>>> qs = Person.objects.raw(sql, [year1, year2])
>>> for raw in qs:
... print(f"{p.name}, {p.grade1}, {p.grade2}")
生SQLを使う前に、ORMの使用を考え
て! ― 素の SQL 文の実行 より
 動的なSQLを書くには文字列操作が必要
 WHEREに条件を追加したい、ORDER BYを変えたい
 文字列操作は、SQLインジェクションの原因に!
 SQLの方言は吸収されない
 SQLはRDBによって方言がある
 ORMなら方言の違いを吸収してくれたが...
生SQL実行における課題
2020/8/28 28
(´・ω・)ちょっとだけね
2020/8/28 29
SQLAlchemy はSQLツールキット(クエリビルダ)とORMを搭載した、
Pythonでは最も高機能なライブラリ。
DjangoのORMと比べて...
 多くのSQL構文をサポート(CTE/WITH構文など)
 ORMマッパーとクエリビルダを個別に定義できる
 思い通りのSQLを生成でき、 SQLを知っている人が使いやすい
 使い始めは、簡単ではない
SQLAlchemy?
2020/8/28 30
参考:
• Django Issue #28919
• Sharding with SQLAlchemy | PyCon JP 2017
テーブル定義 (ORMのモデル定義が不要な場合)
クエリビルダ実行例 (SQLの文法に近い表現)
SQLAlchemyのテーブル定義と実行
2020/8/28 31
import sqlalchemy as sa
metadata = sa.MetaData()
person = sa.Table(
'person',
metadata,
sa.Column('id', sa.Integer),
sa.Column('name', sa.String),
)
>>> stmt = sa.select([person]).where(person.c.name.contains('a'))
>>> print(stmt)
SELECT person.id, person.name
FROM person
WHERE (person.name LIKE '%' || :name_1 || '%')
モデル定義 (テーブル定義は自動(個別定義も可能))
クエリ実行例 (一般的なORMの構文に近い表現)
SQLAlchemyのモデル定義と実行
2020/8/28 32
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
metadata = sa.MetaData()
Base = declarative_base()
class Person(Base):
__tablename__ = 'users'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker()
>>> stmt = session.query(Person).filter(Person.name.contains('a'))
>>> print(stmt)
SELECT person.id AS person_id, person.name AS person_name
FROM person
WHERE (person.name LIKE '%' || :name_1 || '%')
(`・ω・)見せて貰おうか、SQLAlchemyの性能とやらを
332020/8/28
Djangoのモデル定義とは別に定義が必要
SQLAlchemyのテーブル定義
2020/8/28 34
import sqlalchemy as sa
metadata = sa.MetaData()
person = sa.Table(
'person',
metadata,
sa.Column('id', sa.Integer),
sa.Column('name', sa.String),
)
grade = sa.Table(
'grade',
metadata,
sa.Column('id', sa.Integer),
sa.Column('person_id', None, sa.ForeignKey('person.id')),
sa.Column('year', sa.Integer),
sa.Column('seiseki', sa.Integer),
)
SQLビルダーの書き味はほぼSQL
 ほぼSQLをPythonコードで書けるのが最大のメリット
SQLAlchemyのクエリステートメント
2020/8/28 35
import sqlalchemy as sa
year1, year2 = 2019, 2020
g1 = grade.alias('g1') # AS g1
g2 = grade.alias('g2') # AS g2
stmt = sa.select([
person.c.id,
person.c.name,
g1.c.seiseki.label('grade1'),
g2.c.seiseki.label('grade2'),
]).select_from(
person.
outerjoin(g1, sa.and_(g1.c.person_id == person.c.id, g1.c.year == year1)).
outerjoin(g2, sa.and_(g2.c.person_id == person.c.id, g2.c.year == year2))
)
sqliteのdialect(方言)に変換
 SQLAでの実装コードそのままのSQLが生成される
 compile時に接続先RDB用のdialectを選ぶ必要がある
SQLAlchemyで生成されるSQL
2020/8/28 36
>>> from sqlalchemy.dialects.sqlite import dialect
>>> query = stmt.compile(dialect=dialect())
>>> sql, params = str(query), query.params
>>> print(sql)
>>> print('params=', params)
SELECT
person.id,
person.name,
g1.seiseki AS grade1,
g2.seiseki AS grade2
FROM person
LEFT OUTER JOIN grade AS g1 ON g1.person_id = person.id AND g1.year = %s
LEFT OUTER JOIN grade AS g2 ON g2.person_id = person.id AND g2.year = %s
params= [2019, 2020]
 SQLAlchemyからRDBに接続するために独自のコネクションは使いたくない
 Djangoの設定と別に管理したくない、コネクションを共存したい
Djangoのconnectionを使ってSQLを実行
結果は行毎に値のタプルとなるため、行毎の辞書に変換
SQLAlchemyとDjangoの共存
2020/8/28 37
from django.db import transaction
def execute_raw_sql(sql, params):
conn = transaction.get_connection()
with conn.cursor() as cursor:
cursor.execute(sql, params)
yield from cursor
def execute(stmt):
sql, params = ... # compile等
results = execute_raw_sql(sql, params)
columns = [c.name for c in stmt.columns]
for row in results:
yield dict(zip(columns, row))
SQLAlchemyをDjangoに組み込む観点で
メリット
 SQLを知っている人が使いやすい
 文字列操作せずに動的なSQLを組める
 多くのSQL構文に対応していて、RDBの機能をフル活用できる
デメリット
 SQLA用のテーブル定義が必要
 RDBにSQLを渡して実行するための実装が増える
SQLAlchemyをDjangoに組み込むメリットデメリット
2020/8/28 38
(・ω・`) ほぅ
392020/8/28
Aldjemy は SQLAlchemyをDjangoに組み込むプラグイン
メリット
 SQLAを使ってクエリを書ける
 Django settingsのDB接続情報を使ってくれる
 SQLA用のテーブル定義不要
 compileもしてくれるため、RDBの種類を気にしなくて良い
 RDBにSQLを渡す処理もやってくれる
デメリット
 RDBコネクションはSQLA独自で接続
 Djangoのpoolから生connectionを取得 【追記: 2020/08/28 17:50】
 このためDjango Debug Toolbar等でSQLを確認できない
Aldjemy?
2020/8/28 40
インストール
Django settings.py
Django Modelに sa が生えるので、sa経由で実行
.sa 以降はSQLAlchemyのORM
Aldjemyの設定
2020/8/28 41
$ pip install aldjemy
INSTALLED_APPS = [
...
'aldjemy',
]
>>> Person.sa.query().filter(Person.sa.name.contains('a')).all()
[<aldjemy.orm.Person object at 0x0000026D09D49220>,
<aldjemy.orm.Person object at 0x0000026D09D49310>]
 実行されるSQLは、aldjemyを使わない場合と同じ
 実行結果(戻り値)もSQLAlchemyのORMと同じ
Aldjemyでの実装サンプル
2020/8/28 42
import sqlalchemy as sa
from app.models import Person, Grade # Djangoのモデル
year1, year2 = 2019, 2020
g1 = Grade.sa.table.alias('g1') # SQLAで AS g1 を指定
g2 = Grade.sa.table.alias('g2') # SQLAで AS g2 を指定
stmt = Person.sa.query(
Person.sa.id,
Person.sa.name,
g1.c.seiseki.label('grade1'),
g2.c.seiseki.label('grade2'),
).select_from(
Person.sa.table.
outerjoin(g1, sa.and_(g1.c.person_id == Person.sa.id, g1.c.year == year1)).
outerjoin(g2, sa.and_(g2.c.person_id == Person.sa.id, g2.c.year == year2))
)
results = stmt.all()
 RDBコネクションがDjangoと別で管理されるところは気になる
 コネクションをそれほど気にしない場合は無視できる
 【追記: 2020/08/28 17:50】
 connection自体はDjangoから取り出しているため、session数は心配ない
 Djangoのconnection wrapperを回避しているためDjangoのサポートは受けられない
 Django + SQLAlchemy を小さく始めるのに良い
Aldjemy まとめ
2020/8/28 43
(`・ω・´)任せろー バリバリー
442020/8/28
join か outerjoin を明示的に指定
INNER JOIN / LEFT OUTER JOIN
2020/8/28 45
>>> stmt = sa.select([
... person.c.name,
... grade.c.year,
... grade.c.seiseki,
... ]).select_from(
... person.outerjoin(grade,
... sa.and_(grade.c.person_id == person.c.id, grade.c.year == 2019))
... )
SELECT person.name, grade.year, grade.seiseki
FROM person
LEFT OUTER JOIN grade ON grade.person_id = person.id AND grade.year = %s
name year seiseki
===== ==== =======
Ham 2019 4
Spam 2019 4
Egg None None
FKがある場合
JOINの明示は必要だが、ON句は不要
FKがない場合
ON句を指定できます
FKのあるJOIN, FKのないJOIN
2020/8/28 46
>>> stmt = sa.select([
... person.c.name,
... grade.c.seiseki,
... ]).select_from(
... person.join(grade)
... )
>>> stmt = sa.select([
... person.c.name,
... grade.c.seiseki,
... ]).select_from(
... person.join(grade, grade.c.person_id==person.c.id)
... )
セルフJOIN
2020/8/28 47
>>> g1, g2 = grade.alias('g1'), grade.alias('g2')
>>> stmt = sa.select([
... person.c.name,
... g2.c.year,
... g1.c.seiseki.label('last_year_grade'),
... g2.c.seiseki.label('current_year_grade'),
... (g2.c.seiseki - g1.c.seiseki).label('diff'),
... ]).select_from(
... g1.join(person).join(g2,
... sa.and(g1.c.person_id == g2.c.person_id, g1.c.year == g2.c.year + 1))
... ).order_by(person.c.id, g1.c.year)
SELECT person.name, g2.year,
g1.seiseki AS last_year_grade,
g2.seiseki AS current_year_grade,
g2.seiseki - g1.seiseki AS diff
FROM grade AS g1
JOIN person ON person.id = g1.person_id
JOIN grade AS g2 ON g1.person_id = g2.person_id
AND g1.year = g2.year + %s
ORDER BY person.id, g1.year
(´・ω・`)おつかれさまでした
482020/8/28
 DjangoのORMを使いこなすには
 SQLを書けるようになろう
 SQLとORMの関係を覚えよう
 SQLからDjango ORMへの翻訳に疲れたら
 SQLAlchemyでSQLを組み立てよう
 動的に変化しないなら、生SQLを書くのもあり(sql-importerを使うと安全)
 SQLAlchemyをDjangoに組み込むには
 Aldjemyを使うのが比較的楽
 細かく制御が必要になったら、コネクション周りを実装
 Django+SQLAlchemy ってシンプルじゃないよね?
 (・ω・`)はい
 (´・ω・)ORMの書き方で悩むよりはシンプルかと
まとめ と 補足
492020/8/28
【訂正】
connection周りでDjangoのサ
ポートが必要になった場合は、
Questions?
Twitter: @shimizukawa
Slack Channel: jp-2020-track3 .
HashTag: #pyconjp_3 .
2020/8/28 50
Thanks :)
2020/8/28 51

【修正版】Django + SQLAlchemy: シンプルWay