Найти тему

Разбираемся с GenericRelation и GenericForeignKey в Django

Оглавление

Предыстория

Появилась необходимость привязывать заметок к проекту или встречи к проекту. Проекты, заметки и встречи лежат на 3-х отдеьлных микросервисах. Было решено хранить привязки на стороне привязываемых объектов, чтобы из микросервис проектов запрашивал заметки/встречи из соответствующих микросервисов.

На микросервисах могут появиться и другие объекты, которые нужно привязывать, а потому нарисовалась таблица привязки. Её поля:

  • тип объекта, к которому привязываем
  • идентификатор объекта
  • тип объекта, который привязываем
  • идентификатор объекта

Если с первыми двумя полями всё понятно: объект приходит извне, следовательно его тип — произвольное имя, а идентификатор — не является ForeignKey. А вот как быть с остальными двумя полями? Идентификатор обязан быть ForeignKey, иначе не сможем в queryset ни сортировать объекты, ни вынимать нужные поля. Простое решение уже есть в Django.

К делу!

Чтобы поле стало внешним ключом для объектов разного типа мы вместо `ForeignKey` используем комбинацию следующих полей:

  class Linker(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

Имена полей рекомендую такие, они приведены в официальной документации. Хотя в ней и написано, что допустимо использовать произвольные имена полей, указав их соответствия в аргументах, но соблазн мне стоил времени, и я вернул имена по-умолчанию.

в документации говорится о допустимости использования произвольных имён
в документации говорится о допустимости использования произвольных имён

Поле `content_object` существует лишь на уровне Python-кода и отсутствует в SQL-таблице. Являет собой объект из любой модели :)

Чтобы в queryset обращаться к полям конкретного объекта нужно в модель добавить поле GenericRelation. Например, для заметок я использовал:

  • `linker = GenericRelation(Linker, related_query_name='note')`, где `related_query_name` - имя объекта, доступное в queryset. `linker` - произвольное имя поля,

а для холстов фасилитации:

  • `linker = GenericRelation(Linker, related_query_name='faci')`.

Теперь мы можем делать запросы, например:

  • `link = Linker.objects.filter(note__title='заголовок').first()`
  • `link = Linker.objects.filter(faci__aim=2).first()`

В поле `link.content_object` будет объект заметки либо холста.