Django ORM究極ガイド:update_or_create()と賢い代替手段

2025-05-16

update_or_create()とは?

このメソッドは、以下の2つの主要な処理を1つのアトミックな操作として実行します。

  1. 検索(Search): 与えられた条件に一致するオブジェクトをデータベースから検索します。
  2. 更新または作成(Update or Create):
    • もし一致するオブジェクトが見つかった場合、指定された値でそのオブジェクトを更新します。
    • もし一致するオブジェクトが見つからなかった場合、新しいオブジェクトを作成します。

使い方

基本的な構文は以下のようになります。

obj, created = MyModel.objects.update_or_create(
    defaults={
        'field_to_update_1': 'value1',
        'field_to_update_2': 'value2',
    },
    lookup_field_1='lookup_value_1',
    lookup_field_2='lookup_value_2',
)
  • defaults:
    • これは辞書であり、キーと値のペアで構成されます。
    • オブジェクトが作成される場合、この辞書内のすべてのキーと値が新しいオブジェクトに設定されます。
    • オブジェクトが更新される場合、この辞書内のすべてのキーと値が既存のオブジェクトに設定されます。

戻り値

update_or_create()は、2つの値を持つタプルを返します。

  1. obj: 作成または更新されたモデルインスタンス。
  2. created: 論理値(ブール値)。
    • Trueの場合、新しいオブジェクトが作成されたことを意味します。
    • Falseの場合、既存のオブジェクトが更新されたことを意味します。

具体例

例えば、Productというモデルがあり、namepriceというフィールドがあるとします。

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100, unique=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return f"{self.name} (${self.price})"

例1:既存の製品を更新する

もしname='りんご'の製品が既に存在し、その価格を更新したい場合:

product, created = Product.objects.update_or_create(
    name='りんご',  # 検索条件
    defaults={'price': 150.00} # 更新または作成時の値
)

print(product)   # 出力例: りんご ($150.00)
print(created)   # 出力例: False (更新されたため)

この場合、name'りんご'Productオブジェクトがデータベースに見つかるので、そのprice150.00に更新されます。createdFalseになります。

例2:新しい製品を作成する

もしname='みかん'の製品がまだ存在しない場合:

product, created = Product.objects.update_or_create(
    name='みかん',  # 検索条件
    defaults={'price': 120.00} # 更新または作成時の値
)

print(product)   # 出力例: みかん ($120.00)
print(created)   # 出力例: True (新しく作成されたため)

この場合、name'みかん'Productオブジェクトがデータベースに見つからないので、新しいProductオブジェクトが作成され、name'みかん'price120.00に設定されます。createdTrueになります。

update_or_create()は非常に便利ですが、注意すべき点があります。それは**競合状態(Race Condition)**の可能性です。

update_or_create()は、内部的にまずget()を試み、見つからなければcreate()を実行するというロジックを持っています。もし複数のプロセスやリクエストがほぼ同時に、同じ条件でupdate_or_create()を呼び出した場合、以下のような状況が発生する可能性があります。

  1. プロセスAがオブジェクトをget()しようとするが、見つからない。
  2. プロセスBもオブジェクトをget()しようとするが、見つからない。
  3. プロセスAがオブジェクトをcreate()する。
  4. プロセスBもオブジェクトをcreate()しようとするが、この時点でユニーク制約(例: nameフィールドがunique=True)に違反するため、IntegrityErrorが発生する可能性があります。

この問題を緩和するために、Djangoはupdate_or_create()内部でIntegrityErrorを捕捉し、再試行するロジックを実装していますが、ユニーク制約がデータベースレベルで強制されていない場合は、複数の行が挿入される可能性があります。

  • より厳密な制御が必要な場合は、トランザクションやselect_for_update()などを使用してロックをかけることを検討する。
  • モデルのフィールドにunique=Trueを設定するなど、データベースレベルでユニーク制約を適切に設定する。


IntegrityError (ユニーク制約違反)

エラーの原因
これはupdate_or_create()で最もよく見られるエラーの一つです。特に、モデルのフィールドにunique=Trueunique_togetherが設定されている場合に発生しやすいです。

update_or_create()は、まず検索条件(キーワード引数)に基づいてオブジェクトを見つけようとします。見つからなければ新規作成を試みます。この新規作成の際に、データベースに既に存在するユニークな値を持つオブジェクトを作成しようとすると、IntegrityErrorが発生します。


nameフィールドにunique=Trueが設定されているProductモデルがある場合。

# 1. 最初に'ペン'を作成
Product.objects.create(name='ペン', price=100)

