How can I optimize active_admin - ruby-on-rails

Last time I got the problem with active_admin. In tables where I have 5000+ rows of data it's work very slowly. How I can optimize it? Maybe somebody know some async load plugins for this module?

There are a couple things you can do.
By default, Active Admin loads associations as drop-down filters on the index page. If those filters aren't being used, it helps to remove them because they instantiate every record of that model to build the drop-down.
ActiveAdmin.register Post do
remove_filter :categories
end
If your index page has columns that depend on associated records, it helps to eager-load them.
ActiveAdmin.register Post do
controller do
def scoped_collection
super.includes :author, :publisher
end
end
end
This doesn't really apply since you only have 5000 records, but if you get to the point where even a DB COUNT of the table takes a long time, you might want to disable the count in the bottom right of the index page. (this feature was added in 0.6.1)
ActiveAdmin.register Post do
index pagination_total: false
end

Related

How can I filter ActiveAdmin index listings by a join table?

Let's say I have a table posts, and another table reviews that has a post_id and a rating (integer).
How can I add a filter to app/admin/post.rb which returns posts with a certain total score? (e.g. SUM(reviews.rating) GROUP BY (posts.id)). I want the filter to show up on the right side of the index, along with the other filters, and ideally to function as a range input.
To be clear, when I say "filter", I mean the ActiveAdmin filter method which adds filters to the right sidebar on the index page.
I created a scope in Post which returns posts with scores, but I haven't been able to find a way to use that in an ActiveAdmin filter.
Note: I rewrote my example because my original one didn't capture the complexity of the question.
It's common to override scoped_collection to join associated records to increase performance:
ActiveAdmin.register Post do
controller do
def scoped_collection
super.includes :author, :publisher
end
end
end
Since the entire collection now has author and publisher included, you can have a scope that queries those:
scope :random_house do |scope|
scope.where publishers: {name: 'Random House'}
end
I haven't come up with a proper solution to this question, but I have found a workaround.
I can change the scoped_collection based on a query param, and simply pass the param in when I want to use it. For example, if I have a scope with_rating(x), which returns posts with a score of at least x, I can write:
controller do
def scoped_collection
if params[:with_rating]
super.with_rating(params[:with_rating])
else
super
end
end
end
Then I can go to /admin/posts?with_rating=100 and get back posts with a rating of at least 100.
Thanks to #seanlinsley for making me aware of the scoped_collection method that I used in this solution.
Use counter cache column to store the comments count
http://railscasts.com/episodes/23-counter-cache-column
Then the column will get updated each time a comment is created to that post.This would also help in increasing the performance of search.

ActiveAdmin automatically loading full association table

I'm working on a project that uses ActiveAdmin for its administration backend.
I have two models, a Book model which has_many Products. When I try to access the products index view in ActiveAdmin, it seems to try to load the full books table into memory (there are about 1.5 million books in my database). CPU usage goes up to 100% and memory usage spikes to gigabytes.
Turning on mysql logging confirms that this is what happens when this view is called:
17 Query SELECT `books`.* FROM `books`
As far as I can tell this happens before any attempt to load the products.
To figure out this issue I stripped the models down to their bare bones:
class Product < ActiveRecord::Base
belongs_to :book
end
class Book < ActiveRecord::Base
has_many :products
end
I also reduced the AA definition to its most basic form:
ActiveAdmin.register Product do
end
Is this normal for ActiveAdmin? It doesn't seem like desirable behavior.
For anyone dealing with this same issue, I finally traced it to the automatically generated sidebar in ActiveAdmin. This includes a search field that includes a select box for all associated records.
If you have an associated table with over a million records like I do AA will happily attempt to insert the entire table into the select box.
The answer was to include some custom filters in the AA definition for products like so:
ActiveAdmin.register Product do
filter :title
end
That way the association won't be included (unless you specify it yourself.)
A better approach now is to use remove_filter for the particular attribute or relationship:
Or you can also remove a filter and still preserve the default
filters:
preserve_default_filters!
remove_filter :id
https://activeadmin.info/3-index-pages.html

