This is a bit of a long question, but to those of you with some experience, it should be fairly simple I think. I have a notes page that should list all notes the user has entered for all books chronologically like this. Book One: This is the note. Book Four: This is the note. Book One: A different note, entered later. Right now my models are: User has_many :books Book has_many :notes belongs_to :user Note (has column book_id) belongs_to :book has_one :book The NotesController has: @books = current_user.books.find(:all) @notes = @books.notes In my views/notes/list.rhtml I can get all the notes fine, but what I can''t get is what book they belong to. <% for note in @notes %> Doesn''t work: <%= note.book.title %> Works: <%= note.description %> I can''t figure out a clean way to display which book a note belongs to. Any help on correctly setting up my models or how the controller should work is much appreciated. -- Posted via http://www.ruby-forum.com/.
I see a couple newbie errors... 1) I don''t think you should have both a belongs_to and has_one from Note to Book - if Book has_many :notes, then Note belongs_to :book and that''s all you need. Having both is probably what''s messing up the list view (among other things). 2) @books.notes isn''t going to work. You need to either enumerate the @books collection and get the notes from each book (an inject block is a good option for that), or craft a special query to gather all the notes for a user''s books. If you want it to look pretty and are using Edge Rails (or soon, 1.1), you can put it in an association extention so you can use the books.notes syntax. --josh http://blog.hasmanythrough.com Marcus Vorwaller wrote:> This is a bit of a long question, but to those of you with some > experience, it should be fairly simple I think. > > I have a notes page that should list all notes the user has entered for > all books chronologically like this. > Book One: This is the note. > Book Four: This is the note. > Book One: A different note, entered later. > > Right now my models are: > User > has_many :books > Book > has_many :notes > belongs_to :user > Note (has column book_id) > belongs_to :book > has_one :book > > The NotesController has: > @books = current_user.books.find(:all) > @notes = @books.notes > > In my views/notes/list.rhtml I can get all the notes fine, but what I > can''t get is what book they belong to. > <% for note in @notes %> > Doesn''t work: <%= note.book.title %> > Works: <%= note.description %> > > I can''t figure out a clean way to display which book a note belongs to. > > Any help on correctly setting up my models or how the controller should > work is much appreciated.-- Posted via http://www.ruby-forum.com/.
Marcus - I didn''t know you could do such an assignment: @notes = @books.notes Since "@books" is a collection of book objects, and each "book" in "@books" should have a collection of "notes", this seems invalid. Can you please clarify? On 3/4/06, Marcus Vorwaller <vorwaller@gmail.com> wrote:> This is a bit of a long question, but to those of you with some > experience, it should be fairly simple I think. > > I have a notes page that should list all notes the user has entered for > all books chronologically like this. > Book One: This is the note. > Book Four: This is the note. > Book One: A different note, entered later. > > Right now my models are: > User > has_many :books > Book > has_many :notes > belongs_to :user > Note (has column book_id) > belongs_to :book > has_one :book > > The NotesController has: > @books = current_user.books.find(:all) > @notes = @books.notes > > In my views/notes/list.rhtml I can get all the notes fine, but what I > can''t get is what book they belong to. > <% for note in @notes %> > Doesn''t work: <%= note.book.title %> > Works: <%= note.description %> > > I can''t figure out a clean way to display which book a note belongs to. > > Any help on correctly setting up my models or how the controller should > work is much appreciated. > > -- > Posted via http://www.ruby-forum.com/. > _______________________________________________ > Rails mailing list > Rails@lists.rubyonrails.org > http://lists.rubyonrails.org/mailman/listinfo/rails >
Marcus Vorwaller wrote:> This is a bit of a long question, but to those of you with some > experience, it should be fairly simple I think.This is a bit of a long reply, but I tried to keep my solution as close to the "Rails-way" as I could. :)> I have a notes page that should list all notes the user has entered for > all books chronologically like this. > Book One: This is the note. > Book Four: This is the note. > Book One: A different note, entered later.Hmm... that''s kind of a strange way to display the notes that user has entered, but it''s not too difficult to get what you want.> Right now my models are: > User > has_many :books > Book > has_many :notes > belongs_to :user > Note (has column book_id) > belongs_to :book > has_one :bookAs Josh mentioned in his reply, you have to remove the "has_one :book" from the Note model class. Since there''s no note_id column in the books table, the association you''re specifying is invalid.> The NotesController has: > @books = current_user.books.find(:all) > @notes = @books.notesAlso, as Josh mentioned, @books is an array, it won''t have a notes method for you to access. Second, you don''t need to do a find on the current_user.books, since you want all of their books. So that line can just be: @books = current_user.books I would also recommend that when you do the find to set current_user, that you add an ":include => :books" option to the find, so that you get the books in the first SQL call. But you don''t have to do that for "current_user.books" to get you the @books collection. Finally, each item in the @books array, which is a book model item, will have a notes method, but you don''t actually want to loop over them, because those notes won''t be in the order that you specified above. They will be tied to the order of the books that were found. To get the notes in a special order you need to perform a find on the Note class, so you can specify a sort order for the notes. Here''s how I would probably do it, assume these lines of code come after my revised @books statement: book_ids = @books.collect { |b| b.id} # this gets you a list of book ids @notes = Note.find( :all, :include => :book, :order => "notes.created_on", :conditions => ["notes.book_id in ?", book_ids]) What this Find does, it bring back all the notes for the current user, since we only find the notes for the current user''s books, and sorts them by the date/time each note was created. In addition, it tells ActiveRecord to also grab the book information for each note found.> In my views/notes/list.rhtml I can get all the notes fine, but what I > can''t get is what book they belong to. > <% for note in @notes %> > Doesn''t work: <%= note.book.title %> > Works: <%= note.description %> > > I can''t figure out a clean way to display which book a note belongs to.Now, using the @notes collection returned by the custom Find, you should be able to use your views/notes/list.rhtml template as you have it defined above. There are probably other ways to arrive at the same thing, but what I put above is definitely how I would go about getting the notes for a given user, in their creation order, with each note''s associated book info. It''s not the most efficient way of working with your model, since it means duplicating the book info for every book that has more than one note, but it will give you the result you said you were looking for... -Brian
Brian V. Hughes wrote:> book_ids = @books.collect { |b| b.id} # this gets you a list of book ids > @notes = Note.find( :all, > :include => :book, > :order => "notes.created_on", > :conditions => ["notes.book_id in ?", book_ids])Nice response, Brian. Better than I was able to do with my groggy brain this morning :-) But I''d suggest a different approach to yours. What you are doing is essentially a manual join, and I''d prefer to let the database do that for me. It''s safer, as with your approach you could have a stale list of book ids that is no longer valid by the time you run the notes query. Try this instead: In User class... def notes Notes.find(:all, :select => ''notes.*'', :joins => ''INNER JOIN books ON books.id = notes.book_id'', :conditions => "books.user_id = #{self.id}", :readonly => false) end Of course, the simplest way to do this is to use Edge Rails has_many :through associations (coming soon in 1.1): class User < ActiveRecord::Base has_many :books has_many :notes, :through => :books, :order => "created_at" Then you can just say @user.notes and be done with it. --josh http://blog.hasmanythrough.com -- Posted via http://www.ruby-forum.com/.
Josh Susser wrote:> Of course, the simplest way to do this is to use Edge Rails has_many > :through associations (coming soon in 1.1): > > class User < ActiveRecord::Base > has_many :books > has_many :notes, :through => :books, :order => "created_at" > > Then you can just say @user.notes and be done with it.Oops, forget about that. You can''t use :through in this case, as the association from books to notes is backwards from what you need for :through. My bad. --josh -- Posted via http://www.ruby-forum.com/.
Wow! Thanks to both of you for your help. It''s great to come back to such helpful responses. I tried both methods--both worked. In the end, I changed my User model as Josh suggested. It''s funny you should mention the :through method in edge. I upgraded a couple days ago hoping that would solve the problem but obviously it didn''t work in this instance :) Again, thank you both for your responses--I learned a lot from both methods. -Marcus Josh Susser wrote:> Josh Susser wrote: >> Of course, the simplest way to do this is to use Edge Rails has_many >> :through associations (coming soon in 1.1): >> >> class User < ActiveRecord::Base >> has_many :books >> has_many :notes, :through => :books, :order => "created_at" >> >> Then you can just say @user.notes and be done with it. > > Oops, forget about that. You can''t use :through in this case, as the > association from books to notes is backwards from what you need for > :through. My bad. > > --josh-- Posted via http://www.ruby-forum.com/.
Josh Susser wrote:> In User class... > def notes > Notes.find(:all, > :select => ''notes.*'', > :joins => ''INNER JOIN books ON books.id = notes.book_id'', > :conditions => "books.user_id = #{self.id}", > :readonly => false) > endI like the idea of making this a method of the User model class, but what you''ve done here is chop up what would normally be a find_by_sql, so that it sort of reads like a normal ActiveRecord find. Especially with using both :select (not sure why, actually) and :join. But I actually think what you''ve got (while it might be technically "more correct" than my solution) is a good deal more cumbersome than the style that I used. It also forces the programmer to have to know that an inner join, not an outer join, is the "right" way to do the find. That kind of database detail is what you should use :include to handle for you. In fact, I think your method and my method can be combined to produce a better looking, and reading, solution to the problem: def notes Notes.find(:all, :order => "notes.created_on", :include => :book, :conditions => ["books.user_id = ?", self.id], :readonly => false) end Won''t this find do the same as yours, and be easier to maintain from a Rails standpoint? Also, I''m not sure that you actually need to specify the :readonly option, especially since you have it set to false, which is its default value. -Brian
Brian V. Hughes wrote:> Josh Susser wrote: >> In User class... >> def notes >> Notes.find(:all, >> :select => ''notes.*'', >> :joins => ''INNER JOIN books ON books.id = notes.book_id'', >> :conditions => "books.user_id = #{self.id}", >> :readonly => false) >> end > > I like the idea of making this a method of the User model class, but > what you''ve > done here is chop up what would normally be a find_by_sql, so that it > sort of > reads like a normal ActiveRecord find. Especially with using both > :select (not > sure why, actually) and :join. But I actually think what you''ve got > (while it > might be technically "more correct" than my solution) is a good deal > more > cumbersome than the style that I used. > > It also forces the programmer to have to know that an inner join, not an > outer > join, is the "right" way to do the find. That kind of database detail is > what > you should use :include to handle for you. In fact, I think your method > and my > method can be combined to produce a better looking, and reading, > solution to the > problem: > > def notes > Notes.find(:all, > :order => "notes.created_on", > :include => :book, > :conditions => ["books.user_id = ?", self.id], > :readonly => false) > end > > Won''t this find do the same as yours, and be easier to maintain from a > Rails > standpoint? Also, I''m not sure that you actually need to specify the > :readonly > option, especially since you have it set to false, which is its default > value.I think it''s interesting how many ways there are to come at this. I did actually work out the find_by_sql approach first, but then opted for the "choppy" approach because it seemed more idiomatic for Rails. To address your questions (and I''m more than happy to hear if my reasoning is poor)... I opted for :joins instead of :includes because I don''t want to prefetch the book data, but only to get the notes. The :select option is to limit the returned fields to just those in the notes table. If you don''t do that, you''ll get all the joined fields too. The default value for :readonly is true when you set :joins because you normally get the joined fields back too and AR can''t write mixed records. You don''t need to muck with :readonly in your version as you''re using :includes instead of :joins. Hmm. I just had a bit of a brainstorm. I think this would all work out better with a slight refactoring of the data model. If you really care so much about finding a user''s notes, that relationship should be expressed as an association. So try turning a Note into a join model for User and Book using has_many :through. User has_many :notes has_many :books, :through :notes Note belongs_to :user belongs_to :book Book has_many :notes has_many :users, :through :books This way you get all the magic for free. Once again, my obsession with has_many :through pays off! --josh http://blog.hasmanythrough.com -- Posted via http://www.ruby-forum.com/.
Brian V. Hughes
2006-Mar-05 15:58 UTC
[Rails] Re: Re: Help setting up relationships needed
Josh Susser wrote:> I think it''s interesting how many ways there are to come at this. I did > actually work out the find_by_sql approach first, but then opted for the > "choppy" approach because it seemed more idiomatic for Rails. To address > your questions (and I''m more than happy to hear if my reasoning is > poor)...Wow... I was right about your find really being a find_by_sql call. Cool. :)> I opted for :joins instead of :includes because I don''t want to prefetch > the book data, but only to get the notes. The :select option is to limit > the returned fields to just those in the notes table. If you don''t do > that, you''ll get all the joined fields too.But the key thing the OP was looking for was a way to display notes for a users, in chronological order, with their associated book information. Which makes :include the perfect choice, IMO, since pre-fetching the book data for each note (while somewhat inefficient based on the model) is still likely to be more efficient than making the n+1 database calls.> The default value for :readonly is true when you set :joins because you > normally get the joined fields back too and AR can''t write mixed records. You > don''t need to muck with :readonly in your version as you''re using :includes > instead of :joins.Ah... OK. Now that makes sense.> Hmm. I just had a bit of a brainstorm. I think this would all work out > better with a slight refactoring of the data model. If you really care > so much about finding a user''s notes, that relationship should be > expressed as an association. So try turning a Note into a join model for > User and Book using has_many :through. > > User > has_many :notes > has_many :books, :through :notes > > Note > belongs_to :user > belongs_to :book > > Book > has_many :notes > has_many :users, :through :books > > This way you get all the magic for free. > > Once again, my obsession with has_many :through pays off!This is an interesting idea, but if a Book never has more than one User, then you''re doing a whole lot of additional database work for very little benefit. Even though has_many :through makes working with HABTM models a lot easier, shouldn''t you still be starting from a HABTM model? If not, this might be the first case I''ve seen of refactoring into a more complex database model... ;-> -Brian
Brian V. Hughes wrote:> But the key thing the OP was looking for was a way to display notes for a users, > in chronological order, with their associated book information. Which makes > :include the perfect choice, IMO, since pre-fetching the book data for each note > (while somewhat inefficient based on the model) is still likely to be more > efficient than making the n+1 database calls.True, that. For that purpose you''re absolutely right. We don''t know if there are other use cases where the access from users to notes doesn''t need the book information. If that never happens your :includes approach is a clear winner, otherwise it could end up being a bit of a dog.>> Note >> belongs_to :user >> belongs_to :book >> >> Book >> has_many :notes >> has_many :users, :through :notes # [corrected] > > This is an interesting idea, but if a Book never has more than one User, then > you''re doing a whole lot of additional database work for very little benefit. > Even though has_many :through makes working with HABTM models a lot easier, > shouldn''t you still be starting from a HABTM model? If not, this might be the > first case I''ve seen of refactoring into a more complex database model... ;->LOL. OK, we don''t know if Marcus'' system would want multiple users to be able to annotate the same book, but I see your point. By the way, nice discussion. --josh http://blog.hasmanythrough.com -- Posted via http://www.ruby-forum.com/.