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!