前回はSELECT文まわりを試したので、今回はINSERT, UPDATEまわりを試してみます。
なお、ベースのアプリは前回のものを引き継ぎます。
目次
環境
- Windows7 x64
- Python 3.4.3
- Django 1.8.4
- SQLite3
- PostgreSQL 9.4.4 x64
複数データベース環境の用意
今回はSELECT FOR UPDATE
を試してみようと思いますが、前回の環境で使用したSQLiteでは実装されていません。
PostgreSQLではSELECT FOR UPDATE
が実装されていることから、以下を参考に、Djangoで複数データベース環境を構築します。
Multiple databases | Django documentation | Django
Database Routerの作成
DATABASESのdefault
以外に接続する場合、using()
で使用するデータベースを指定できます。
Multiple databases | Django documentation | Django
ただ、毎回手動で指定するのは面倒なので、今回は以下を参考にDatabase Routerを作成し、テーブル名がstore
の場合はPostgreSQLへ自動接続するように設定しました。
Writing database migrations | Django documentation | Django
settings.py
と同じディレクトリにrouters.py
ファイルを作成します。
# <project_root>/django_sql_sample/routers.py class DatabaseRouter(object): def db_for_read(self, model, **hints): # 通常はapp_labelを使うと思われるが、 # 今回は同じアプリ内なので、識別する方法として`db_table`を使う if model._meta.db_table == 'store': # DATABASESのconnection名を返す # テーブル名ではないので注意 return 'postgres' return None def db_for_write(self, model, **hints): if model._meta.db_table == 'store': return 'postgres' return None def allow_relation(self, obj1, obj2, **hints): # 今回のアプリではJOINは扱わないので、デフォルトのNoneを返す return None def allow_migrate(self, db, app_label, model_name=None, **hints): # Migrationは常に許可する return True
settings.py
の設定
PostgreSQLの設定
以下を参考に、DATABASES
へ設定を追加します。
Settings - #databases | Django documentation | Django
# <project_root>/django_sql_sample/settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), }, 'postgres': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'django_sql', 'USER': 'postgres', 'PASSWORD': 'postgres', 'HOST': '127.0.0.1', 'PORT': '5432', } }
DATABASE_ROUTERSの設定
使用するDatabase routerの値を設定します。
# <project_root>/django_sql_sample/settings.py # DATABASE_ROUTERSは`<パス>.<ファイル名>.<クラス名>` DATABASE_ROUTERS = ['django_sql_sample.routers.DatabaseRouter', ]
Modelの追加
SELECT FOR UPDATEで使うModelを、model.pyに追加します。
なお、接続先をテーブル名で振り分けるため、Meta
オプションのうちdb_table
を使ってテーブル名を指定しておきます。
Model Meta options | Django documentation | Django
# <project_root>/apps/runner/models.py class Store(models.Model): name = models.CharField(max_length=300) registered_users = models.PositiveIntegerField() hoge = models.CharField(max_length=100, default='fuga') class Meta: db_table = 'store'
本来ならapp_label
を使うと思いますが、単一のDjangoアプリで複数データベースを扱ってみたかったため、db_table
を使いました。
他に良い方法があれば、そちらに切り替えたいです。
INSERT
Store(name='st1', registered_users=1).save() #=> INSERT INTO "store" ("name", "registered_users", "hoge") VALUES ('st1', 1, 'fuga') RETURNING "store"."id"
なお、複数件を一括でINSERTしたい場合は、いくつか注意するところがあるものの、bulk_create()
が使えます。
QuerySet API reference - #bulk-create | Django documentation | Django
UPDATE
filter()
による条件指定での更新
対象のオブジェクトを取得した後、オブジェクトを更新してsave()
にてUPDATE文が実行されます。
s = Store.objects.filter(name='st1').first() s.registered_users += 1 s.save() #=> UPDATE "store" SET "name" = 'st1', "registered_users" = 2, "hoge" = 'fuga' WHERE "store"."id" = 1
update()
による複数件の一括更新
update()
メソッドで、複数件の一括更新を行います。
Making queries - #updating-multiple-objects-at-once | Django documentation | Django
今回は、複数行のregistered_users列をインクリメントしてみます。テーブルのregistered_users
列を参照するため、F() expressions
を使っています。
- Query Expressions - #f-expressions | Django documentation | Django
- Query-related classes - #f-expressions | Django documentation | Django
Store.objects.values().filter(name__istartswith='st').update(registered_users=F('registered_users') + 1) #=> UPDATE "store" SET "registered_users" = ("store"."registered_users" + 1) # WHERE UPPER("store"."name"::text) LIKE UPPER('st%')
SELECT or INSERT
Djangoのget_or_create()
を使うと、テーブルに存在すればSELECT、存在しなければINSERTを行えます。
- QuerySet API reference - #django.db.models.query.QuerySet.get_or_create | Django documentation | Django
- get_or_createの更新時のデフォルト値の扱いなど | 遍歴プログラマ日記
発行されるSQLの確認
# 実行前に全削除 Store.objects.all().delete() # INSERT Store.objects.get_or_create( name = 'get_or_create1', registered_users = 2, hoge = 'fuga1', ) #=> INSERT INTO "store" ("name", "registered_users", "hoge") VALUES ('get_or_create1', 2, 'fuga1') # SELECT Store.objects.get_or_create( name = 'get_or_create1', registered_users = 2, ) #=> SELECT * FROM "store" WHERE ("store"."registered_users" = 2 AND "store"."name" = 'get_or_create1')
なお、結果が複数件となる場合は、apps.runner.models.MultipleObjectsReturned: get() returned more than one Stores -- it returned 2!
のようなエラーが発生します。
UPDATE or INSERT
Django1.7より追加されたupdate_or_create()
を使うと、テーブルに存在すればUPDATE、存在しなければINSERTを行えます。
QuerySet API reference - #django.db.models.query.QuerySet.update_or_create | Django documentation | Django
update_or_create()
でも、結果が複数件となる場合はapps.runner.models.MultipleObjectsReturned: get() returned more than one Stores -- it returned 2!
のエラーが発生します。
発行されるSQLの確認
# 実行前に全削除 Store.objects.all().delete() # INSERT Store.objects.update_or_create( name = 'update_or_create1', registered_users = 2, hoge = 'fuga1', ) #=> INSERT INTO "store" ("name", "registered_users", "hoge") VALUES ('update_or_create1', 2, 'fuga1') # UPDATE Store.objects.update_or_create( name = 'update_or_create1', registered_users = 2, ) #=> UPDATE "store" SET "name" = 'update_or_create1', "registered_users" = 2, "hoge" = 'fuga1' WHERE "store"."id" = 17
列の更新内容の指定
列の更新内容については、default
引数にて指定します。
# 実行前に全削除 Store.objects.all().delete() # データ登録 Store.objects.update_or_create( name = 'update_or_create1', hoge = 'fuga1', registered_users = 1, ) #=> INSERT INTO "store" ("name", "registered_users", "hoge") VALUES ('update_or_create1', 1, 'fuga1') # 登録結果: {'hoge': 'fuga1', 'registered_users': 1, 'id': 20, 'name': 'update_or_create1'} # defaultsで指定した列・値で更新する # hoge列の値を`fuga1`から`fuga2`へと更新する Store.objects.update_or_create( name = 'update_or_create1', hoge = 'fuga1', defaults={ 'hoge': 'fuga2' } ) #=> UPDATE "store" SET "name" = 'update_or_create1', "registered_users" = 1, "hoge" = 'fuga2' WHERE "store"."id" = 20 # 更新結果: {'hoge': 'fuga2', 'registered_users': 1, 'id': 20, 'name': 'update_or_create1'}
SELECT FOR UPDATE
SELECT FOR UPDATEするためには、
- トランザクションの中で実行
- トランザクション外だとエラーが発生
django.db.transaction.TransactionManagementError: select_for_update cannot be used outside of a transaction.
- トランザクション外だとエラーが発生
- SELECT FOR UPDATEを実装しているDBへ接続
- default以外への接続の場合、
with transaction.atomic(using='postgres'):
のようにして、トランザクションを使う接続を指定
- default以外への接続の場合、
を満たす必要があります。
なお、SQLiteなどのSELECT FOR UPDATEを実装していないDBの場合には、エラーが出ずにFOR UPDATE
なしのSQLが発行されます。
QuerySet API reference - #django.db.models.query.QuerySet.select_for_update | Django documentation | Django
# PostgreSQLの接続でトランザクションを使うように明示的に指定 with transaction.atomic(using='postgres'): Store.objects.select_for_update().filter(name='st1') #=> SELECT * FROM "store" WHERE "store"."name" = 'st1' LIMIT 21 FOR UPDATE
ソースコード
GitHubに追加してあります。今回のメインはinsert_update.py
となります。
その他
SQLのダンプ
単一DBであれば、django.db.connection
から発行されたSQLを取得します。
今回は複数DBだったため、django.db.connections
から発行されたSQLを取得しました。このあたりで使っています。
FAQ: Databases and models - #how-can-i-see-the-raw-sql-queries-django-is-running | Django documentation | Django
オブジェクトがiterableかどうか
以下を参考に、isinstance(model, collections.Iterable)
で判定しました。このあたりで使っています。
In Python, how do I determine if an object is iterable? - Stack Overflow