SQLのINSERT, UPDATE文を、DjangoのQuerySet APIで書いてみた

前回はSELECT文まわりを試したので、今回はINSERT, UPDATEまわりを試してみます。

なお、ベースのアプリは前回のものを引き継ぎます。

 
目次

 

環境

 

複数データベース環境の用意

今回は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を使っています。

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

Djangoget_or_create()を使うと、テーブルに存在すればSELECT、存在しなければINSERTを行えます。

 

発行される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'):のようにして、トランザクションを使う接続を指定

を満たす必要があります。

なお、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