Предыстория
Появилась необходимость привязывать заметок к проекту или встречи к проекту. Проекты, заметки и встречи лежат на 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` будет объект заметки либо холста.