# 2. 別のプロセス/リクエストがほぼ同時に'ペン'を作成しようとする
#    または、検索条件に合致しないがdefaultsにユニークな値が含まれる場合
try:
    product, created = Product.objects.update_or_create(
        defaults={'price': 120}, # priceだけ更新しようとするが、nameは検索条件にない
        name='ペン' # ここで検索条件として使われる
    )
    print(f"Product: {product.name}, Created: {created}")
except IntegrityError as e:
    print(f"IntegrityErrorが発生しました: {e}")

この例では、name='ペン'が存在するので更新されます。しかし、もし検索条件にnameを含めず、defaultsにユニークな値を含めた場合、また別の競合状態が発生すると、IntegrityErrorになる可能性があります。

トラブルシューティング

  • 競合状態の考慮
    複数のプロセスや同時リクエストが同じレコードに対してupdate_or_create()を実行する可能性がある場合、競合状態によってIntegrityErrorが発生することがあります。 Djangoは内部的にこのエラーを捕捉して再試行するロジックを持っていますが、完全に防ぐことはできません。 非常に高い同時実行性が必要な場合や、厳密なアトミック性が必要な場合は、以下の対策を検討してください。

    • データベーストランザクション
      transaction.atomic()を使用して、操作全体をアトミックにします。
    • select_for_update()
      行レベルのロックをかけて、他のトランザクションがその行を変更できないようにします。ただし、デッドロックのリスクもあるため慎重に。
    • カスタムロジック
      try-except IntegrityErrorブロックで手動で処理し、必要に応じてリトライロジックを実装します。
  • ユニーク制約の確認
    モデルに設定されているunique=Trueunique_togetherを再確認してください。意図しないフィールドにユニーク制約が設定されていないか、または、複数のフィールドの組み合わせでユニーク制約がある場合に、それらのフィールドが適切に検索条件に含まれているかを確認します。

  • 検索条件(キーワード引数)の確認
    update_or_create()のキーワード引数として渡す値は、オブジェクトを一意に特定するための「検索条件」として使用されます。これらの検索条件が、データベース内の既存のオブジェクトを適切に特定できることを確認してください。 もしdefaultsに入れたい値が、同時に検索条件にもなる場合は、defaultsから除外し、キーワード引数として直接渡す必要があります。

    悪い例

    # nameで検索したいが、defaultsにもnameを入れてしまうと意図しない挙動になる可能性
    obj, created = MyModel.objects.update_or_create(
        defaults={'name': '新しい名前', 'value': '新しい値'},
        name='古い名前' # ここで検索
    )
    

    この場合、name='古い名前'のオブジェクトが見つからなければ、name='新しい名前'で新しいオブジェクトが作成されます。しかし、name='古い名前'のオブジェクトが見つかった場合、そのオブジェクトのname'新しい名前'に更新されることになります。nameがユニークな場合、他の場所で'新しい名前'が使われているとエラーになります。

    良い例
    nameを検索条件として使い、valueを更新または作成したい場合。

    obj, created = MyModel.objects.update_or_create(
        name='対象の名前',  # 検索条件
        defaults={'value': '新しい値'} # 更新または作成時の値
    )
    

TypeError: __init__() got an unexpected keyword argument

エラーの原因
update_or_create()の引数として、モデルに存在しないフィールド名や、予期しないキーワード引数を渡してしまった場合に発生します。これは特に、タイポや、異なるモデルのフィールド名を誤って使用した場合に見られます。


Productモデルにstockというフィールドがないのに、defaultsや検索条件にstockを渡した場合。

# Productモデルに'stock'フィールドがないと仮定
# class Product(models.Model):
#     name = models.CharField(max_length=100, unique=True)
#     price = models.DecimalField(max_digits=10, decimal_places=2)

try:
    product, created = Product.objects.update_or_create(
        name='鉛筆',
        defaults={'price': 50.00, 'stock': 100} # 'stock'が問題
    )
except TypeError as e:
    print(f"TypeErrorが発生しました: {e}") # 例: __init__() got an unexpected keyword argument 'stock'

トラブルシューティング

  • defaults辞書の内容の確認
    defaults辞書内のキーも、モデルのフィールド名と一致している必要があります。
  • フィールド名の確認
    モデル定義を確認し、渡しているキーワード引数がすべて有効なフィールド名であることを確認してください。

ValueError: Cannot assign "some_value": "MyModel.foreign_key_field" must be a "RelatedModel" instance.

