Skip to main content
 首页 » 编程设计

python之Django ManyToManyField 在哪里/如何在数据库中表示

2024年08月27日27lidabo

更新: 刚发现在选择特定相册时 ManyToManyField 导致管理界面崩溃。我将它们注释掉,注释掉对它的所有引用,重新运行 makemigrations 和 migrate,现在管理界面再次工作......这让我离制作这个更远了“最喜欢的”专栏作品 :( 请参阅此跟进:Why is Django ManyToManyField causing admin interface to crash? Why is no through table being created?


背景:我的目标是成为“最爱?”此网页中的列反射(reflect)了当前登录用户最喜欢的相册,其中每个是“否”或"is",并且是一个可单击的链接以切换选择。 (未登录时,它们都是灰色的“n/a”-s。)

因此,对于每个相册,每个用户可能恰好有零个或一个“已收藏”条目。如果该条目存在,则表示他们收藏了它。如果它不存在,他们就没有。

这是我的 Album 模型,带有 favorited_by_users 多对多列(完整的 models.py 在底部):

class Album(models.Model): 
    OFFICIALITY = ( 
        ('J', 'Major studio release'), 
        ('I', 'Non-major official release'), 
        ('U', 'Unofficial'), 
    ) 
    title = models.CharField(max_length=70) 
    description = models.TextField(max_length=500, default="", null=True, blank=True) 
    pub_date = models.DateField('release date') 
    officiality = models.CharField(max_length=1, choices=OFFICIALITY) 
    is_concert = models.BooleanField(default=False) 
    main_info_url = models.URLField(blank=False) 
    thumbnail = models.FileField(upload_to=get_upload_file_name, blank=True, null=True) 
 
    #virtual field to skip over the through table. 
    songs = models.ManyToManyField("Song", through="AlbumSong") 
 
    favorited_by_users = models.ManyToManyField(User) 
 
    def __str__(self): 
        return  self.title 
 
    class Meta: 
        #Default ordering is by release date, ascending. 
        ordering = ['pub_date'] 

我最初有这个 FavoriteAlbum 模型,但是由于它除了外键之外没有额外的信息,所以建议我删除它以支持上面的多对多列。

class FavoriteSongs(models.Model): 
    user = models.ForeignKey(User) 
    song = models.ForeignKey(Song) 
 
    class Meta: 
        unique_together = ('user', 'song',) 
 
    def __str__(self): 
        return  "user=" + str(self.user) + ", song=" + str(self.song) 

我需要做的是相册和用户之间的“左连接”,其中所有相册都被选中,并且当前登录用户的任何收藏夹都加入其中(None 如果他们还没有收藏它)。我不知道该怎么办。

我也听说过 extra()函数来执行此连接。 View 的 get_queryset() 中的当前工作查询是

return  super(AlbumList, self).get_queryset().order_by("pub_date") 

(下面是完整的 views.py。)我目前的猜测是:

return super(AlbumList, self).get_queryset().order_by("pub_date").extra(select={"is_favorite": "favorited_by_users__id = "+ str(request.user.id) })

但是,虽然这不会崩溃,但模板中每个 {{ is_favorite }} 的值都是nothing(空字符串)。这是有道理的,因为数据库中还没有任何内容,但是现在呢?我不知道这是否是正确的 Django 查询。

我想在数据库中添加一个项目来测试它,在 postgres 中使用手动 SQL 语句(还不是通过 Django 命令),但是我应该如何以及在何处执行此操作?

我已成功运行 makemigrations,然后使用这个新的 m2m 列(并且没有 FavoriteSongs 模型)进行 migrate,但我什么也没看到在代表 is-favorite 值的数据库中。 billyjoel_album 中没有额外的列,也没有类似于 billyjoel_favoritealbum 的直通表。那么这些数据在数据库中存储在哪里/如何存储?

(关于这个额外的“收藏夹”列的任何其他建议也将不胜感激!)

谢谢。


模型.py

from  django.db import models 
from  django.contrib.auth.models import User 
from  time import time 
 
def get_upload_file_name(instance, filename): 
    return  "uploaded_files/%s_%s" % (str(time()).replace(".", "_"), filename) 
 
class Album(models.Model): 
    OFFICIALITY = ( 
        ('J', 'Major studio release'), 
        ('I', 'Non-major official release'), 
        ('U', 'Unofficial'), 
    ) 
    title = models.CharField(max_length=70) 
    description = models.TextField(max_length=500, default="", null=True, blank=True) 
    pub_date = models.DateField('release date') 
    officiality = models.CharField(max_length=1, choices=OFFICIALITY) 
    is_concert = models.BooleanField(default=False) 
    main_info_url = models.URLField(blank=False) 
    thumbnail = models.FileField(upload_to=get_upload_file_name, blank=True, null=True) 
 
    #virtual field to skip over the through table. 
    songs = models.ManyToManyField("Song", through="AlbumSong") 
 
    favorited_by_users = models.ManyToManyField(User) 
 
    def __str__(self): 
        return  self.title 
 
    class Meta: 
        #Default ordering is by release date, ascending. 
        ordering = ['pub_date'] 
 
 
class Song(models.Model): 
    name = models.CharField(max_length=100) 
    description = models.TextField(max_length=500, default="", null=True, blank=True) 
    length_seconds = models.IntegerField() 
    lyrics_url = models.URLField(default="", blank=True, null=True) 
    albums = models.ManyToManyField("Album", through="AlbumSong") 
 
    favorited_by_users = models.ManyToManyField(User) 
 
    def get_length_desc_from_seconds(self): 
        if(self.length_seconds == -1): 
            return  "-1" 
        m, s = divmod(self.length_seconds, 60) 
        h, m = divmod(m, 60) 
        if(h): 
            return  "%d:%02d:%02d" % (h, m, s) 
        else: 
            return  "%d:%02d" % (m, s) 
 
    def __str__(self): 
        return  self.name 
 
class AlbumSong(models.Model): 
    song = models.ForeignKey(Song) 
    album = models.ForeignKey(Album) 
    sequence_num = models.IntegerField() 
 
    class Meta: 
        unique_together = ('album', 'sequence_num',) 
        unique_together = ('album', 'song',) 
 
    def __str__(self): 
        return  str(self.album) + ": " + str(self.sequence_num) + ": " + str(self.song) 

View .py

from  .models import Album, Song, AlbumSong 
from  datetime import datetime, timedelta 
from  django.core.context_processors import csrf 
from  django.shortcuts import render, render_to_response 
from  django.views.generic import DetailView, ListView 
from  enum import Enum 
 
def get_str_with_appended(string, between_if_str_non_empty, new_value): 
    if(len(string) == 0): 
        return  new_value 
    else: 
        return  string + between_if_str_non_empty + new_value 
 
class PrependQuestionMark(Enum): 
    YES, NO = range(2) 
 
def get_url_param_string_from_params(prepend_question_mark=PrependQuestionMark.YES, **kwargs_all_params): 
    param_list = "" 
    for  key in iter(kwargs_all_params): 
        value = kwargs_all_params[key] 
        if(value is not None): 
            param_list = get_str_with_appended(param_list, '&', str(key) + "=" + str(value)) 
 
    if(len(param_list) == 0): 
        return  param_list; 
 
    if(prepend_question_mark == PrependQuestionMark.YES): 
        return  "?" + param_list 
    else: 
        return  param_list 
 
class AlbumList(ListView): 
    model = Album 
    context_object_name = "albums" 
 
    #Derived from irc/#dango/tbaxter...START 
    def dispatch(self, request, *args, **kwargs): 
                                             #default to asc 
        self.sort_order = request.GET.get("sort_order", None) 
        self.sort_item = request.GET.get("sort_item", None) 
        self.csrf_token = csrf(request)["csrf_token"] 
        self.logged_in_user = request.user 
 
        #self.csrf_token = request.GET.get("csrf_token", None) 
        return super(AlbumList, self).dispatch(request, *args, **kwargs) 
 
    def get_queryset(self): 
        #Item zero in both is the default 
        #should be static global 
        asc_desc_list = ["asc", "dsc"] 
        sort_by_types = ["pub_date", "title"] 
 
        if(self.sort_order is None  and  self.sort_item is None): 
            #Use default ordering 
            return  super(AlbumList, self).get_queryset() 
 
        #Custom ordering requested 
 
        sort_order = self.sort_order 
        sort_item = self.sort_item 
 
        if(sort_order is None  or 
                sort_order not in asc_desc_list): 
            sort_order = asc_desc_list[0] 
        if(sort_item is None  or 
                sort_item not in sort_by_types): 
            sort_item = sort_by_types[0] 
 
        order_minus = ""  if  sort_order == "asc"  else "-" 
 
        return  super(AlbumList, self).get_queryset().order_by(order_minus + sort_item).extra(select={"is_favorite": "favorited_by_users__id = " + str(self.logged_in_user.id) }) 
 
    def get_context_data(self, **kwargs): 
        context = super(AlbumList, self).get_context_data(**kwargs) 
 
        context["sort_order"] = self.sort_order 
        context["sort_item"] = self.sort_item 
 
        context["url_params"] = get_url_param_string_from_params( 
            sort_item=self.sort_item, 
            sort_order=self.sort_order, 
            csrf_token=self.csrf_token) 
 
        return  context 
 
 
class AlbumDetail(DetailView): 
    model = Album 
    context_object_name = "album" 
 
    def dispatch(self, request, *args, **kwargs): 
                                             #default to asc 
        self.sort_order = request.GET.get("sort_order", None) 
        self.sort_item = request.GET.get("sort_item", None) 
        self.csrf_token = csrf(request)["csrf_token"] 
 
        return super(AlbumDetail, self).dispatch(request, *args, **kwargs) 
 
    def get_context_data(self, **kwargs): 
        #Call the base implementation first to get a context 
        context = super(AlbumDetail, self).get_context_data(**kwargs) 
 
        #Add in the required extra info: album_songs, ordered by 
        #sequence_num 
 
        #select_related is to query the database for all songs at once, here 
        #in the view, to prevent the template from pin-pricking the database 
        #in each for loop iteration. For large datasets, this is critical. 
        context['album_songs'] = kwargs["object"].albumsong_set.order_by('sequence_num').select_related("song") 
 
        context["url_params"] = get_url_param_string_from_params( 
            sort_item=self.sort_item, 
            sort_order=self.sort_order, 
            csrf_token=self.csrf_token) 
 
        return  context 

专辑列表.html

 {% extends "base.html" %} 
 {% load bj_filters %} 
 {% block title %}Billy Joel Album Browser{% endblock %} 
 
 {% block sidebar %} 
 <UL> 
  <LI><a href="{% url 'album_list' %}{{ url_params }}">All albums</A></LI> 
  <LI><a href="/admin/">Admin</A></LI> 
 </UL> 
 {% endblock %} 
 
 {% block content %} 
 
 <TABLE ALIGN="center" WIDTH="100%" BORDER="1" CELLSPACING="0" CELLPADDING="4" BGCOLOR="#EEEEEE"><TR ALIGN="center" VALIGN="middle"> 
 {% if  user.is_authenticated %} 
  <TD>My profile (<a href="{% url 'accounts_logout' %}">Logout</A>)</TD> 
 {% else %} 
  <TD><a href="{% url 'accounts_login' %}">Login</A> to view your favorites</TD> 
 {% endif %} 
 </TR></TABLE> 
 
 <H1>Billy Joel Album Browser</H1> 
 
 <!-- 
 <P>url_params={{ url_params }}</P> 
  --> 
 
 {% if  albums.count > 0 %} 
  <P>Officiality: <IMG SRC="/static/images/major.jpg" height="20"/>=Major studio release, <IMG SRC="/static/images/minor.jpg" height="20"/>=Official release, <IMG SRC="/static/images/unofficial.jpg" height="20"/>=Unofficial</P> 
  <TABLE ALIGN="center" WIDTH="100%" BORDER="1" CELLSPACING="0" CELLPADDING="4" BGCOLOR="#EEEEEE"><TR ALIGN="center" VALIGN="middle"> 
     <TD><B><U><a href="{% url 'album_list' %}?sort_item=title&sort_order= 
        {% if  sort_item == 'pub_date' %}asc{% else %} 
           {{ sort_order|multival_to_str:'asc,dsc->dsc,asc,dsc' }} 
        {% endif %} 
     &csrf_token={{ csrf_token }}">Title</A></U></B><BR><I><FONT SIZE="-1">(click a title to view its song list)</FONT></I></TD> 
     <TD><B><U><a href="{% url 'album_list' %}?sort_item=pub_date&sort_order= 
        {% if  sort_item == 'title' %}asc{% else %} 
           {{ sort_order|multival_to_str:'asc,dsc->dsc,asc,dsc' }} 
        {% endif %} 
     &csrf_token={{ csrf_token }}">Released</A></U></B></TD> 
     <TD>Officiality</TD> 
     <TD>Concert</TD> 
     <TD>Wiki</TD> 
     <TD>Favorite?</TD> 
  {% for  album in albums %} <!-- No colon after "albums" --> 
  </TR><TR> 
     <TD VALIGN="top"> 
        {% if  album.thumbnail %} 
           <img src="/static/{{ album.thumbnail }}" width="25"/> 
        {% else %} 
           <img src="/static/images/white_block.jpg" width="25"/> 
        {% endif %} 
        &nbsp; <a href="/albums/get/{{ album.id }}{{ url_params }}">{{ album.title }}</a> 
        {% if  album.description %} 
           <BR/><FONT SIZE="-1"><I>{{ album.description|truncatewords:10 }}</I></FONT> 
        {% endif %} 
     <TD>{{ album.pub_date|date:"m/y" }}</TD> 
     <TD><IMG SRC="/static/images/{{ album.officiality|multival_to_str:"J,I,U->major,minor,unofficial,broken_image"}}.jpg" height="20"/></TD> 
     <TD>{{ album.is_concert|yesno:"Yes,No" }}</TD> 
     <TD><A HREF="{{ album.main_info_url }}">Wiki</A></TD> 
     <TD><I>n/a {{ is_favorite }}</I></TD> 
  {% endfor %} 
  </TR></TABLE> 
 {% else %} 
  <P><I>There are no albums in the database.</I></P> 
 {% endif %} 
 
 {% endblock %} 

请您参考如下方法:

M2M 关系是全新创建的表格。他们有一个唯一的名字,并且有两个外键。创建复合键,因此直接和相关模型组合是唯一的。

当你这样做时:

class Topping(models.Model): 
    name = ... 
 
class Pizza(models.Model): 
    name = ... 
    toppings = models.ManyToManyField(Topping, related_name="pizzas") 
    #not including a related_name will generate a "pizza_set" related name. 

出现一个描述关系的新表,带有一个内部名称。该表有一个 pizza_id 和一个 topping_id 外键,以及一个包含这两个字段的复合唯一键。您不能也不应该预测此类表的名称。

另一方面,如果您想要访问关系并可能声明更多字段,您可以:

class Topping(models.Model): 
    name = ... 
 
class Pizza(models.Model): 
    name = ... 
    toppings = models.ManyToManyField(Topping, related_name="pizzas", through="PizzaAndTopping") 
    #not including a related_name will generate a "pizza_set" related name. 
 
class PizzaAndTopping(models.Model): 
    more_data = models.TextField() 
    pizza = models.ForeignKey(Pizza, null=False) 
    topping = models.ForeignKey(Topping, null=False) 
 
    class Meta: 
        unique_together = (('pizza','topping'),) 

请注意我是如何添加 through 参数的。现在您可以控制中间表但是您不能从关系中添加或删除模型。这意味着,使用这种方法您不能:

Pizza.objects.get(pk=1).toppings.append(Topping.objects.get(pk=2)) 

也不能删除,也不能在生命的Topping面做这些操作。

如果您想添加或删除 toppin-pizza 关系,您必须直接在 PizzaAndTopping 关系中进行。

如果您想知道当前用户是否将任何歌曲标记为他们的最爱,您应该预取 关系。在 Django 1.7 中,您可以使用自定义过滤器预取 M2M 相关字段:您可以使用 Prefetch 对象获取所有相册,并且仅获取当前用户的查询。请参阅有关预取的官方文档 here .

Antoher 解决方案包括:

  • 获取当前用户最喜欢的专辑列表:ulist = user.album_set.all()
  • 获取专辑的当前页面:_list = Album.objects.all()[0:20]
  • 获取用户相册的值:ulist = ulist.values_list('id', flat=True)

    [1, 2, 4, 5, 10, ...] #you'll get a list of ids 
    
  • 当遍历页面中的每个相册时,您测试 ulist 中的 currentAlbum.id 并打印不同的消息(是或否)。