Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Database Modeling with multiple many-to-many relations in Django

I could need some help on designing my models and their relationships.

Short overview

  • Book: book titles (e.g. "Lord of the Rings")
  • Tag: tags associated with books (e.g. "Fantasy", "Wizard", "Epic fight")
  • Aspect: aspects associated with tags, or, in other words, things users can rate when a specific tag occurs, e.g.:
    • for the tag "Fantasy" aspects could be "World Detail" and "Time"
    • for the tag "Epic fight" aspects could be "Gore Level" and "Fight tension"

Each Tag can be used in multiple instances of Book (e.g. "Lord of the Rings" and "Discworld" both have the tag "Fantasy").

Each Aspect can be used in multiple instances of Tag (e.g. "Fantasy" and "Scifi" both have the aspect "World Detail").

Here is a descriptive image:

enter image description here

(wow, these are big ones)

"Why do you need that extra table BookAspect?" you might ask?

Because i want to store user ratings for each aspect that is related to a specific book.

That's the main problem here. I want to model this in Django and this is what i got so far:

class Book(models.Model):
    title = models.CharField(max_length=100, unique=True)
    tags = ManyToManyField(Tag)
    # the following line is a workaround...
    aspects = models.ManyToManyField(Aspect, through='BookAspect')

class Tag(models.Model):
    name = models.CharField(max_length=100)
    aspects = models.ManyToManyField(Aspect)

class Aspect(models.Model):
    name = models.CharField(max_length=100)

# this class is a workaround ...
class BookAspect(models.Model):
    book = models.ForeignKey(Book)
    aspect = models.ForeignKey(Aspect)
    # this is from django-ratings
    rating = RatingField(range=5, can_change_vote=True, allow_delete=True, blank=True)

    class Meta:
        unique_together = ('book', 'aspect',)

In addition to the models, i created a m2m_changed signal listener for action="post_add":

@receiver(m2m_changed, sender=Book.tags.through)
def m2m_changed_book(sender, instance, action, reverse, pk_set, **kwargs):
    if action is not 'post_add' or reverse:
        return

    # iterate through all newly created tags to manually add
    # the aspects from each tag to the BookAspect table
    for tag_id in pk_set:
        aspects = Tag.objects.get(pk=tag_id).aspects.all()
        # this is annoying, i have to manually set the relations...
        for aspect in aspects:
            bookAspect = BookAspect(book=instance, aspect=aspect)
            bookAspect.save()

Although that should work, i would need additional logic to deal with removed tags.

But the real annoying thing is that i have to manually add the per-book-aspect-relations so that i can store user ratings. Naturally i need different ratings for the same aspect of different books.

Questions

1. Is this the right way to do it, am i missing something or isn't it that complicated as i think?

2. Is it possible to "automate" the Book-Aspect relation so that i don't have to manually update the relations?

3. How can i model it in Django?

like image 304
Alp Avatar asked May 14 '12 18:05

Alp


1 Answers

  • I think it can be done with more simplicity.
  • I think, to some extent.
  • I would change:

    class Book(models.Model):
      title = models.CharField(max_length=100, unique=True)
      tags = ManyToManyField(Tag)
    

Since the aspects field has nothing to do in the Book class.

I have no idea of why you wrote BookAspect. About user ratings, you can do:

class BookRating(models.Model):
  book = models.ForeignKey(Book)
  aspect = models.ForeignKey(Aspect)
  rating = models.RatingField()
  # Method to rate a book and check the aspect belong to one of the book's tags
  def rate(book, aspect):
    # check the aspect is related to a tag which is also related to the book
    # if so, save a new rating entry
  # You can also override the save() method to check the aspect is valid for the book
like image 157
Steve K Avatar answered Sep 30 '22 14:09

Steve K