エラーの原因
外部キー(ForeignKey)やOneToOneFieldなどのリレーションフィールドに対して、関連オブジェクトのインスタンスではなく、生のID値などを直接割り当てようとした場合に発生します。


OrderモデルがCustomerモデルへの外部キーcustomerを持っている場合。

```python

class Order(models.Model):

customer = models.ForeignKey(Customer, on_delete=models.CASCADE)

order_date = models.DateTimeField(auto_now_add=True)

customer_id = 1 try: order, created = Order.objects.update_or_create( customer=customer_id, # ここでIDを渡してしまうとエラー defaults={'order_date': datetime.now()} ) except ValueError as e: print(f"ValueErrorが発生しました: {e}")


**トラブルシューティング:**

  * **関連オブジェクトの取得:**
    外部キーやOneToOneFieldに値を設定する際は、そのフィールドが参照するモデルの**インスタンス**を渡す必要があります。IDだけを渡すのではなく、事前にそのIDを持つオブジェクトを取得してから渡してください。

    ```python
    from datetime import datetime
    from myapp.models import Customer, Order

    # 顧客オブジェクトを取得
    try:
        customer = Customer.objects.get(id=1)
        order, created = Order.objects.update_or_create(
            customer=customer, # 関連オブジェクトのインスタンスを渡す
            defaults={'order_date': datetime.now()}
        )
        print(f"Order created/updated for customer: {order.customer.name}, Created: {created}")
    except Customer.DoesNotExist:
        print("指定された顧客が見つかりません。")
    except ValueError as e:
        print(f"ValueErrorが発生しました: {e}")
    ```

### 4\. `update_or_create` が常に新しいオブジェクトを作成してしまう

**エラーの原因:**
`update_or_create()`の検索条件(キーワード引数)が、既存のどのオブジェクトとも一致しない場合に発生します。これは、検索条件が不十分であるか、または意図せず常に新しいユニークな検索条件を生成している場合に起こります。

**例:**
本来更新したいオブジェクトのキーが異なっている、あるいは検索条件にプライマリキーを含めないで、ユニークなフィールドも指定していない場合。

```python
# nameはunique=Trueだが、nameを検索条件に含めていない場合
# class Product(models.Model):
#     name = models.CharField(max_length=100, unique=True)
#     price = models.DecimalField(max_digits=10, decimal_places=2)

# Product.objects.create(name='Tシャツ', price=2000)

product_name = 'Tシャツ' # この名前のTシャツを更新したい
new_price = 2500

# nameを検索条件に含め忘れた、または別のフィールドで検索しようとした場合
# これだと常に新しいオブジェクトを作成しようとする(そしてユニーク制約違反になる)
# product, created = Product.objects.update_or_create(
#     defaults={'name': product_name, 'price': new_price}
# )

# 正しい例
product, created = Product.objects.update_or_create(
    name=product_name, # これが検索条件
    defaults={'price': new_price}
)

print(f"Product: {product.name}, Created: {created}")

トラブルシューティング

  • createdフラグの利用
    update_or_create()の戻り値であるcreatedフラグを常にチェックし、意図せずTrueになっている場合は、検索条件を見直す必要があります。
  • 検索条件の厳密性の確認
    update_or_create()のキーワード引数が、既存のオブジェクトを一意に特定できる十分な条件になっているか確認してください。特に、ユニーク制約を持つフィールド(プライマリキーやunique=Trueが設定されたフィールド)を検索条件に含めることが重要です。

DoesNotExist や MultipleObjectsReturned が発生する (get_or_create を混同している場合)

update_or_create()自体は通常これらの例外を直接発生させませんが、もしupdate_or_create()を使うべき場面でget_or_create()を使用していたり、その逆だったりする場合に混乱が生じることがあります。

  • get_or_create()の場合
    • DoesNotExist: 検索条件に一致するオブジェクトが見つからない場合。
    • MultipleObjectsReturned: 検索条件に一致するオブジェクトが複数見つかった場合(通常、ユニーク制約に違反しているか、検索条件が不十分)。

update_or_create()は、これらの内部的なget()の失敗を処理し、create()update()を行うため、通常はこれらの例外を直接発生させません。

トラブルシューティング

  • メソッドの選択
    • update_or_create(): オブジェクトが存在すれば更新し、存在しなければ作成したい場合。
    • get_or_create(): オブジェクトが存在すれば取得し、存在しなければ作成したい場合。通常、作成時にだけ特定の値(defaultsとは別の)を設定したい場合に検討されます。
    • get() / create() / save() の組み合わせ: より細かい制御が必要な場合。

