LoginSignup
0
0

More than 1 year has passed since last update.

getattrを使った新規作成 or 更新処理

Posted at

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)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0