I've got an ActiveAdmin index page
ActiveAdmin.register Bill
And I am trying to display links to associated models
index do
column "User" do |bill|
link_to bill.user.name, admin_user_path(bill.user)
end
end
But I run into the N+1 query problem - there's a query to fetch each user.
Is there a way of eager loading the bills' users?
The way to do this is to override the scoped_collection method (as noted in Jeff Ancel's answer) but call super to retain the existing scope. This way you retain any pagination/filtering which has been applied by ActiveAdmin, rather than starting from scratch.
ActiveAdmin.register Bill do
controller do
def scoped_collection
super.includes :user
end
end
index do
column "User" do |bill|
link_to bill.user.name, admin_user_path(bill.user)
end
end
end
As noted in official documentation at http://activeadmin.info/docs/2-resource-customization.html
There is an answer on a different post, but it describes well what you need to do here.
controller do
def scoped_collection
Bill.includes(:user)
end
end
Here, you will need to make sure you follow scope. So if your controller is scope_to'ed, then you will want to replace the model name above with the scope_to'ed param.
The existing answers were right at the time, but ActiveAdmin supports eager loading with a much more convenient syntax now:
ActiveAdmin.register Bill do
includes :user
end
See the docs for resource customization
IMPORTANT EDIT NOTE : what follows is actually false, see the comments for an explanation. However I leave this answer where it stands because it seems I'm not the only one to get confused by the guides, so maybe someone else will find it useful.
i assume that
class Bill < ActiveRecord::Base
belongs_to :user
end
so according to RoR guides it is already eager-loaded :
There’s no need to use :include for immediate associations – that is,
if you have Order belongs_to :customer, then the customer is
eager-loaded automatically when it’s needed.
you should check your SQL log if it's true (didn't know that myself, i was just verifying something about :include to answer you when i saw this... let me know)
I've found scoped_collection loads all the entries, instead of just the ones for the page you are displaying. I think a better option is apply_collection_decorator that will only preload the items you are effectively displaying.
controller do
def apply_collection_decorator(collection)
collection.includes(:user)
end
end
Related
I use Rails 3 with MongoMapper.
I want to add some records to the result of has many association.
For example, user has_many posts
class User
include MongoMapper::Document
many :posts
end
By default it will show only posts which belongs to the user, but if he/she specify special option in query (or in the user's settings menu, say show-commented=true), then I also need to add posts where user left any comments. So I think to override posts method
def posts
super + (show_commented_posts ? commented_posts : [])
end
But of course it doesn't work. How can I correctly override this method using mongo_mapper? Or is there any better approach for that problem?
Overriding methods on mongomapper is a very bad idea, you should try to refrain from doing it as it creates a lot of problems that are hard to trace back (I've been burned before by this).
Instead, you should consider using a scope such as
class Post
scope :related_to_user, lambda {|user| where('$or' => [ {user_id: user.id}, {'comments.user_id' => user.id}]) }
end
Then you can call
Post.related_to_user(current_user)
I have read a lot about soft deletes and archive and saw all the pros and cons. I'm still confused on which approach would work best for my situation. I'll use the concept of posts and comments to see if I can explain it a bit easier
Post -> Comments
Post.all
Outside RSS Feeds -> Post -> Comments
RSSFeed.posts (Return the ones that are deleted or not)
Post gets "deleted" but I need the posts still accessible from say an RSS Feed but not the admin of the application.
I hear a lot of headaches with soft deletes but think it might make the most sense for my application and feel if I use Archive then I would have to run multiple queries
RSSFeed.posts || RSSFeed.archived_posts
not sure which would be more efficient or more a pain in the #$$. Thoughts or examples? I know this example sounds stupid but trying to think of multiple situations that could be used to figure out which way to go.
Just add another column in your database and call it archivated.
Use link_to_if for the links:
<%= link_to_unless #post.archivated?, #post.name, post_path(#path) %>
Some more rails goodness:
app/models/post.rb
class Post < ActiveRecord::Base
default_scope where( active: true )
def archivate
unless self.archivated?
self.archivated = true
self.save
end
end
def dectivate
if self.archivated?
self.archivated = false
self.save
end
end
end
app/models/archive.rb
class Archive < Post
set_table_name :posts # make this model use the posts table
default_scope where( active: false )
end
Now you can do stuff like this:
#post = Post.find(some_id)
#post.archivate
Archive.find(some_id) # should return the post you just archivated
Definitely you will get idea , take a look :
http://railspikes.com/2010/2/26/acts-as-archive
tl;dr: Is it possible to intercept posted values from a nested model for further processing? I've tried everything I can think of to access the nested attributes to use for a before_save callback, but that may be only a testament to the limits of my imagination.
I'm writing a library application, where books can have many authors and vice versa. That bit is working just fine with a has_many_through and accepts_nested_attributes_for. The app saves all the book and author information just fine. Except... I can't seem to write a working before_save on either the Author or Book model to check if the Author that we're trying to create exists. Here's what I already have:
class Book < ActiveRecord::Base
has_many :authorships
has_many :authors, :through => :authorships
accepts_nested_attributes_for :authors, :authorships
end
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, :through => :authorships
before_save :determine_distinct_author
def determine_distinct_author
Author.find_or_create_by_author_last( #author )
end
end
## books_controller.rb ##
def new
#book = Book.new
#book.authors.build
respond_to do |format|
format.html
format.xml { render :xml => #book }
end
end
def create
#book = Book.new(params[:book])
#author = #book.authors #I know this line is wrong. This is the problem (I think.)
# more here, obviously, but nothing out of ordinary
end
When I post the form, the dev log passes on this:
Parameters: {"commit"=>"Create Book",\
"authenticity_token"=>"/K4/xATm7eGq/fOmrQHKyYQSKxL9zlVM8aqZrSbLNC4=",\
"utf8"=>"✓", "book"{"title"=>"Test", "ISBN"=>"", "genre_id"=>"1", "notes"=>"", \
"authors_attributes"=>{"0"{"author_last"=>"McKinney", "author_first"=>"Jack"}}, \
"publisher"=>"", "pages"=>"", "language_id"=>"1", "location_id"=>"1"}}
So... the data's there. But how do I get to it to process it? When I post the data, here's that log:
Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE\
`authors`.`author_last` IS NULL LIMIT 1
I've done a fair bit of searching around to see what there is out there on nested attributes, and I see a lot on the form side, but not much on how to get to the assets once they're submitted.
Any explanation of why the solution which works does actually work would also be appreciated. Thanks.
First, this line
#author = #book.authors
assigns an array of authors to #author, but in your before_save you seem to be expecting a single author model. Secondly, the #author variable is scoped to the controller and will be empty in your model's scope.
Remove that line and try this in your Book class (not tested):
def determine_distinct_author
authors.each do |author|
Author.find_or_create_by_author_last( author )
end
end
1) The nested attributes is an array, and should be accessed as an array. Getting the submission from the one model to the next still presents quite a problem.
2) accepts_nested_attributes_for is probably not going to help out a whole lot, and there will undoubtedly be a lot of custom processing which will have to happen between now and when the whole system is fully functional. Probably best to ditch it now and move on.
In that direction: It looks like Ryan Bates has done a recent bit on the jQuery TokenInput plugin which will take care of a lot of the autocomplete feature on the author. More to the larger problem (and a bit harder to find), is a follow up that he posted to some issues with the plugin on working with new entries.
I am not quite sure what you are trying to accomplish, but inside your Author you have a before_save callback, and that is obviously giving the error.
To me it seems you are looking for an author with the given name, and then want to use that? Is that correct?
Using a nested form, you will always create a new author, unless you want to use something like a select-box, and then select an author-id (which is part of the book, so not nested), instead of the authors details.
UX-wise, i would offer the option to either select an existing author (use an autocomplete field), or create a new one (using the nested fields option --without your before_save callback).
I've got a nice 3-level nested-form using formtastic_cocoon (jquery), and now I want to be able to sort the 2nd set of items in the form.
I've got jQuery ui working no problem, so now to set and update the sort order in rails.
I started following the rails bits of railscasts sortable lists
http://asciicasts.com/episodes/147-sortable-lists
The form structure is User->Tasks->Location.
In my Task Model I set index to
def index
#task = Task.find(params[:id],:order=>'position')
end
def edit
#task = Task.find(params[:id],:order=>'position')
end
and I was expecting my console to see
... FROM 'tasks' WHERE ('users'.'id' = 12) ORDER BY ('position')
or something along those lines, but there is no order by output.
Is there somewhere else that I need to define this order?? Where does the nested_object get its relationship from? The model only?
My models are
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :user
end
I changed the model to
class User < ActiveRecord::Base
has_many :tasks, :order=>'position'
end
You are using the wrong model, that id is of a user, so you have to do:
User.find(params[:id]).tasks(:order => 'position ASC')
Otherwise you are just getting the task with id = 12 and not the tasks whose user_id = 12
From the answer that you've given, I suspect the real issue lies not in the tasks controller. The default order you gave is great, but if you had some other order or filter requirements, the tasks model won't quite do it either.
I suspect you were actually in users#edit, or possibly a _form.html.erb, where it displays the form elements for each task. There might have been a #user.tasks.each {...} or similar loop block.
For a given user then, do: #user.tasks.order(:position). Or maybe you need open tasks: #user.tasks.where(:open=>true) etc.
Your code is slightly wrong here.
To find that user's tasks you would do this in your route:
User.find(params[:id]).tasks(:order=>'position')
I have a post controller that has many comments.
The post model has a field called has_comments which is a boolean (so I can quickly select from the database only posts that have comments).
To create a new comment for a post, I use the create action of my comments controller.
After I create the comment I need to update my post's has_comments field and set it to true.
I can update this field from the create action of my comments controller, but that doesn't seem right - I feel that I should really be using the post's update action, but I'm not sure if it's right to call it (via send?) from the create action of the comments controller.
Where should the code for updating the post be?
Thank you!
Why clutter your database with another column when the interface is programmatic? Make has_comments a method on Post:
def has_comments
comments.size > 0
end
Then implement a counter_cache as suggested to reduce the query load.
EDIT: Alternatively, after implementing a counter cache, you could use a named_scope on Post to retrieve all posts that have comments, using a single query, if that's the main goal:
class Comment
belongs_to :post, :counter_cache => true
end
class Post
named_scope :with_comments, {:conditions=>"comments_count > 0"}
end
EDIT: You can also avoid the famous n+1 query problem by a judicious use of :include:
posts = Post.find(:all, :include => :comments)
You could use before_save callback in your model.
Even better way would be to use built in :counter_cache option which automatically caches the number of comments for each post. (see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001835 under options for belongs_to)
use after_save in comment model
def after_save
#this will set "has_comment" of the Specified Post to true if it's not true already
self.post.update_attribute('has_comment', true) unless self.post.has_comment
end