sdispater / orator

The Orator ORM provides a simple yet beautiful ActiveRecord implementation.

Home Page:https://orator-orm.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Many-to-many relationship with extra attributes not working

opened this issue · comments

Hello,

This is really impressive work -- I'm very familiar with this structure from Rails, and I like the intuitiveness of the way you've put things together. I also like that there's a good separation of concerns and choice of design patterns.

For this reason, I wanted to check it out for a new side project I'm considering, but I found no tutorials. I am trying to create a many-to-many relationship with a few extra attributes, and I'm not able to figure it out from the documentation alone. Here's what I have.

Say, I'm modelling an Amazon-like bookseller, Fakazon. There's a catalog of Books, and Users who can buy them. A User can buy many Books and a Book can be bought by many Users. A User can also give a rating and a review for a Book. Fakazon also wants to store the date a User purchased a Book. So it's a many-to-many association, but with extra data on the association. The database model I came up with is this:

User
----
id
name

Book
----
id
title

UserBook
--------
id
user_id
book_id
purchased_on
rating
review

I want to be able to know not only which Books a certain User bought, but also which Users bought a certain Book. That is, I should be able to say book.users as well as user.books, each of which returns a list. For example, user.books returns:

[
  {
    "title": "Genome",
    "rating": 5,
    "review": "A review of human genomics in 23 chapters",
    "purchased_on": "2020-04-02"
    . . .
  }
]

Here's my code;

# user.py

class User(Model):
    __fillable__ = ["name"]

    def __repr__(self):
        return self.name

    @has_many_through("user_books", "book_id")
    def books(self):
        return models.Book
class Book(Model):
    __fillable__ = ["title"]

    def __repr__(self):
        return self.title

    @has_many_through("user_books", "user_id")
    def users(self):
        return models.User
class UserBook(Model):
    __fillable__ = ["purchased_on", "rating", "review"]

    @belongs_to
    def book(self):
        return Book

    @belongs_to
    def user(self):
        return User

The migrations are correct, the database tables are also correct, but when I create my User and Book objects, they are not linked to each other, neither in the database nor in the code:

user = User.create(name="Rohit")

hp2 = Book.create(title="Harry Potter and the Chamber of Secrets")

user.books.append(hp2) # <-- exception here, no column books.user_book_id, and a HUGE SQL 
# statement with multiple repeated columns

for book in user.books:
    print(book)

I want to be able to say, e.g., user.books[0].rating, which will give me the rating the user put on the first book in his collection.

Here's the dump of the SQL statement, in which you'll see the same column repeated multiple times:

user.books

An exception was raised: no such column: books.user_book_id (SQL: SELECT "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id", "books".*, "user_books"."book_id", "books".*, 
"user_books"."book_id", "books".*, "user_books"."book_id", "books".*, "user_books"."book_id", 
"books".*, "user_books"."book_id" FROM "books" INNER JOIN "user_books" ON "user_books"."id" = "books"."user_book_id"
WHERE "user_books"."book_id" = ? ([2]))

(In the future there will be other many-to-many relationships like books to authors, but that'll be simpler, I'm expecting!)

Could you please help resolve this issue?

Thank you!

Rohit

Hi Rohit,

I'm not a contributor to this project, but I came across your question when I was reporting a bug in one of the methods related to many to many relationships. I used Orator in a few projects now and I'm also really enthusiastic about it!

Why are you making a model for the user-book relationship? It seems that the relationship itself is not complex enough that it needs to have its own model. You could use a regular many-to-many relationship and add additional attributes to the pivot table. When following the link, check out the second code block. In your case it could be as simple ass user.books().attach(3, {'purchased_on': date, ...}). The data can be queried by accessing the pivot table.

Also, the example from the docs for the has-many-through relationship uses the following order:
Country (one) - User (many) - User (one) - Post (many)
Notice that Post has only one User (the linking entity). In your case a Book can have many UserBooks.
User(one) - UserBook (many) - UserBook (many) - Book(one)
I haven't worked with the has-many-through relationship of Orator before, but it could be that your use case is not supported by this type of relationship.

So I would suggest to use pivot tables!