update_or_create()を効果的に利用するためには、以下の点を常に意識することが重要です。

  • 戻り値の確認
    objcreatedフラグを常にチェックし、期待通りの挙動をしているか確認する。
  • 競合状態への対応
    高い同時実行性が予想される場合は、データベースレベルでのロックやトランザクションを検討する。
  • ユニーク制約の理解と設定
    モデルのユニーク制約がupdate_or_create()の挙動にどう影響するかを理解し、適切に設定する。
  • 検索条件(キーワード引数)とdefaultsの役割の区別
    どの引数が検索に使われ、どの引数が新規作成/更新時の値に使われるかを明確にする。


モデルの準備

まず、例で使用するDjangoモデルを定義します。シンプルな商品管理システムを想定します。

# myapp/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.name

class Product(models.Model):
    name = models.CharField(max_length=200, unique=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField(default=0)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    is_active = models.BooleanField(default=True)
    last_updated = models.DateTimeField(auto_now=True) # 更新時に自動で日時を記録

    def __str__(self):
        return f"{self.name} (${self.price})"

このモデル定義後、makemigrationsmigrateを実行してデータベースに適用してください。

update_or_create() の基本的な使い方

update_or_create()は、キーワード引数で検索条件を指定し、defaults辞書で作成または更新される値を指定します。

パターン1: 新規作成(オブジェクトが存在しない場合)

# Pythonシェル (python manage.py shell) で実行
from myapp.models import Product, Category

# カテゴリを作成しておく(もし存在しない場合)
electronics, created = Category.objects.update_or_create(
    name='Electronics',
    defaults={'description': 'Electronic devices and gadgets'}
)
print(f"Category 'Electronics' created: {created}")

# 'Laptop'という商品が存在しない場合
laptop, created = Product.objects.update_or_create(
    name='Laptop', # 検索条件
    defaults={      # 新規作成時に適用される値
        'price': 1200.00,
        'stock': 50,
        'category': electronics
    }
)
print(f"Product 'Laptop' created: {created}") # Trueが出力される

# データベースには新しいProductレコードが追加される
# Product.objects.get(name='Laptop') で確認可能

説明
name='Laptop'という商品がデータベースに存在しないため、defaultsで指定された値(price, stock, category)を持つ新しいProductオブジェクトが作成されます。created変数にはTrueが返されます。

パターン2: 更新(オブジェクトが既に存在する場合)

from myapp.models import Product, Category

# 前の例で作成した'Laptop'が存在すると仮定
# 'Laptop'の価格と在庫を更新したい場合
laptop, created = Product.objects.update_or_create(
    name='Laptop', # 検索条件
    defaults={      # 既存のオブジェクトに適用される更新値
        'price': 1150.00, # 価格を更新
        'stock': 45       # 在庫を更新
        # categoryはdefaultsに含めない (変更しないため)
    }
)
print(f"Product 'Laptop' created: {created}") # Falseが出力される

# データベースでは既存の'Laptop'レコードが更新される
# Product.objects.get(name='Laptop').price で更新後の価格を確認

説明
name='Laptop'という商品がデータベースに既に存在するため、そのオブジェクトのpricestockdefaultsで指定された値に更新されます。created変数にはFalseが返されます。

複雑な検索条件とdefaultsの利用

update_or_create()は複数の検索条件を組み合わせることも、defaultsに多くのフィールドを含めることも可能です。

パターン3: 複数の検索条件とリレーションフィールド

特定のカテゴリに属する、特定の名前の製品を扱いたい場合。

from myapp.models import Product, Category

# まずカテゴリが確実に存在することを確認
software, created_cat = Category.objects.update_or_create(
    name='Software',
    defaults={'description': 'Digital applications and programs'}
)
print(f"Category 'Software' created: {created_cat}")

# カテゴリが'Software'で名前が'IDE'の製品を検索/更新/作成
ide_software, created_prod = Product.objects.update_or_create(
    name='Integrated Development Environment (IDE)', # 検索条件1
    category=software,                               # 検索条件2 (リレーションフィールド)
    defaults={
        'price': 299.99,
        'stock': 100,
        'is_active': True
    }
)
print(f"Product 'IDE' created: {created_prod}")

# もし'IDE'が既に存在し、かつcategoryが'Software'であれば更新
# そうでなければ新規作成

説明
この例では、namecategoryの両方を検索条件として使用しています。categoryCategoryモデルのインスタンスを直接渡す必要があります(IDを渡すのはエラーになります)。これにより、複数の条件でオブジェクトを一意に特定し、存在すれば更新、なければ新規作成します。

defaultsに含めないフィールドの扱い

defaults辞書に含めないフィールドは、新規作成時には設定されず、更新時には変更されません。これは意図的な挙動です。

パターン4: 特定のフィールドは変更しない

from myapp.models import Product, Category

# 'Laptop'が存在すると仮定。そのstockだけを更新し、priceは変更しない
laptop, created = Product.objects.update_or_create(
    name='Laptop',
    defaults={
        'stock': 40 # priceはdefaultsに含めないため、変更されない
    }
)
print(f"Product 'Laptop' created: {created}") # False

# 変更前と変更後のpriceを比較して確認
# original_price = Product.objects.get(name='Laptop').price
# print(f"Original Price: {original_price}")
# product_after_update = Product.objects.get(name='Laptop')
# print(f"Updated Price: {product_after_update.price}") # original_priceと同じはず

説明
defaultspriceを含めなかったため、Laptoppriceフィールドは既存の値から変更されません。stockのみが更新されます。

auto_now / auto_now_add フィールドとの連携

auto_now=Trueが設定されたフィールドは、オブジェクトが保存されるたびに自動的に更新されます。update_or_create()でもこの挙動は維持されます。

パターン5: auto_now フィールドの自動更新

import time
from myapp.models import Product, Category

# 'Laptop'が存在すると仮定
# 少し待ってから更新し、last_updatedが自動更新されることを確認
# original_last_updated = Product.objects.get(name='Laptop').last_updated
# print(f"Original last_updated: {original_last_updated}")

time.sleep(1) # 1秒待機

laptop, created = Product.objects.update_or_create(
    name='Laptop',
    defaults={'stock': 38} # 他のフィールドはdefaultsに含めない
)
print(f"Product 'Laptop' created: {created}")

# updated_last_updated = Product.objects.get(name='Laptop').last_updated
# print(f"Updated last_updated: {updated_last_updated}")
# updated_last_updated は original_last_updated より新しいはず

説明
last_updatedフィールドにはauto_now=Trueが設定されているため、update_or_create()が既存のLaptopオブジェクトを更新した際に、last_updatedが現在のタイムスタンプに自動的に更新されます。新規作成時にはauto_now_add=Trueも同様に自動設定されます。

update_or_create()はアトミックな操作を目指していますが、厳密なユニーク制約がない場合や、非常に高い同時実行環境では競合状態による問題が発生する可能性があります。

パターン6: IntegrityError(ユニーク制約違反)への対処

Djangoは内部的にIntegrityErrorを処理し、再試行するロジックを持っていますが、理解しておくことは重要です。

from myapp.models import Product
from django.db import IntegrityError

try:
    # 意図的に同じ名前で同時に作成しようとすると(擬似的に)
    # 実際には複数プロセス/スレッドからの同時実行で発生しやすい
    product1, created1 = Product.objects.update_or_create(
        name='Mouse',
        defaults={'price': 25.00, 'stock': 200}
    )
    print(f"Product 'Mouse' (1) created: {created1}")

    product2, created2 = Product.objects.update_or_create(
        name='Mouse',
        defaults={'price': 26.00, 'stock': 190} # わずかに異なる値
    )
    print(f"Product 'Mouse' (2) created: {created2}")

except IntegrityError as e:
    print(f"IntegrityErrorが発生しました: {e}")
    # このエラーは、Djangoが内部的に処理しようとするため、
    # 通常はユーザーコードで直接捕捉する必要はあまりないが、
    # ユニーク制約がデータベースレベルで適用されていることを確認する。
    # ここでエラーが出る場合は、検索条件とユニーク制約の関係を見直す必要がある。

説明
nameフィールドにunique=Trueが設定されているため、update_or_create()nameをキーとしてオブジェクトを見つけようとします。 もし複数のリクエストが同時にname='Mouse'のオブジェクトを作成しようとした場合、最初の呼び出しがオブジェクトを作成し、二番目の呼び出しはそれを更新します。 しかし、もしデータベースのトランザクション分離レベルが低かったり、他の複雑な要因が絡んだりすると、二番目の呼び出しが既存のオブジェクトを見つけられずに新規作成を試み、IntegrityErrorを引き起こす可能性があります。 ほとんどの場合、Djangoのunique=Trueupdate_or_create()の組み合わせは安全ですが、高負荷環境ではselect_for_update()などによる明示的なロックの検討が必要になることもあります。

update_or_create()は、Djangoアプリケーションで以下のような場面で非常に役立ちます。

  • フォームからのデータ処理
    フォームで送信されたデータに基づいてレコードを作成または更新する場合。
  • データインポート/同期
    外部システムからデータをインポートする際に、既存のレコードは更新し、新しいレコードは追加したい場合。
  • 冪等性(Idempotency)の実現
    同じ操作を何度実行しても同じ結果が得られるようにしたい場合。


get_or_create() + save()

これはupdate_or_create()の最も一般的な代替方法であり、多くの場合、update_or_create()が内部的に行っている処理に近いものです。

方法

  1. get_or_create()を使ってオブジェクトを取得または作成します。
  2. get_or_create()の戻り値であるcreatedフラグを見て、オブジェクトが新規作成されたか、既存のものが取得されたかを判断します。
  3. もしオブジェクトが既存のもので、かつ更新が必要な場合は、フィールドを更新し、save()メソッドを呼び出します。

コード例

from myapp.models import Product, Category
from django.db import IntegrityError # 競合状態対策

# カテゴリを作成しておく
electronics, _ = Category.objects.get_or_create(name='Electronics')

product_name = 'Smartwatch'
new_price = 300.00
new_stock = 70

try:
    product, created = Product.objects.get_or_create(
        name=product_name,
        defaults={ # 新規作成時のみ適用される値
            'price': new_price,
            'stock': new_stock,
            'category': electronics
        }
    )

    if not created:
        # 既存のオブジェクトが見つかった場合
        # ここで更新したいフィールドを設定
        if product.price != new_price:
            product.price = new_price
        if product.stock != new_stock:
            product.stock = new_stock
        # 他のフィールドも必要に応じて更新

        # 変更があった場合のみ保存
        if product.has_changed: # これはカスタムメソッドが必要な場合がある (例: django-model-utils)
            product.save()
        else:
            # 変更がなければ保存は不要
            pass
        print(f"Product '{product_name}' updated.")
    else:
        print(f"Product '{product_name}' created.")

except IntegrityError:
    # 競合状態が発生した場合(非常にまれだが、複数プロセスが同時に作成を試みた場合など)
    # 通常、get_or_createは内部でIntegrityErrorをハンドルするが、
    # より複雑なシナリオでは手動で捕捉することもある
    print(f"IntegrityError: Failed to create or get {product_name}.")
    # ロギングや再試行ロジックなどを実装する

メリット

  • デフォルト値の挙動
    defaults辞書は新規作成時のみに適用されます。既存のオブジェクトが見つかった場合、defaults内の値は無視されます。これは、update_or_create()defaults内の値を常に適用するのとは異なる挙動です。
  • 制御の細かさ
    createdフラグに基づいて、新規作成時と更新時で異なるロジックを実行できます。例えば、新規作成時にのみ特定の通知を送ったり、更新時にのみログを記録したりできます。

デメリット

  • 追加の保存処理
    更新が必要な場合、明示的にsave()を呼び出す必要があります。これにより、データベースへのアクセスが1回増えることになります(SELECT + INSERT/SELECT + UPDATE)。update_or_create()は通常、1回のSELECTとその後のINSERTまたはUPDATEで完了します。
  • 冗長性
    常にif not created:ブロックで更新ロジックを記述する必要があり、コードが長くなりがちです。

利用ケース

  • defaultsの内容が新規作成時にのみ適用されればよい場合。
  • 新規作成時と更新時でロジックを明確に分けたい場合。

filter() + update() または create()

これは最も基本的な「取得して、なければ作成、あれば更新」のロジックを手動で構築する方法です。

方法

  1. filter()を使ってオブジェクトを検索します。
  2. オブジェクトが見つかった場合、update()メソッドを使って一括で更新します。
  3. オブジェクトが見つからなかった場合、create()メソッドを使って新規作成します。

コード例

from myapp.models import Product, Category

# カテゴリを作成しておく
electronics, _ = Category.objects.get_or_create(name='Electronics')

product_name = 'Wireless Earbuds'
update_values = {
    'price': 150.00,
    'stock': 120,
    'category': electronics
}

# 1. まず更新を試みる
updated_count = Product.objects.filter(name=product_name).update(**update_values)

if updated_count == 0:
    # 2. オブジェクトが見つからなかった場合、新規作成
    new_product = Product.objects.create(name=product_name, **update_values)
    print(f"Product '{product_name}' created.")
else:
    # 3. オブジェクトが更新された場合
    print(f"Product '{product_name}' updated.")

# 注意: この方法では、更新されたオブジェクトインスタンス(obj)は直接返されない。
# 必要であれば別途取得する必要がある。
# updated_product = Product.objects.get(name=product_name)

メリット

  • save()メソッドのバイパス
    update()はモデルのsave()メソッドを呼び出さないため、save()メソッド内に複雑なロジックがある場合、それをバイパスできます(メリットにもデメリットにもなり得る)。
  • 複数オブジェクトの更新
    filter().update()は、複数のオブジェクトを一度に更新できます。update_or_create()は常に1つのオブジェクトに対して機能します。
  • 直接的なSQL操作に近い
    update()は単一のSQLクエリ(UPDATE ... WHERE ...)として実行されるため、非常に効率的です。大量のフィールドを更新する場合に有利です。

デメリット

  • 複雑なロジック
    update_or_create()のように一つのメソッドで完結せず、if-elseブロックと複数のクエリを記述する必要があります。
  • オブジェクトインスタンスの欠如
    update()は更新された行数を返すだけで、更新されたオブジェクトのインスタンスを返しません。更新後のオブジェクトにアクセスしたい場合は、別途get()などのクエリを発行する必要があります。
  • 競合状態のリスク
    最も大きなデメリットは競合状態です。filter()でオブジェクトの有無を確認し、その後create()を実行するまでの間に、別のプロセスが同じオブジェクトを作成してしまう可能性があります。これにより、IntegrityErrorが発生したり、意図しない重複レコードが作成されたりする可能性があります(特にunique=Trueがない場合)。

利用ケース

  • 複数の既存レコードを一度に更新したいシナリオと、単一レコードの作成/更新を組み合わせたい場合(ただし、この場合はupdate_or_createの直接の代替とは少し異なる)。
  • save()メソッドのカスタムロジックを意図的にバイパスしたい場合。
  • パフォーマンスが非常に重視され、競合状態のリスクが低い、または許容される環境。

PostgreSQLのON CONFLICT DO UPDATE (UPSERT)

これはデータベース固有の機能を利用する方法で、特にPostgreSQLで強力です。DjangoのORMに直接的なメソッドとしては提供されていませんが、Raw SQLまたはdjango-postgres-extraのようなライブラリを使って利用できます。

方法 (Raw SQLの例)

from myapp.models import Product
from django.db import connection

product_name = 'Smart TV'
price = 800.00
stock = 30

with connection.cursor() as cursor:
    # ON CONFLICT (name) DO UPDATE SET price = EXCLUDED.price, stock = EXCLUDED.stock;
    # EXCLUDED.price は、コンフリクトを引き起こしたINSERT文のpriceの値を参照
    sql = """
    INSERT INTO myapp_product (name, price, stock, category_id, is_active, last_updated)
    VALUES (%s, %s, %s, %s, %s, NOW())
    ON CONFLICT (name) DO UPDATE SET
        price = EXCLUDED.price,
        stock = EXCLUDED.stock,
        is_active = EXCLUDED.is_active,
        last_updated = NOW()
    RETURNING id;
    """
    # category_id は ForeignKey なので、関連するCategoryオブジェクトのIDを渡す
    # ここでは仮にNone (NULL) または既存のカテゴリIDを使用
    category_id = None # または electronics.id など
    is_active = True
    
    cursor.execute(sql, [product_name, price, stock, category_id, is_active])
    product_id = cursor.fetchone()[0] # 挿入または更新された行のIDを取得

    # 必要であれば、IDを使ってオブジェクトを取得
    # product = Product.objects.get(id=product_id)
    print(f"Product '{product_name}' UPSERTED with ID: {product_id}")

メリット

  • 条件付き更新
    WHERE句を追加して、特定の条件が満たされた場合のみ更新を行うなど、柔軟なロジックを実装できます。
  • 高いパフォーマンス
    複数のクエリを発行するオーバーヘッドがないため、非常に高速です。特に大量のデータを処理する場合に有利です。
  • 真のアトミック性
    データベースレベルで単一のアトミックな操作として実行されるため、最も強力な競合状態対策となります。

デメリット

  • 学習曲線
    データベースのUPSERT構文に関する知識が必要です。
  • モデルのsave()メソッドのバイパス
    update()と同様に、モデルのsave()メソッドは呼び出されません。
  • Raw SQLの複雑さ
    SQLを直接書く必要があり、ORMの恩恵を受けられません。フィールド名やテーブル名の変更があった場合に、SQL文も手動で修正する必要があります。
  • データベース依存
    PostgreSQLやMySQL 8.0以降などの特定のデータベースシステムでしか利用できません。データベースを変更する可能性がある場合は移植性が低くなります。

利用ケース

  • 大規模なデータインポートや同期処理で、極限のパフォーマンスが求められる場合。
  • PostgreSQLなどの特定のデータベースを使用しており、そのデータベースのUPSERT機能のパフォーマンスとアトミック性を最大限に活用したい場合。

Django 4.1以降で追加されたbulk_create()update_conflicts引数を使用すると、一括でupdate_or_createのような操作を行うことができます。これは、複数のレコードを一度に処理したい場合に非常に強力です。

方法

  1. モデルのリストを作成します。
  2. bulk_create()update_conflicts=Trueunique_fields(競合を検出するフィールド)およびupdate_fields(更新するフィールド)を指定して呼び出します。

コード例

from myapp.models import Product, Category
from django.db import connection

# カテゴリの準備
electronics, _ = Category.objects.get_or_create(name='Electronics')
appliances, _ = Category.objects.get_or_create(name='Home Appliances')

products_to_process = [
    Product(name='Smart TV', price=850.00, stock=25, category=electronics), # 存在すれば更新
    Product(name='Blender', price=75.00, stock=50, category=appliances),    # 存在しなければ新規作成
    Product(name='Laptop', price=1100.00, stock=40, category=electronics),  # 既存のLaptopを更新
    Product(name='Coffee Maker', price=120.00, stock=60, category=appliances), # 新規作成
]

# 'name'フィールドでユニーク制約があることを前提
# update_fields には、衝突があった場合に更新したいフィールドを指定
# unique_fields には、衝突を検出するために使用するユニークなフィールドを指定
created_objects = Product.objects.bulk_create(
    products_to_process,
    update_conflicts=True,
    unique_fields=['name'], # ここで指定したフィールドの組み合わせで衝突を検出
    update_fields=['price', 'stock', 'category'] # 衝突時にこれらのフィールドを更新
)

for product in created_objects:
    # created_objects には、作成または更新されたオブジェクトが含まれる。
    # created_objects[i].pk は、更新されたオブジェクトのIDも含む。
    # ただし、どのオブジェクトが新規作成で、どれが更新されたかを直接判断するcreatedフラグは返されない。
    print(f"Processed product: {product.name}")

# 確認のため
# print(Product.objects.get(name='Laptop').price) # 1100.00 に更新されているはず

メリット

  • Django ORMとの統合
    ORMメソッドとして提供されているため、生のSQLを書く必要がありません。
  • アトミック性
    データベースレベルでのUPSERT操作に相当するため、競合状態に強いです。
  • 一括処理
    複数のレコードを一度に処理できるため、ループ内でupdate_or_create()を何度も呼び出すよりもはるかに効率的です(データベースへのクエリ回数が大幅に削減されます)。

デメリット

  • 柔軟性
    update_or_create()と比較すると、更新時に特定の条件を追加するなどの柔軟性では劣ります。
  • save()メソッドのバイパス
    bulk_create()(およびbulk_update)は、モデルのsave()メソッドを呼び出しません。
  • createdフラグの欠如
    update_or_create()のように、各オブジェクトが新規作成されたか更新されたかを直接示すcreatedフラグは返されません。必要であれば、別途ロジックで判断する必要があります。
  • Django 4.1以降の機能
    古いDjangoバージョンでは利用できません。
  • パフォーマンスが非常に重要で、単一のクエリで処理を完了したい場合。
代替方法メリットデメリット利用ケース
get_or_create() + save()新規作成時/更新時でロジックを細かく制御可能。コードが冗長、更新時に追加のDBクエリが必要。ロジックの複雑性が高く、作成時と更新時で異なる処理が必要な場合。
filter() + update() / create()直接的なSQL更新で高速、複数オブジェクトの一括更新。競合状態のリスク、オブジェクトインスタンスが直接返されない。パフォーマンス重視で、競合状態リスクが低いか許容される場合。save()メソッドのバイパスが必要な場合。
Raw SQL (UPSERT)真のアトミック性、最高のパフォーマンス。DB依存、SQLの複雑さ、save()メソッドのバイパス。PostgreSQLなど特定DBのUPSERT機能を最大限活用したい大規模なデータ処理。
bulk_create(update_conflicts=True)一括処理で高性能、アトミック性。Django 4.1+限定、createdフラグなし、save()メソッドのバイパス。大量のレコードをまとめて処理し、パフォーマンスとアトミック性が重要なデータインポート/同期。