Here are the relevant facts:
Each topic has_many comments.
The Comment model has a scope called very_popular, which we'll pretend involves comparing a several of its columns.
def self.very_popular
# lots of cool stuff
end
The Topic model has a scope called exciting, which includes all topics with very_popular comments.
Number 3 is where I'm stuck. The following results in a missing method exception, and as pitiful as it sounds, I don't know what else to try!
def self.exciting
join(:comments).very_popular
end
How can I re-use the very_popular scope from the Comment model in the Topic model's scope?
You can't use the scope from another model directly. What you can do is merge the queries.
Topic.joins(:comments).merge(Comment.very_popular)
Ryan explains it beautifully here: http://railscasts.com/episodes/215-advanced-queries-in-rails-3
Related
I have multiple models that in practice are created and deleted together.
Basically I have an Article model and an Authorship model. Authorships link the many to many relation between Users and Articles. When an Article is created, the corresponding Authorships are also created. Right now, this is being achieved by POSTing multiple times.
However, say only part of my request works. For instance, I'm on bad wifi and only the create article request makes it through. Then my data is in a malformed half created, half not state.
To solve this, I want to send all the data at once, then have Rails split up the data into the corresponding controllers. I've thought of a couple ways to do this. The first way is having controllers handle each request in turn, sort of chaining them together. This would require the controllers to call the next one in the chain. However, this seems sorta rigid because if I decide to compose the controllers in a different way, I'll have to actually modify the controller code itself.
The second way splits up the data first, then calls the controller actions with each bit of data. This way seems more clean to me, but it requires some logic either in the routing or in a layer independent of the controllers. I'm not really clear where this logic should go (another controller? Router? Middleware?)
Has anybody had experience with either method? Is there an even better way?
Thanks,
Nicholas
Typically you want to do stuff like this -- creating associated records on object creation -- all in the same transaction. I would definitely not consider breaking up the creation of an Authorship and Article if creating an Authorship is automatic on Article creation. You want a single request that takes in all needed parameters to create an Article and its associated Authorship, then you create both in the same transaction. One way would be to do something like this in the controller:
class Authorship
belongs_to :user
belongs_to :article
end
class Article
has_many :authorships
has_many :users, through: :authorships
end
class ArticlesController
def create
#article = Article.new({title: params[:title], stuff: [:stuff]...})
#article.authorships.build(article: #article, user_id: params[:user_id])
if #article.save
then do stuff...
end
end
end
This way when you hit #article.save, the processing of both the Article and the Authorship are part of the same transaction. So if something fails anywhere, then the whole thing fails, and you don't end up with stray/disparate/inconsistent data.
If you want to assign multiple authorships on the endpoint (i.e. you take in multiple user id params) then the last bit could become something like:
class ArticlesController
def create
#article = Article.new({title: params[:title], stuff: [:stuff]...})
params[:user_ids].each do |id|
#article.authorships.build(article: #article, user_id: id)
end
if #article.save
then do stuff...
end
end
end
You can also offload this kind of associated object creation into the model via a virtual attribute and a before_save or before_create callback, which would also be transactional. But the above idiom seems more typical.
I would handle this in the model with one request. If you have a has_many relationship between Article and Author, you may be able to use accept_nested_attributes_for on your Article model. Then you can pass Authorship attributes along with your Article attributes in one request.
I have not seen your code, but you can do something like this:
model/article.rb
class Article < ActiveRecord::Base
has_many :authors, through: :authorship # you may also need a class_name: param
accepts_nested_attributes_for: :authors
end
You can then pass Author attributes to the Article model and Rails will create/update the Authors as required.
Here is a good blog post on accepts_nested_attributes_for. You can read about it in the official Rails documentation.
I would recommend taking advantage of nested attributes and the association methods Rails gives you to handle of this with one web request inside one controller action.
Imagine we have an Article and a Comment model. We setup our routes like:
# routes.rb
resources :articles do
resources :comments
end
Now, we can destroy a comment via the CommentController, but there are a number of approaches that I've seen been implemented.
# method 1
def destroy
Comment.where(article_id: params[:article_id]).find(params[:id]).destroy
end
# method 2
def destroy
Comment.find(params[:id]).destroy
end
# method 3
def destroy
article = Article.find(params[:article_id])
comment = article.comments.find(params[:id])
comment.destroy
end
Which is better and why?
I've seen in old Railscasts episodes and blogs that we should do the former for "security" reasons or because it is better to ensure that comments can only be found within their respective article, but why is that better? I haven't been able to find anything that goes too deep into the answer.
When you are working with nested data in this way, it's better to scope your model lookup under the parent to avoid people iterating through ids in a simplistic way. You generally want to discourage that, and it will protect you against more serious security problems if it's a habit.
For instance, say you have some sort of visibility permission on Article. With method 2, it is possible to use an article_id that you are allowed to see to access Comments that you aren't.
Methods 1 & 3 are ostensibly doing the same thing, but I would prefer 1 because it uses fewer trips to the DB.
As the title explains, say I have two ActiveRecord::Base models: SatStudentAnswer and ActStudentAnswer.
I have a Student model
has_many :act_student_answers
has_many :sat_student_answers
I would like to create a collection proxy that concats them together so I can query all student answers together.
student.sat_student_answers.concat(student.act_student_answers)
However I get an
ActiveRecord::AssociationTypeMismatch: SatStudentAnswer(#70111600189260) expected, got ActStudentAnswer(#70111589566060) error.
Is there a way to create a collection proxy with two different models so I can continue using the Active Record query interface? If not, what would be the best way to solve this problem?
Thank you for the help!
As far as I know, there isn't way to create Rails AR:Relation with different models in it (I digged in this issue when I needed to create newsfeed with different kind of posts). My solution wasn't very beautiful, but was working: I've created additional model with polymorphic has_many. You could query collection of these new defined models and distinguish sat_answers and act_answers by field answer_type.
You can do it like this
array = student.sat_student_answers + student.act_student_answers
Then for example
array.each {|item| p item.student}
It seem like you actually want the two models to be just one model categorized into two things act and sat.
class Student < ActiveRecord::Base
# ...
has_many :answers
# ...
end
class Answer < ActiveRecord::Base
# ...
scope :act, -> { where kind: :act }
scope :sat, -> { where kind: :sat }
# ...
end
Then you'll be able to do your concat.
student.answers.act.concat student.answers.sat
# that is similar to
student.answers.act << student.answers.sat
However, concat methods adds the records to the receiving association and saves it in the DB if the owner of the association is persisted. I'm not really sure what you're trying to do there. Are you mixing the ACT answers with SAT answers making SAT answers include the ACT answers?
See: http://apidock.com/rails/ActiveRecord/Associations/CollectionAssociation/concat
What you probably want is below:
student.answers
# or
act_and_sat_answers = student.answers.where(kind: [:act, :sat])
# chain more filter query
act_and_sat_answers.where(correct: true)
If you really want to use two distinct models then you'll have to create your own collection proxy.
I have an ActiveRecord model called Books which has a has_one association on authors and a has_many association on publishers. So the following code is all good
books.publishers
Now I have another AR model, digital_publishers which is similar but which I would like to transparently use if the book's author responds to digital? - let me explain with some code
normal_book = Book.find(1)
normal_book.author.digital? #=> false
normal_book.publishers #=> [Publisher1, Publisher2, ...]
digital_book = Book.find(2)
digital_book.digital? #=> true
digital_book.publishers #=> I want to use the DigitalPublishers class here
So if the book's author is digital (the author is set through a has_one :author association so it's not as simple as having a has_many with a SQL condition on the books table), I still want to be able to call .publishers on it, but have that return a list of DigitalPublishers, so I want some condition on my has_many association that first checks if the book is digital, and if it is, use the DigitalPublishers class instead of the Publishers class.
I tried using an after_find callback using the following code:
after_find :alias_digital_publisher
def alias_digital_publisher
if self.author.digital?
def publishers
return self.digital_publishers
end
end
end
But this didn't seem to do the trick. Also, I'm using Rails 2.3.
I need more information about the project to really make a recommendation, but here are some thoughts to consider:
1. Publishers shouldn't belong to books
I'm guessing a publisher may be linked to more than one book, so it doesn't make a lot of sense that they belong_to books. I would consider
#Book.rb
has_many :publishers, :through=>:publications
2. Store digital publishers in the publishers table
Either use Single Table Inheritance (STI) for digital publishers with a type column of DigitalPublisher, or just add a boolean indicating whether a publisher is digital.
This way you can just call book.publishers, and you would get publishers that may or may not be digital, depending on which were assigned.
The trick is that you would need to ensure that only digital publishers are assigned to books with a digital author. This makes sense to me though.
3. (alternatively) Add a method for publishers
def book_publishers
author.digital? ? digital_publishers : publshers
end
I'm not really a fan of this option, I think you're better off having all the publishers in one table.
Have a look at this section from Rails Guides v-2.3.11. In particular note the following:
The after_initialize and after_find callbacks are a bit different from the others. They have no before_* counterparts, and the only way to register them is by defining them as regular methods. If you try to register after_initialize or after_find using macro-style class methods, they will just be ignored.
Basically, try defining your after_find as
def after_find
...
end
If that doesn't work, it might be because the book's fields haven't been initialized, so try after_initialize instead.
My solution is very simple.
class Book < ActiveRecord::Base
has_many :normal_publishers, :class_name => 'Publisher'
has_many :digital_publishers, :class_name => 'DigitalPublisher'
def publishers
if self.digital?
self.digital_publishers
else
self.normal_publishers
end
end
end
You can still chain some methods, like book.publishers.count, book.publishers.find(...).
If you need book.publisher_ids, book.publisher_ids=, book.publishers=, you can define these methods like book.publishers.
The above code works on Rails 2.3.12.
UPDATE: Sorry, I noticed klochner's alternate solution after I had posted this.
In RoR, it is pretty common mistake for new people to load a class and assiocations like this# the solution to eager load
# The bellow generates an insane amount of queries
# post has many comments
# If you have 10 posts with 5 comments each
# this will run 11 queries
posts = Post.find(:all)
posts.each do |post|
post.comments
end
The solution is pretty simple to eager load
# should be 2 queries
# no matter how many posts you have
posts = Post.find(:all, :include => :comments) # runs a query to get all the comments for all the posts
posts.each do |post|
post.comments # runs a query to get the comments for that post
end
But what if you don't have access to the class methods, and only have access to a collection of instance methods.
Then you are stuck with the query intensive lazy loading.
Is there a way to minimize queries to get all the comments for the collection of posts, from the collection of instances?
Addition for Answer (also added to the code above)
So to eager load from what I can see in the rdoc for rails is a class method on any extension of ActiveRecord::Associations, the problem is say you you don't have the ability to use a class method, so you need to use some sort of instance method
a code example of what I think it would look like would be is something like
post = Posts.find(:all)
posts.get_all(:comments) # runs the query to build comments into each post without the class method.
In Rails 3.0 and earlier you can do:
Post.send :preload_associations, posts, :comments
You can pass arrays or hashes of association names like you can to include:
Post.send :preload_associations, posts, :comments => :users
In Rails 3.1 this has been moved and you use the Preloader like this:
ActiveRecord::Associations::Preloader.new(posts, :comments).run()
And since Rails 4 its invocation has changed to:
ActiveRecord::Associations::Preloader.new.preload(posts, :comments)
I think I get what you're asking.
However, I don't think you have to worry about what methods you have access to. The foreign key relationship (and the ActiveRecord associations, such as has_many, belongs_to, etc.) will take care of figuring out how to load the associated records.
If you can provide a specific example of what you think should happen, and actual code that isn't working, it would be easier to see what you're getting at.
How are you obtaining your collection of model instances, and what version of Rails are you using?
Are you saying that you have absolutely no access to either controllers or models themselves?
giving you the best answer depends on knowing those things.