Ruby on Rails Active Admin - Duplicate Records showing for HABTM

I am designing a basic file manager (the Asset model) in the Active Admin gem. Each Asset HABTM Groups, and vice-versa.
In my active_admin Asset resource I have a filter where I want to be able to
select multiple groups to filter by, so I added:
filter :groups_id, :as => :check_boxes, :collection => proc {Group.all}
All of the groups show up as checkboxes as expected. However, if I have asset_1, asset_2 and I have group_1 assigned to asset_1 and asset_2, and group_2 to asset_2, when I
filter by both roles, asset_2 lists itself twice.
How can I restrict the filter to use only "distinct" or "unique" assets to be returned?
I also have another problem, which is that the filters are not working at all in any of my scopes.
A quick update on Will's answer. I'm running Rails 5.0 and ActiveAdmin 1.0, and clean_search_params returned an error. But this worked instead:
def apply_filtering(chain)
super
#search.result(distinct: true)
end
Thanks!
Active admin read indicates to add
distinct: true
to get unique results.
To apply that to active admin, I'm using doing that like this:
controller do
def apply_filtering(chain)
#search = chain.ransack clean_search_params params[:q]
#search.result(distinct: true)
end
end
has_and_belongs_to_many accepts a :uniq option which ensures that only uniq records will be returned. Setting this in your model should do the trick.
class MyModel
has_and_belongs_to_many :things, :uniq => true
end
... and, a quick addition Alex's answer:
If you want to do this for all controllers in your app, you can add this to an initializer (mine's called active_admin_patches.rb) -
# This guarantees that the results for a filtered #index page search do not appear more than once, on any #index page in the AA app
# TODO: THIS WILL PROBABLY FAIL WITH SOME FUTURE UPDATE, SO BE READY TO UPDATE IT FROM THE LATEST GEM SOURCE
module ActiveAdmin::ResourceController::DataAccess
# Applies any Ransack search methods to the currently scoped collection.
# Both `search` and `ransack` are provided, but we use `ransack` to prevent conflicts.
def apply_filtering(chain)
#search = chain.ransack(params[:q] || {})
# This is the original line
# #search.result
# This is the patch
#search.result(distinct: true)
end
end
I'm not sure why anybody wouldn't want this to be the default behavior, but there's probably a reason. Hmm, maybe for cases where a column of the index view is one of the non-distinct rows. Yeah, that must be it.
Also, there's bound to be a better way to patch this less intrusively, but I'm in a hurry. :-)

Eager loading associated models in ActiveAdmin sql query

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

N+1 problem in mongoid

