DjangoのModel.save()の動作と、MySQLのデッドロックエラーについて

## はじめに

DjangoのModel.save()を使ってレコード保存をしようとしたところ、MySQLのデッドロックエラーが発生したことがありましたので、その理由を調べました。

## 前提
Django1.8.4, MySQL5.5 InnoDB、トランザクション分離レベルは REPEATABLE-READ です。

## DjangoのModel.save()について

https://docs.djangoproject.com/en/1.8/ref/models/instances/#how-django-knows-to-update-vs-insert を引用します。

> You may have noticed Django database objects use the same save() method for creating and changing objects. Django abstracts the need to use INSERT or UPDATE SQL statements. Specifically, when you call save(), Django follows this algorithm:

> - If the object’s primary key attribute is set to a value that evaluates to True (i.e., a value other than None or the empty string), Django executes an UPDATE.
> - If the object’s primary key attribute is not set or if the UPDATE didn’t update anything, Django executes an INSERT.

Djangoはsave()を使う際に、利用者にINSERTかUPDATEを意識させない作りにしているようです。インスタンスのprimary keyがTrueと評価できる場合、UPDATEを発行します。このUPDATEで変更行が0だった場合には改めてINSERTを発行するようです。

## デッドロックが起きる例

以下のようなmodelを用意します。DerivedはBaseを継承したモデルです。

```models.py
from django.db import models

class Base(models.Model):
    a = models.IntegerField()

class Derived(Base):
    b = models.IntegerField()
```

Derivedを `Model.save()` を使ってレコードを生成しようとするとどうなるでしょうか。答えを言うと、以下のような簡単なレコード生成処理を高頻度並列で行った場合、MySQLのデッドロックエラー `ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction` が発生します。

```create_derived.py
d = Derived(a=1, b=2)
d.save()

=> ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction !!!!
```

## 理由

まず `model.py` のテーブル定義を確認します。以下のようなCREATE TABLE文になっています。Derivedモデルは `base_ptr_id` を継承元情報として保持し、なおかつPRIMARY KEYになっています。

```sql
CREATE TABLE `ham_base` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) NOT NULL,
  PRIMARY KEY (`id`)
)

CREATE TABLE `ham_derived` (
  `base_ptr_id` int(11) NOT NULL,
  `b` int(11) NOT NULL,
  PRIMARY KEY (`base_ptr_id`),
  CONSTRAINT `ham_derived_base_ptr_id_12f18f813c81ff4f_fk_ham_base_id` FOREIGN KEY (`base_ptr_id`) REFERENCES `ham_base` (`id`)
)
```

次に `create_derived.py` の `save()` 時に発行されるクエリを確認します。

```sql
INSERT INTO `ham_base` (`a`) VALUES (1);
UPDATE `ham_derived` SET `b` = 2 WHERE `ham_derived`.`base_ptr_id` = 1 ; args=(2, 1)
INSERT INTO `ham_derived` (`base_ptr_id`, `b`) VALUES (1, 2); args=(1, 2)
```

- 最初にBaseのレコードを生成しています。これはPRIMARY KEYがFalseなのでINSERTになっています。
- 次にDerivedに対してUPDATEが発行されています。これは `base_ptr_id` が最初のINSERTによって確定し、PRIMARY KEYがTrueと評価できたからです。ただし、この時点でDerivedのレコードはないので UPDATEは必ず空振りします。
- 最後に、UPDATEが空振ったのでINSERTでDerivedレコードを作成しています。

2番目の空振りするUPDATEがクセモノで、トランザクション分離レベルREPEATABLE-READの場合にギャップロックを獲得するようです。タイミング次第で以下のような状況になり、デッドロックが発生します。

| Transaction1                             | Transaction2                      | コメント |
|:-----------------------------------------|:----------------------------------|:---------------------------|
| INSERT INTO `ham_base` (`a`) VALUES (1);| |  |
| |INSERT INTO `ham_base` (`a`) VALUES (1); |  |
|UPDATE `ham_derived` SET `b` = 2 WHERE `ham_derived`.`base_ptr_id` = 1;| | ロックA獲得 |
| | UPDATE `ham_derived` SET `b` = 2 WHERE `ham_derived`.`base_ptr_id` = 2;| ロックB獲得|
| INSERT INTO `ham_derived` (`base_ptr_id`, `b`) VALUES (1, 2)    | | ロックB待ち |
| | INSERT INTO `ham_derived` (`base_ptr_id`, `b`) VALUES (2, 2)  | **ロックA待ちになるので即座にデッドロック検出** |


## 回避方法

INSERTであることを明示すればよいです。以下のような方法で回避できます。

- `Model.objects.create()` を使う
- `Model.save()` の `force_insert` オプションを利用する。
- Modelのmetaオプションで [select_on_save=True](https://docs.djangoproject.com/en/1.8/ref/models/options/#django.db.models.Options.select_on_save) を指定する。

    > Determines if Django will use the pre-1.6 django.db.models.Model.save() algorithm. The old algorithm uses SELECT to determine if there is an existing row to be updated. The new algorithm tries an UPDATE directly.