今川館

都内勤務の地味OLです

django-pyodbcでselect_for_updateは機能しないので要注意 (2015/4時点)

django-pyodbcはselect_for_updateメソッドを呼び出しても普通のSELECT文を発行する

Django1.4以上でモデルマネージャやクエリセットにselect_for_updateというメソッドが使えるけれども、django-pyodbc(=SQLServer)だとこれが使えないので注意が必要。

DjangoSQLServerを使うときの背景事情

select_for_updateを使っても普通のSELECT文しか生成してくれない

わかりにくいのが、select_for_updateメソッドを呼んでも単なるSELECT文を発行してしまう点。
(NotImplementedErrorなど実行時エラーを出してくれるわけではない)

django-pyodbcを使ってSQLServerをデータベースバックエンドに指定して以下のプログラムを動かすとそれが判る。

# -*- coding:utf-8 -*-
import django.db.models as models


class Foo(models.Model):
    class Meta:
        app_label = 'hello'
        db_table = 'foo'

    name = models.CharField(max_length=32)
    age = models.IntegerField(null=True)


print(Foo.objects.select_for_update().filter(pk=1).query)

結果

SELECT [foo].[id], [foo].[name], [foo].[age] FROM [foo] WHERE [foo].[id] = 1

実際どう処理しているのか?

django-pyodbcのソースコードを見るとテストコードでいかにもselect_for_updateをサポートしているように見えるが、skipUnlessDBFeatureデコレータが付いている点が落とし穴で、実はこのテストはスキップされるようだ。

https://github.com/lionheart/django-pyodbc/blob/ba2daee41d222a76b4459733a5f6098f0098ab6b/tests/django15/select_for_update/tests.py#L88-L95

@skipUnlessDBFeature('has_select_for_update')
def test_for_update_sql_generated(self):
    """
    Test that the backend's FOR UPDATE variant appears in
    generated SQL when select_for_update is invoked.
    """
    list(Person.objects.all().select_for_update())
    self.assertTrue(self.has_for_update_sql(connection))

実際にconnectionのfeaturesを調べてみると、Falseを返す。

from django.db import connection
print(connection.features.has_select_for_update)  # => False

SQLServerのSELECT FOR UPDATEに対応する構文

SQLServerでSELECT FOR UPDATEをやろうとすると WITH (UPDLOCK) というようにヒントを加えるらしい(以下URL)。

テーブル ヒント (Transact-SQL)

例:

SELECT * FROM ACCOUNT WITH (UPDLOCK) WHERE ID = 100;

これがOracleMySQLだと末尾に FOR UPDATE と付けるだけ。

SELECT * FROM ACCOUNT WHERE ID = 100 FOR UPDATE;

LIMIT/OFFSETのやり方もSQLServerだけ全然違って、ORマッパーでうまいこと構文通りにSELECT文をジェネレートするのが難しいから放置されているんだろう。