OneToOneのモデルの新規作成、更新処理を書いてたらへんなバグ踏んだのでメモ。
前提
class Parent(models.Model):
pass
class Child(models.Model):
parent = models.OneToOneField(Parent, on_delete=models.CASCADE)
こんな感じの1対1のモデルがあり、すでにChildがある場合は更新、ない場合は新規作成したい。
結論
parent = Parent.objects.get(id=1)
child = getattr(parent, 'child', None) or Child(parent=parent)
# ...中略...
child.save()
こう書く。
蛇足
やりたいことを愚直に書くとこんな感じ。
parent = Parent.objects.get(id=1)
if Child.objects.filter(parent=parent).exists():
child = parent.child
else:
child = Child(parent=parent)
# ...中略...
child.save()
リレーション先の存在確認は hasattr
でできるので、 if文はもう少し短くなる。
if hasattr(parent, 'child'):
child = parent.child
else:
child = Child(parent=parent)
三項演算子で書くと1行にまとまる。
child = parent.child if hasattr(parent, 'child') else Child(parent=parent)
失敗コード
getattr使えば更に短くなると思って以下のように書いた。
child = getattr(parent, 'child', Child(parent=parent))
新規作成時には問題ないが、更新時にsaveすると例外が出た。
django.db.utils.IntegrityError: UNIQUE constraint failed: my_app_child.parent_id
getattrの第3引数(デフォルト値)の評価は判定前に行われるらしく、 Child(parent=parent)
の時点で
OneToOneField
なので parent.child
がすでに設定されているものから新しく作られたものに差し替えられてしまうのが原因っぽい。
# すでに child.id=1 のオブジェクトが紐付け済み
>>> parent.child
<Child: Child object (1)>
# 戻り値は `parent.child` だが、第3引数の評価が先に行われるので `parent.child` が新しく作ったオブジェクトになる
>>> getattr(parent, 'child', Child(parent=parent))
<Child: Child object (None)>
>>> parent.child
<Child: Child object (None)>
Child(parent=parent)
はリレーションがない場合だけ実行してほしいので、結論の通り短絡評価使えばシンプルに書ける。
ただし、getattr使う場合はデフォルト値の指定がなくてリレーション先のオブジェクトがないと RelatedObjectDoesNotExist
の例外が飛ぶ。
>>> parent2 = Parent.objects.create()
<Parent: Parent object (2)>
>>> hasattr(parent2, 'child')
False
# デフォルト値がないと例外吐く
>>> getattr(parent2, 'child')
Traceback (most recent call last):
File "<console>", line 1, in <module>
File ".../django/db/models/fields/related_descriptors.py", line 421, in __get__
raise self.RelatedObjectDoesNotExist(
my_app.models.Parent.child.RelatedObjectDoesNotExist: Parent has no child.
# デフォルト値渡してあげると大丈夫
>>> getattr(parent2, 'child', None)