【问题标题】:Is it possible to foreign key on bulk_create() Django?是否可以在 bulk_create() Django 上设置外键?
【发布时间】:2020-10-04 13:27:40
【问题描述】:

我需要一次创建许多 EVENT 对象,然后创建许多 ARCHIVED_EVENT 对象,这些对象具有对应事件的外键。

我的代码看起来像这样:

events = []
archivedEvents = []
for _ in range(1000):
    event = Event(name="Test")
    archivedEvent = ArchivedEvent(event_id=event.id)
    archivedEvents.append(archivedEvent)
    events.append(event)

Event.objects.bulk_create(events)
ArchivedEvent.objects.bulk_create(archivedEvents)

不幸的是,这里创建的所有存档事件都有一个指向 EVENT 的 NULL 外键。我知道在将对象保存到数据库之前不会生成对象的主键。但是我在创建存档事件之前保存了这些事件。 我错过了什么吗? 我应该在批量创建归档事件之前刷新缓存吗?

【问题讨论】:

    标签: mysql django django-models


    【解决方案1】:

    event.id 不会被创建,除非它通过bulk_create() 创建。可能您可以使用另一个循环来创建ArchivedEvent。像这样:

    events = []
    archivedEvents = []
    
    for _ in range(1000):
        event = Event(name="Test")
        events.append(event)
    events = Event.objects.bulk_create(events)
    
    for event in events:
        archivedEvent = ArchivedEvent(event_id=event.id)
        archivedEvents.append(archivedEvent)
    ArchivedEvent.objects.bulk_create(archivedEvents)
    

    根据文档,此解决方案适用于 Postgresql。如果你有其他数据库,那么试试这样:

    events = []
    archivedEvents = []
    
    for _ in range(1000):
        event = Event(name="Test")
        events.append(event)
    Event.objects.bulk_create(events)
    
    events = Event.objects.all().order_by('-id')[:1000][::-1]
    for event in events:
        archivedEvent = ArchivedEvent(event_id=event.id)
        archivedEvents.append(archivedEvent)
    ArchivedEvent.objects.bulk_create(archivedEvents)
    

    为避免竞争条件,您可以预先生成主键,然后使用它们。例如:

    last_event_id = Event.objects.last().id + 1000 # getting last event id and adding thousand to avoid duplicates.
    
    events = [x+last_event_id for x in range(1000)]
    archivedEvents = []
    
    for e in events:
        event = Event(name="Test",pk=e)
        events.append(event)
        archivedEvent = ArchivedEvent(event_id=e)
        archivedEvents.append(archivedEvent)
    
    Event.objects.bulk_create(events)
    ArchivedEvent.objects.bulk_create(archivedEvents)
    

    【讨论】:

    • 因为 event.id 现在有了值。
    • 这不起作用,归档事件的 event_id 仍然为 NULL。
    • 您是否考虑过这种方法的竞争条件?如果在 bulk_create() 和查询之间创建了 Event 对象,那么一切都会非常糟糕。最好的选择真的是单独保存()吗?
    • 竞争条件似乎仍在继续。如果运行此代码的视图同时被调用两次,那么一切都会崩溃。
    • 那么这超出了在此处使用批量创建的目的。这段代码不应该与视图一起使用,而是在管理命令或 shell 中使用。因此,正如 Ludovico 建议的那样,您应该使用单独的 save()。
    【解决方案2】:

    这是一个基于@ruddra 的其他数据库解决方案避免使用db 锁的竞争条件的解决方案。希望对您有所帮助。

    from django.db import transaction
    
    with transaction.atomic():
    
       # lock first row to avoid race condition
       # note: first row of Event table must have content,
       #       if not, you need use other tables's not empty row to add lock.
       Event.objects.select_for_update().first()
    
       # just @ruddra's mysql solution
       events = []
       archivedEvents = []
    
       for _ in range(1000):
           event = Event(name="Test")
           events.append(event)
       Event.objects.bulk_create(events)
    
       event_ids = Event.objects.values_list('id', flat=True).order_by('-id')[:1000][::-1]
       for event_id in event_ids:
           archivedEvent = ArchivedEvent(event_id=event_id)
           archivedEvents.append(archivedEvent)
       ArchivedEvent.objects.bulk_create(archivedEvents)
    

    【讨论】:

    • 这里到底发生了什么?为什么要锁定第一行?
    • 这只是 db 为避免竞争条件而提供的一个锁,您可以使用任何其他表行。通常,我使用第一行作为锁。见select-for-update
    • 嗯,也许这种锁定技术会比单次 saves() 花费更多的时间,因为这个视图现在可以同时运行,但不是真的,因为某些线程需要等待。
    • 我不这么认为。最终所有数据在插入时都需要等待数据库级别的锁定。参见Which is faster: multiple single INSERTs or one multiple-row INSERT?,虽然视图现在同时运行,但它可以更快地处理请求。不管怎样,为了时间成本,你最好做个测试。
    猜你喜欢
    • 2012-03-11
    • 2012-06-03
    • 1970-01-01
    • 1970-01-01
    • 2014-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多