了解 Django 的 ORM 中的规范化表

Understanding normalization tables in Django's ORM

我正在尝试自己直接对数据库模式进行编码的背景来学习 Django。我想了解我应该如何有效地使用数据库抽象工具进行规范化。

作为一个人为的例子,假设我有一个对话可以就 3 个主题提出问题,并且每个问题都足够复杂以保证其本身 Class。

Class Conversation(models.Model):
  partner = models.CharField()
Class Weather_q(models.Model):
  #stuff
Class Health_q(models.Model):
  #stuff
Class Family_q(models.Model):
  #stuff

假设我想进行 2 次对话:

通常,我会为此编写规范化 table:

INSERT INTO Conversation (partner) values ("Bob", "Alice"); --primary keys = 1 and 2
INSERT INTO NormalizationTable (fk_Conversation, fk_Weather_q, fk_Health_q,  fk_Family_q) VALUES 
  (1,1,0,0), -- Bob weather#1
  (1,2,0,0), -- Bob weather#2
  (1,0,1,0), -- Bob health#1
  (2,1,0,0), -- Alice weather#1
  (2,0,0,1); -- Alice family#1

我是否需要明确创建此规范化 table 还是不鼓励这样做?

Class NormalizationTable(models.Model):
  fk_Conversation = models.ForeignKey(Conversation)
  fk_Weather_q = models.ForeignKey(Weather)
  fk_Health_q = models.ForeignKey(Health)
  fk_Family_q = models.ForeignKey(Family)

然后我想执行对话。我写了一个这样的视图(跳过异常捕获和逻辑来遍历每个对话的多个问题):

from myapp.models import Conversation, Weather_q, Health_q, Family_q
def converse(request):
  #get this conversation's pk
  #assuming "mypartner" is provided by the URL dispatcher
  conversation = Conversation.objects.filter(partner=mypartner)[0]
  #get the relevant row of the NormalizationTable
  questions = NormalizationTable.objects.filter(fk_Conversation=conversation)[0]
  for question in questions:
    if question.fk_Weather_q:
      return render("weather.html", Weather_q.objects.filter(pk=fk_Weather_q)[0])
    if question.fk_Health_q:
      return render("health.html", Health_q.objects.filter(pk=fk_Health_q)[0])
    if question.fk_Family_q:
      return render("family.html", Family_q.objects.filter(pk=fk_Family_q)[0])

从整体上考虑,这是"Django"解决这种归一化问题(N个对象与一个容器对象相关联)的方法吗?我可以更好地利用 Django 的内置 ORM 或其他工具吗?

我不熟悉术语规范化table,但我知道你在做什么。

在我看来,您所描述的并不是一种非常令人满意的数据库建模方法。最简单的方法是使所有问题成为同一个 table 的一部分,带有一个 "type" 字段,可能还有一些其他可选字段因类型而异。在那种情况下,这在 Django 中变得非常简单。

但是,好吧,你说 "let's say... each question is complicated enough to warrant its own class." Django 确实有一个解决方案,那就是 generic relations。它看起来像这样:

class ConversationQuestion(models.Model):
    conversation = models.ForeignKey(Conversation)
    content_type = models.ForeignKey(ContentType)
    question_id = models.PositiveIntegerField()
    question = GenericForeignKey('content_type', 'question_id')

# you can use prefetch_related("question") for efficiency
cqs = ConversationQuestion.objects.filter(conversation=conversation)
for cq in cqs:
    # do something with the question
    # you can look at the content_type if, as above, you need to choose
    # a separate template for each type.
    print(cq.question)

因为它是 Django 的一部分,所以您在管理、表单等方面得到了一些(但不是全部)支持。

或者您可以执行上述操作,但是,如您所见,它很丑陋并且似乎没有体现使用 ORM 的优势。

撇开"normalization tables"(我不熟悉这个词),这就是我认为"djangish"解决问题的方法。请注意,我同意你的声明 "each question is complicated enough to warrant its own Class"。对我来说,这意味着每种类型的问题都需要其独特的领域和方法。否则,我会创建一个 Question 模型,通过 ForeignKey.

连接到 Category 模型
class Partner(models.Model):
    name = models.CharField()


class Question(models.Model):
    # Fields and methods common to all kinds of questions
    partner = models.ForeignKey(Partner)
    label = models.CharField()  # example field


class WeatherQuestion(Question):
    # Fields and methods for weather questions only


class HealthQuestion(Question):
    # Fields and methods for health questions only


class FamilyQuestion(Question):
    # Fields and methods for family questions only

这样,您将拥有一个基础 Question 模型,用于所有问题共有的所有字段和方法,以及一堆用于描述不同类型问题的子模型。基础模型与其子模型之间存在隐式关系,由 Django 维护。这使您能够创建具有不同问题的单个查询集,无论它们的类型如何。此查询集中的项目默认为 Question 类型,但可以通过访问特殊属性(例如 HealtQuestionhealthquestion 属性)转换为特定问题类型。这在 "Multi-table model inheritance" section of Django documentation.

中有详细描述

然后在视图中您可以获得(不同类型的)问题列表,然后检测它们的特定类型:

from myapp.models import Question

def converse(request, partner_id):
    question = Question.objects.filter(partner=partner_id).first()

    # Detect question type
    question_type = "other"
    question_obj = question
    # in real life the list of types below would probably live in the settings
    for current_type in ['weather', 'health', 'family']:
        if hasattr(question, current_type + 'question'):
            question_type = current_type
            question_obj = getattr(question, current_type + 'question')
            break

    return render(
        "questions/{}.html".format(question_type),
        {'question': question_obj}
    )

检测问题类型的代码非常难看和复杂。您可以使用 InheritanceManager from django-model-utils 包使其更简单、更通用。您需要安装软件包并将行添加到 Question 模型:

objects = InheritanceManager()

然后视图将如下所示:

from myapp.models import Question

def converse(request, partner_id):
    question = Question.objects.filter(partner=partner_id).select_subclasses().first()
    question_type = question._meta.object_name.lower()

    return render(
        "questions/{}.html".format(question_type),
        {'question': question}
    )

两种观点 select 只有一个问题 - 第一个。这就是您示例中的视图的行为方式,所以我同意了。您可以轻松地将这些示例转换为 return 问题列表(不同类型)。