Django views.py 版本的 SQL Join 与多表查询

Posted

技术标签:

【中文标题】Django views.py 版本的 SQL Join 与多表查询【英文标题】:Django views.py Version of SQL Join with Multi Table Query 【发布时间】:2014-04-14 07:21:00 【问题描述】:

在 Django 版本的 SQL 多表查询方面需要一些帮助。该查询使用 3 个表从Restaurants table 检索餐厅名称、地址和从Cuisinetypes table 检索美食类型。全部基于通过 URL 传递的菜品名称,菜品 ID 存储在菜品表中。

Models.py

class Restaurant(models.Model):
    name = models.CharField(max_length=50, db_column='name', blank=True)
    slugname = models.SlugField(max_length=50, blank=True)
    address = models.CharField(max_length=100, blank=True)
    city = models.ForeignKey('City', related_name="restaurants")
    location = models.ForeignKey('Location', related_name="restaurants")
    hood = models.ForeignKey('Hood', null=True, blank=True, related_name="restaurants")
    listingrole = models.ForeignKey('Listingrole', related_name="restaurants")
    cuisine_types = models.ManyToManyField('Cuisinetype', null=True, blank=True, related_name="restaurants")
    class Meta:
        db_table = 'restaurant'

class City(models.Model):
    name = models.CharField(max_length=50, db_column='city')
    state = models.CharField(max_length=50, blank=True, null=True)
    switch = models.SmallIntegerField(null=True, blank=True, default='1')
    class Meta:
        db_table = 'city'

class Cuisinetype(models.Model):
    name = models.CharField(max_length=50, db_column='cuisine', blank=True) # Field name made lowercase.
    switch = models.SmallIntegerField(null=True, blank=True, default='1')
    class Meta:
        db_table = 'cuisinetype'

class Location(models.Model):
    name = models.CharField(max_length=50, db_column='location', blank=False, null=False)
    city = models.ForeignKey('City', related_name="locations")
    switch = models.SmallIntegerField(null=True, blank=True, default='1')
    class Meta:
        db_table = 'location'

class Hood(models.Model):
    name = models.CharField(max_length=50, db_column='hood')
    city = models.ForeignKey('City', related_name='hoods')
    location = models.ForeignKey('Location', related_name='hoods')
    switch = models.SmallIntegerField(null=True, blank=True, default='1')
    class Meta:
        db_table = 'hood'    

class Listingrole(models.Model):
    id = models.AutoField(primary_key=True, db_column='id')
    name = models.CharField(max_length=50, db_column='listingrole', blank=True) # Field name made lowercase.
    switch = models.SmallIntegerField(null=True, blank=True, default='1')
    class Meta:
        db_table = 'listingrole'

urls.py

url(r'^cuisine/(?P<cuisine>[-\w]+)/$', 'views.cuisinesearch'),

views.py

def cuisinesearch(request, name='unknown'):
name = name.replace('-', ' ').capitalize()
return render_to_response('cuisinesearch.html', 
                          'cuisinesearch': Restaurant.objects.filter(city_id=8, switch=1, listingrole__in=[1,2,3,4], cuisine_types__name=name)
                          .distinct().prefetch_related("cuisine_types").order_by('listingrole', 'displayorder')[:50] )

HTML

还有什么是显示查询的正确方法?

% for restaurant in cuisinesearch %
<h2> restaurant.name </h2>
<div class="location"> restaurant.location </div>
<h3>Cuisines:</h3>
<ul class="cuisines">% for ct in restaurant.cuisine_types.all %
<li> ct.name </li>% endfor %
</ul>
% endfor %

【问题讨论】:

使用 Django 的大部分意义在于使用它的 ORM 为你做这种事情,即使用 models,而不是执行任意 SQL。请出示您的型号代码。 根据您的要求,我添加了模型。谢谢! 您似乎试图强迫 Django 以某种方式制作数据库,而不是让它做它擅长的事情。您为什么不看一下 Django 文档,了解它如何处理 many-to-many relationships 并相应地调整您的模型... 我只是在处理我已经从另一个项目中获得的东西,而且几乎不可能从头开始,因为我已经有超过 100 个包含数据的表。 你看过this吗? 【参考方案1】:

嗯,这些是一些不清楚的表和字段名称,但我可以说查询看起来像:

(Restaurant.objects.filter(city=8, 
     cuisine__cuisinetype__cuisine="Italian").distinct().order_by('name')[:20])

但除非您被锁定在该数据库架构中,否则您的模型看起来会更好:

class CuisineType(models.Model):
    name = models.CharField(max_length=50)
    class Meta:
        db_table = 'cuisinetype'

