#この記事について
Djangoのデータベース操作について網羅している日本語の記事が無かったので自分でまとめてみる。
#Djangoのデータベース操作
DjangoではORM(オブジェクト関係マッピング)を使ってデータベース操作を行う。
DjangoのORMの実装はActiveRecordパターンを採用している。
RubyOnRailsのActiveRecordと同様に以下のような機能があり、最低限の記述で簡易にデータベースを扱うことができる。
・モデルを設計すればマイグレーション機能でDB定義を自動作成する。
・SQLの記述は不要。モデルマネージャ経由でクエリを実行するとレコードをモデルのインスタンスとして扱える。
・クエリは人間に読みやすく命名されたマネージャメソッドの連鎖として表現する。等
参考:ActiveRecord入門 - Qiita
https://qiita.com/kimioka0/items/8c10e01def23fdbf3aa6
#公式マニュアルを読む
モデル設計、データベースの情報は、公式サイトの以下のトピック内のリンクで網羅されている
肝心なところは大体日本語化されているので、とりあえず日本語のとこだけでも読むと理解がすすむ。
※URLは最新バージョンに合わせて「2.0」の部分を変える
インデックス
https://docs.djangoproject.com/ja/2.0/topics/db/
https://docs.djangoproject.com/ja/2.0/ref/models/
モデルの基礎知識
https://docs.djangoproject.com/ja/2.0/topics/db/models/
クエリの基礎知識
https://docs.djangoproject.com/ja/2.0/topics/db/queries/
クエリAPIの全リスト
https://docs.djangoproject.com/ja/2.0/ref/models/querysets/
リレーション用クエリの説明
https://docs.djangoproject.com/ja/2.0/ref/models/relations/
リレーション設定のサンプル
https://docs.djangoproject.com/ja/2.0/topics/db/examples/
クエリの使い方:検索
https://docs.djangoproject.com/ja/2.0/topics/db/search/
クエリの使い方:高度なクエリ表現(Fオブジェクト等)
https://docs.djangoproject.com/ja/2.0/ref/models/expressions/
クエリの使い方:集計関数(SQLのCOUNT,SUM等関数を使う方法)
https://docs.djangoproject.com/ja/2.0/topics/db/aggregation/
クエリの使い方:素のSQLを実行する方法
https://docs.djangoproject.com/ja/2.0/topics/db/sql/
複数のデータベースを使う
https://docs.djangoproject.com/ja/2.0/topics/db/multi-db/
最適化・パフォーマンスチューニング
https://docs.djangoproject.com/ja/2.0/topics/db/optimization/
公式サイトがGoogleの検索で出てこない問題
Djangoの欠点の一つともいえるが、日本へのSEO対策が不十分なのでグーグルで「django database」のように検索しても公式サイトのページが上位に出てこない。公式サイトの検索機能も残念な出来映え。
調べたいことがあったらGoogleのサイト内検索を使うと確実である。
検索例:
site:https://docs.djangoproject.com/ select_related
#動作を確認する(SQLのログ出力方法)
ある程度理解したら、実際にクエリを実行し、どのようなSQLが発行されるか色々試行してみるといい。SQL をログで確認する方法はこちらのサイトの記事に完璧にまとまっている。
Django ORM の SQL を出力する方法まとめ - akiyoko blog
http://akiyoko.hatenablog.jp/entry/2016/08/04/232531
複数の方法があるが、とりあえずDjangoの動作の確認をする場合は「manage.py」を起動して6番の方法を採るのがいい。
#ログ出力有効化コマンド
import logging
l = logging.getLogger('django.db.backends')
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())
#クエリの要点
マニュアルだとボリュームがありすぎるので要点だけメモする
##クエリメソッド(QueryAPI)一覧表
Django v2.0に対応
各メソッドの詳細は公式サイトを参照のこと
https://docs.djangoproject.com/ja/2.0/ref/models/querysets/
大分類 | 小分類 | メソッド |
---|---|---|
全件取得する | all() | |
検索条件を指定 | 肯定 | filter(**kwargs) |
NOT条件 | exclude(**kwargs) | |
IN条件の中身をリストで指定する | in_bulk(id_list=None, field_name='pk') | |
並び順を指定 | 昇順 | order_by(*fields) |
降順 | reverse() | |
クエリセットからインスタンスを取得する | 検索に一致したもの | get(**kwargs) |
指定した日付順で最新のもの | latest(*fields) | |
指定した日付順で最古のもの | earliest(*fields) | |
クエリセットの先頭 | first() | |
クエリセットの最後 | last() | |
レコードを新規登録する | 新規登録 | create(**kwargs) |
条件に一致するものを取得、無い場合は新規登録して取得 | get_or_create(defaults=None, **kwargs) | |
条件に一致するものを更新、無い場合は新規登録 | update_or_create(defaults=None, **kwargs) | |
一括登録する | bulk_create(objs, batch_size=None) | |
一括更新する | update(**kwargs) | |
一括削除する | delete() | |
特定のフィールドを集計する | テーブル内の集計 | aggregate(*args, **kwargs) |
参照先テーブルの集計 | annotate(*args, **kwargs) | |
該当のレコード件数 | count() | |
抽出したレコードから重複の無い日付配列を作る | dates(field, kind, order='ASC') | |
抽出したレコードから重複の無い時刻配列を作る | datetimes(field_name, kind, order='ASC', tzinfo=None) | |
クエリセットの存在チェックをする | exists() | |
インスタンスの一部だけを取得 | 結果を辞書のリストで取得する | values(*fields, **expressions) |
結果をタプルのリストで取得する | values_list(*fields, flat=False, named=False) | |
特定のフィールドの重複無しリストを作る | distinct(*fields) | |
SQLを直接実行する | SQLを直に設定する(一部) | extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None) |
SQLを直に設定する(全部) | raw(raw_query, params=None, translations=None) | |
クエリセット同士の集合演算をする | 和集合 | union(*other_qs, all=False) |
積集合 | intersection(*other_qs) | |
差集合 | difference(*other_qs) | |
パフォーマンスチューニング キャッシング | SQLのJOIN句を使って関係先を同時に取得しキャッシュする。1-1,N-1で有効 | select_related(*fields) |
関係先を先行取得してキャッシュする。1-N、N-Nで有効。 | prefetch_related(*lookups) | |
パフォーマンスチューニング 重いカラムを取得しない | 指定カラムだけ取得しない | defer(*fields) |
指定カラムだけ取得する | only(*fields) | |
パフォーマンスチューニング 巨大なテーブルをループで扱う | iterator(chunk_size=2000) | |
データベースのレコードロックを使う | select_for_update(nowait=False, skip_locked=False, of=()) | |
デフォルト以外のデータベースを使う | using(alias) | |
空のクエリセットを作る | none() |
##概要
###クエリとは
データベースからデータを取得するための記述法。
マネージャメソッドを連鎖させて表現する。
記述に対応するSQL が内部的に実行され、テーブルのレコードをモデルのインスタンスとして得ることができる。
取得したインスタンスに対して変更を行い、インスタンスのsaveメソッド(またはdelete)を実行することで変更を反映できる。
ショートハンド機能として、一括更新や一括削除、集計値を辞書型で取得するメソッドも多種用意されている。
###クエリセットとは
クエリの実行結果をクエリセットと呼ぶ。
外形的にはインスタンスの配列が戻り値として返ると考えていいが、内部ではパフォーマンス向上のために遅延評価やキャッシングなどの処理が行われているので、よく知らないで使うと問題が起きるかもしれない。
公式サイトのクエリの説明ページに概要がのっているので、前述のログ出力設定を有効にして動作の確認をしてほしい。
###クエリの文法
クエリはモデルマネージャのメソッドとして実行する
モデルクラス.モデルマネージャ.メソッド()
以下の例で、「objects」の部分がモデルマネージャと呼ばれている。
all = Entry.objects.all() #全件取得
メソッドは続けて書くことで処理内容を追加できる。
Entry.objects.filter(name='Taro').order_by('pub_date').reverse()
レコードの取得を行うクエリの場合、メソッドの最後に添字やスライスを加えることで、取得位置や件数の指定をすることができる。
Entry.objects.order_by('pub_date')[0] #最初の1件
Entry.objects.order_by('pub_date')[:5] #上位5件
レコードを新規作成(INSERT)したい場合は、モデルのインスタンスを新規作成してsaveコマンドを実行すればよい。
既存レコードを取得した各インスタンスはsaveまたはdeleteメソッドで変更内容をデータベースに反映できる。
#新規作成:
b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
b.save() #INSERTが実行される
#更新
b = Blog.objects.get(id=1)
b.name = 'new Name'
b.save() #ここでUPDATEが実行される
#削除
b = Blog.objects.get(id=1)
b.delete() #DELETEが実行される
このように個別のインスタンスを操作する他、Update、Deleteメソッドのように、複数レコードに対して一括処理をする機能がある。
get_or_createメソッドメソッドのように該当データがあれば取得、なければ新規作成して取得という複合的なメソッドもある。
良く調べて最適なメソッドを使うようにすること。
検索する
filter, exclude, getでは検索条件を指定する。
指定方法は用意された記述法を使う。詳しくは公式サイトの以下のページを参照
###参考:公式サイト
https://docs.djangoproject.com/ja/2.0/topics/db/search/
https://docs.djangoproject.com/ja/2.0/ref/models/expressions/
記法
#フィールド名__演算子=比較値
#アンダースコア2つでつなぐ。スペースは入れない。
Person.objects.filter(name__startwith='T')
#演算子未指定時は完全一致(exact)として扱われる
Person.objects.filter(name='Taro')
#カンマで区切ることで、複数の条件を指定可能(ANDになる)
Entry.objects.filter(name='Taro', age__gte=12)
#条件はQオブジェクトでも指定可能。
#OR条件の指定はQオブジェクトを使わなければできない。
from django.db.models import Q
Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)
#条件にフィールド値を使う場合はFオブジェクトを使う
from django.db.models import F
# Find companies that have more employees than chairs.
Company.objects.filter(num_employees__gt=F('num_chairs'))
#そのまま計算にも使える
reporter.update(stories_filed=F('stories_filed') + 1)
検索キーワードの一覧
キーワード | 効果 |
---|---|
exact, iexact | 完全一致 ※演算子未指定時のデフォルト |
contains, icontains | 部分一致 |
in | いずれかを完全一致(配列を指定) |
gt | 超 |
gte | 以上 |
lt | 未満 |
lte | 以下 |
startswith, istartswith | ~で始まる |
endswith, iendswith | ~で終わる |
range | 範囲指定 |
regex, iregex | 正規表現 |
isnull | null判定 |
date,year...second | 時間の要素で判定(以下参照) |
小文字のiがついているのは大文字小文字の区別無しバージョン
null判定はisnull以外のもfilter(pub_date=None)でもできる。
isnullは主にIS NOT NULLの指定で使う。
filter(pub_date__isnll=False)
時間系のキーワードはdateまたはdatetimeフィールドに続けて指定し、任意の要素(年、月、日、時間など)を取り出して比較する際に使う
filter(pub_date__year__gt>2017)
##データベース関数
update, create, filter, order by, annotation, aggregate の各メソッドではデータベース関数を使うことができる。
#例 LENGTH関数 名前の長さで降順に並び替え
Company.objects.order_by(Length('name').asc())
指定方法は関数によってことなる。使える関数は以下の通り
機能 | メソッド |
---|---|
型のキャスト | Cast(expression, output_field) |
NULLじゃない最初の要素 | Coalesce(*expressions, **extra) |
複数の中で一番大きい要素 | Greatest(*expressions, **extra) |
複数の中で一番大きい要素 | Least(*expressions, **extra)L |
全て小文字に変換 | Lower(expression, **extra) |
全て大文字に変換 | Upper(expression, **extra) |
データベースの現在時間 | Now() |
指定時間以下の切り捨て | Trunc(expression, kind, output_field=None, tzinfo=None, **extra) |
文字列の結合 | Concat(*expressions, **extra) |
文字列の文字数 | Length(expression, **extra) |
文字列の検索 | StrIndex(string, substring, **extra) |
文字列の抜出 | Substr(expression, pos, length=None, **extra) |
使い方は公式サイトを参照。
https://docs.djangoproject.com/en/2.0/ref/models/expressions/
https://docs.djangoproject.com/en/2.0/ref/models/database-functions/
##リレーションを扱う
結合先テーブルの操作も簡単にできる。
###記法
####1対1、N対1の関係
#関係先モデル名__結合先モデルのフィールド名__演算子 = 比較値
e = Entry.objects.select_related('blog').get(id=5)
b = e.blog
**注意:select_relatedは無くても動くが、キャッシングによりパフォーマンスが上がるので、対1の関係では極力使う。**ループ処理の対象とする場合は1+N問題が解消されるので必ず使う。
####1対N、N対Nの関係
#関連先モデル名_setのプロパティでアクセスできる。
#モデル名に大文字があっても、全て小文字になることに注意!
b = Blog.objects.get(id=1)
b.entry_set.all() # Returns all Entry objects related to Blog.
# b.entry_set is a Manager that returns QuerySets.
b.entry_set.filter(headline__contains='Lennon')
b.entry_set.count()
対Nのパフォーマンスチューニングにはprefetch_relatedが使える。
####対Nリレーション操作専用メソッド
対1の関係の「モデル名_set」に対する専用メソッドがある。
参考
https://docs.djangoproject.com/ja/2.0/ref/models/relations/
機能 | メソッド |
---|---|
レコードの追加(既存のオブジェクトを追加) | add(*objs, bulk=True) |
レコードの追加(オブジェクトを作って追加) | create(**kwargs) |
レコードの一部削除 | remove(*objs) |
レコードの全部削除 | clear() |
レコードの置き換え | set(objs, bulk=True, clear=False) |
##集計する
参考
公式サイト
https://docs.djangoproject.com/ja/2.0/topics/db/aggregation/
記法
集計はaggregateかannotateで集計用の記法を使って指定する。
参考:公式サイト
https://docs.djangoproject.com/en/2.0/ref/models/querysets/#aggregation-functions
機能 | 記法 |
---|---|
平均 | Avg(expression, output_field=FloatField(), filter=None, **extra) |
件数 | Count(expression, distinct=False, filter=None, **extra) |
最大 | Max(expression, output_field=None, filter=None, **extra) |
最小 | Min(expression, output_field=None, filter=None, **extra) |
合計 | Sum(expression, output_field=None, filter=None, **extra) |
標準偏差 | StdDev(expression, sample=False, filter=None, **extra) |
分散 | Variance(expression, sample=False, filter=None, **extra) |
filterで、集計フィールドに対して条件を指定することでSQLのHAVINGの表現になる。
Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)
###使用例
集計式は目的によって使用するメソッドが異なる。
####テーブル内で集計
aggregateを使う
Book.objects.all().aggregate(Avg('price'))
####テーブル内でグループ化して集計する
GROUP_BYに使うフィールドをvaluesで指定し、集計内容をannotateで指定
Author.objects.values('name').annotate(average_rating=Avg('book__rating'))
####関係元テーブルの付属情報として関係先テーブルの値を集計する
annotateを使う
Publisher.objects.annotate(num_books=Count('book'))
####注意:集計は難しい
以下のページにあるように、Django の集計処理は複雑なことをするのが苦手。
http://pod.hatenablog.com/entry/2016/06/04/011530
簡単なものはともかく、複雑な集計をクエリーをメソッドで再現するのは色々問題が出る模様。rawメソッドを使って直接SQLを設定することも検討しよう。
##フィールド値で重複を取り除く
distinctの使い方。
values または values_list でフィールドを選択してからdistinct()をかける。
Prefecture.objects.values('Region').distinct()
参考:field値での重複削除
https://qiita.com/maisuto/items/de5d5ad77b5a502ecd03
###サブクエリを使う
クエリセットそのものをサブクエリとして指定することができる。
参考:公式サイト
https://docs.djangoproject.com/en/2.0/ref/models/expressions/#subquery-expressions
from django.db.models import OuterRef, Subquery
newest = Comment.objects.filter(post=OuterRef('pk')).order_by('-created_at')
Post.objects.annotate(newest_commenter_email=Subquery(newest.values('email')[:1]))