I'm using Mongoid to work with MongoDB in Rails.
What I'm looking for is something like active record include. Currently I failed to find such method in mongoid orm.
Anybody know how to solve this problem in mongoid or perhaps in mongomapper, which is known as another good alternative.
Now that some time has passed, Mongoid has indeed added support for this. See the "Eager Loading" section here:
http://docs.mongodb.org/ecosystem/tutorial/ruby-mongoid-tutorial/#eager-loading
Band.includes(:albums).each do |band|
p band.albums.first.name # Does not hit the database again.
end
I'd like to point out:
Rails' :include does not do a join
SQL and Mongo both need eager loading.
The N+1 problem happens in this type of scenario (query generated inside of loop):
.
<% #posts.each do |post| %>
<% post.comments.each do |comment| %>
<%= comment.title %>
<% end %>
<% end %>
Looks like the link that #amrnt posted was merged into Mongoid.
Update: it's been two years since I posted this answer and things have changed. See tybro0103's answer for details.
Old Answer
Based on the documentation of both drivers, neither of them supports what you're looking for. Probably because it wouldn't solve anything.
The :include functionality of ActiveRecord solves the N+1 problem for SQL databases. By telling ActiveRecord which related tables to include, it can build a single SQL query, by using JOIN statements. This will result in a single database call, regardless of the amount of tables you want to query.
MongoDB only allows you to query a single collection at a time. It doesn't support anything like a JOIN. So even if you could tell Mongoid which other collections it has to include, it would still have to perform a separate query for each additional collection.
Although the other answers are correct, in current versions of Mongoid the includes method is the best way to achieve the desired results. In previous versions where includes was not available I have found a way to get rid of the n+1 issue and thought it was worth mentioning.
In my case it was an n+2 issue.
class Judge
include Mongoid::Document
belongs_to :user
belongs_to :photo
def as_json(options={})
{
id: _id,
photo: photo,
user: user
}
end
end
class User
include Mongoid::Document
has_one :judge
end
class Photo
include Mongoid::Document
has_one :judge
end
controller action:
def index
#judges = Judge.where(:user_id.exists => true)
respond_with #judges
end
This as_json response results in an n+2 query issue from the Judge record. in my case giving the dev server a response time of:
Completed 200 OK in 816ms (Views: 785.2ms)
The key to solving this issue is to load the Users and the Photos in a single query instead of 1 by 1 per Judge.
You can do this utilizing Mongoids IdentityMap Mongoid 2 and Mongoid 3 support this feature.
First turn on the identity map in the mongoid.yml configuration file:
development:
host: localhost
database: awesome_app
identity_map_enabled: true
Now change the controller action to manually load the users and photos. Note: The Mongoid::Relation record will lazily evaluate the query so you must call to_a to actually query the records and have them stored in the IdentityMap.
def index
#judges ||= Awards::Api::Judge.where(:user_id.exists => true)
#users = User.where(:_id.in => #judges.map(&:user_id)).to_a
#photos = Awards::Api::Judges::Photo.where(:_id.in => #judges.map(&:photo_id)).to_a
respond_with #judges
end
This results in only 3 queries total. 1 for the Judges, 1 for the Users and 1 for the Photos.
Completed 200 OK in 559ms (Views: 87.7ms)
How does this work? What's an IdentityMap?
An IdentityMap helps to keep track of what objects or records have already been loaded. So if you fetch the first User record the IdentityMap will store it. Then if you attempt to fetch the same User again Mongoid queries the IdentityMap for the User before it queries the Database again. This will save 1 query on the database.
So by loading all of the Users and Photos we know we are going to want for the Judges json in manual queries we pre-load the data into the IdentityMap all at once. Then when the Judge requires it's User and Photo it checks the IdentityMap and does not need to query the database.
ActiveRecord :include typically doesn't do a full join to populate Ruby objects. It does two calls. First to get the parent object (say a Post) then a second call to pull the related objects (comments that belong to the Post).
Mongoid works essentially the same way for referenced associations.
def Post
references_many :comments
end
def Comment
referenced_in :post
end
In the controller you get the post:
#post = Post.find(params[:id])
In your view you iterate over the comments:
<%- #post.comments.each do |comment| -%>
VIEW CODE
<%- end -%>
Mongoid will find the post in the collection. When you hit the comments iterator it does a single query to get the comments. Mongoid wraps the query in a cursor so it is a true iterator and doesn't overload the memory.
Mongoid lazy loads all queries to allow this behavior by default. The :include tag is unnecessary.
This could help https://github.com/flyerhzm/mongoid-eager-loading
You need update your schema to avoid this N+1 there are no solution in MongoDB to do some jointure.
Embed the detail records/documents in the master record/document.
In my case I didn't have the whole collection but an object of it that caused n+1 (bullet says that).
So rather than writing below which causes n+1
quote.providers.officialname
I wrote
Quote.includes(:provider).find(quote._id).provider.officialname
That didn't cause a problem but left me thinking if I repeated myself or checking n+1 is unnecessary for mongoid.

Resources