class Restaurants(models.Model):
    city = models.ForeignKey("City", null=True, blank=True) # Apparently defined elsewhere. Should be part of location?
    name = models.CharField(max_length=50)
    location = models.ForeignKey("Location", null=True, blank=True) # Apparently defined elsewhere.
    cuisines = models.ManyToManyField(CuisineType)

那么查询会更像:

Restaurant.objects.filter(city=8, cuisines__name="Italian").order_by('name')[:20]

好的,让我们看看您的查询,假设您的代码没有更改。我们将从子查询开始。

SELECT DISTINCT res_id FROM cuisine 
        JOIN    cuisinetype ON cuisine.cuisineid = cuisinetype.`cuisineid`
        WHERE   cuisinetype.`cuisine` = 'Italian'

我们查看 WHERE 子句,发现我们需要一个 JOIN。要进行连接,您必须在其中一个连接模型中声明一个关系字段(Django 将添加一个反向关系,我们应该命名它)。所以我们将cuisine.cuisineid 与`cuisinetype.cuisineid 匹配。这是一个可怕的命名。

这是一个多对多的关系,所以我们需要一个ManyToManyField。好吧,看看Cuisine 模型,它确实是这个M2M 的连接表。 Django 期望连接表有两个ForeignKey 字段,一个指向关节的每一侧。通常它会为你创建这个以节省理智。看来你没那么幸运。所以你必须手动连接它。

“GID”字段似乎是记录的(无用)ID 字段,所以我们假设它是自增整数。 (可以肯定的是,检查 CREATE TABLE 命令。)现在我们可以将Cuisine 模型重写为接近理智的东西:

class Cuisine(models.Model):
    cuisinegid = models.AutoField(primary_key=True, db_column='CuisineGID')
    cuisineid = models.ForeignKey("Cuisinetype", null=True, 
        db_column='CuisineID', blank=True)
    res_id = models.ForeignKey("Restaurant", null=True, db_column='Res_ID', 
        blank=True)
    class Meta:
        db_table = 'cuisine'

引用模型名称是因为模型尚未定义(它们在文件的后面)。现在没有要求 Django 字段名称与列名称匹配,所以让我们将它们更改为更具可读性的名称。记录 ID 字段通常只是命名为id,而外键通常以它们相关的名称命名:

class Cuisine(models.Model):
    id = models.AutoField(primary_key=True, db_column='CuisineGID')
    cuisine_type = models.ForeignKey("CuisineType", null=True, 
        db_column='CuisineID', blank=True)
    restaurant = models.ForeignKey("Restaurant", null=True, db_column='Res_ID', 
        blank=True)
    class Meta:
        db_table = 'cuisine'

好的,我们已经完成了联合表的定义。在此过程中,让我们将相同的东西应用于我们的Cuisinetype 模型。注意更正后的驼峰类名:

class CuisineType(models.Model):
    id = models.AutoField(primary_key=True, db_column='CuisineID')
    name = models.CharField(max_length=50, db_column='Cuisine', blank=True)
    class Meta:
        db_table = 'cuisinetype'

所以我们终于得到了Restaurant 模型。注意名称是单数的;一个对象只代表一个记录。

我注意到它缺少任何 dp_tabledb_column 的东西,所以我冒昧地猜测 Django 正在创建它。这意味着我们可以让它为我们创建id 字段,我们可以在我们的代码中省略它。 (如果不是这样,那么我们只需像其他模型一样添加它。但你真的不应该有一个可以为空的记录 ID。)这就是我们的美食类型 ManyToManyField 所在的地方:

class Restaurants(models.Model):
    city_id = models.ForeignKey(null=True, blank=True)
    name = models.CharField(max_length=50, blank=True)
    location = models.ForeignKey(null=True, blank=True)
    cuisine_types = models.ManyToManyField(CuisineType, through=Cuisine,
        null=True, blank=True)

请注意,M2M 字段的名称是复数,因为这种关系会导致多条记录。

我们想要添加到这个模型的另一件事是反向关系的名称。换句话说,如何从其他模型回到Restaurant。我们通过添加related_name 参数来做到这一点。他们相同并不罕见。

class Restaurant(models.Model):
    city_id = models.ForeignKey(null=True, blank=True, 
        related_name="restaurants")
    name = models.CharField(max_length=50, blank=True)
    location = models.ForeignKey(null=True, blank=True, 
        related_name="restaurants")
    cuisine_types = models.ManyToManyField(CuisineType, through=Cuisine,
        null=True, blank=True, related_name="restaurants")

现在我们终于准备好了。那么让我们看看您的查询:

SELECT  restaurants.`name`, restaurants.`address`, cuisinetype.`cuisine`
FROM    restaurants
JOIN    cuisinetype ON cuisinetype.cuisineid = restaurants.`cuisine`
WHERE   city_id = 8 AND restaurants.id IN (
        SELECT DISTINCT res_id FROM cuisine 
        JOIN    cuisinetype ON cuisine.cuisineid = cuisinetype.`cuisineid`
        WHERE   cuisinetype.`cuisine` = 'Italian')
ORDER BY restaurants.`name`
LIMIT 20

由于这是FROM restaurants,我们将从该模型的默认对象管理器objects开始:

Restaurant.objects

本例中的WHERE 子句是filter() 调用,因此我们将它添加到第一个术语:

Restaurant.objects.filter(city=8)

您可以在该术语的右侧有一个主键值或一个City 对象。但是,查询的其余部分变得更加复杂,因为它需要JOIN。 Django 中的连接看起来就像通过关系字段取消引用。在查询中,这意味着用双下划线连接相关字段名称:

Restaurant.objects.filter(city=8, cuisine_type__name="Italian")

Django 知道要加入哪些字段,因为这是在Cuisine 表中声明的,该表由cuisine_types 中的through=Cuisine 参数拉入。它还知道执行子查询,因为您正在经历 M2M 关系。

这样我们的 SQL 就相当于:

SELECT  restaurants.`name`, restaurants.`address`
FROM    restaurants
WHERE   city_id = 8 AND restaurants.id IN (
        SELECT res_id FROM cuisine 
        JOIN    cuisinetype ON cuisine.cuisineid = cuisinetype.`cuisineid`
        WHERE   cuisinetype.`cuisine` = 'Italian')

到了一半。现在我们需要SELECT DISTINCT,这样我们就不会得到同一记录的多个副本:

Restaurant.objects.filter(city=8, cuisine_type__name="Italian").distinct()

并且您需要拉入菜肴类型以进行展示。事实证明,您的查询在那里效率低下,因为它只会让您进入连接表,并且您需要运行进一步的查询以获取相关的 CuisineType 记录。猜猜看:Django 已经满足你了。

(Restaurant.objects.filter(city=8, cuisine_type__name="Italian").distinct()
    .prefetch_related("cuisine_types"))

Django 将运行两个查询:一个像你的查询来获取联合 ID,另一个查询是获取相关的 CuisineType 记录。那么通过查询结果访问就不需要回数据库了。

最后两件事是排序:

(Restaurant.objects.filter(city=8, cuisine_type__name="Italian").distinct()
    .prefetch_related("cuisine_types").order_by("name"))

还有LIMIT

(Restaurant.objects.filter(city=8, cuisine_type__name="Italian").distinct()
    .prefetch_related("cuisine_types").order_by("name")[:20])

您的查询(和相关查询)被打包到两行 Python 中。请注意,此时,查询甚至还没有执行。在它执行任何操作之前,您必须将其放入模板之类的东西中:

def cuisinesearch(request, cuisine):
    return render_to_response('cuisinesearch.html', 
        'restaurants': (Restaurant.objects.filter(city=8, 
             cuisine_type__name="Italian").distinct()
             .prefetch_related("cuisine_types").order_by("name")[:20])
        )

模板:

% for restaurant in cuisinesearch %
<h2> restaurant.name </h2>
<div class="location"> restaurant.location </div>
<h3>Cuisines:</h3>
<ul class="cuisines">% for ct in restaurant.cuisine_types.all %
<li> ct.name </li>% endfor %
</ul>
% endfor %

【讨论】:

谢谢,我已经用 Models.py 文件更新了原帖。 非常感谢您的深入回复。根据您的回复,我意识到我需要从头开始并重新制作模型。根据您的建议,我重新设计了模型并在我的原始帖子中发布了模型的新版本。如果您能在我运行“syncdb”之前查看并提供反馈,我将不胜感激。再次感谢! 您可以删除id = models.AutoField(primary_key=True, db_column='id')的每个实例; Django 会自动为您创建这些。唯一需要写出来的就是db_column 是不是id 2) 不要在字段名称后面加上 _id。对于ForeignKey 字段,Django 将其单独放在列名中。 3) listingrole 应该是什么?是一对多还是多对多的关系? 4) 从max_length 字段中删除L 后缀;对它们的需求为零。 5) 除非您要连接到现有表,否则不要使用db_tabledb_column。 6) ForeignKey 字段的相关名称应该是复数,因为它们可以返回任意数量的记录。例如,Hood 的 FK 中的相关名称应为 hoods 谢谢,我会的。我主要出于视觉原因包括该领域。作为每个ManyToManyField,我是否正确或是否需要包含 through='' 值?再次感谢,非常感谢您的帮助!

以上是关于Django views.py 版本的 SQL Join 与多表查询的主要内容,如果未能解决你的问题,请参考以下文章

Django:如何通过views.py更改值

Django-编写Views.py

Django之views.py视图函数学习

[Django笔记] views.py 深入学习

每次请求视图时,django 是不是都会编译 views.py?

django导入 views.py