My Thread model has many Posts. Let's say I want to reorder posts by an array containg ids I wish to see my posts sorted by.
thread.posts.collect {|x| x.id} # => [1,2,3]
order = [2,3,1]
posts = thread.posts.sort_by {|x| order.index x.id}
posts.collect {|x| x.id} # => [2,3,1]
thread.update_attributes(:posts => posts) # => true
thread.posts.collect {|x| x.id} # => [1,2,3]
What am I doing wrong? Is sorting by id always preserved in collections and can I somehow disable it?
You should always assume the order of results retrieved from your database as being more or less "random", unless you specifically ask it to sort them. This means that you can not rely on your database to magically store the order of posts associated with a thread (in fact, the code sample you posted would probably not query the database at all, because there is nothing to update).
The easiest way to achieve what you want is to add an order field to your Post model like this:
class AddOrderToPost < ActiveRecord::Migration
def up
change_table :posts do |t|
t.integer :order, :default => 0
end
Post.update_all ["order = ?", 0]
end
def down
remove_column :posts, :order
end
end
In your Thread model:
class Thread < ActiveRecord::Base
# ...
has_many :posts, :order => 'order ASC'
# ...
end
Afterwards you will be able to reorder the posts like this:
thread.posts.zip([2,3,1]) { |p,i| p.order = i }
If you want, you can also use a plugin like acts_as_list which provides this and other useful functionality.
Related
I have a requirement to sort/filter a column in an ActiveAdmin view. The column is the count of a sub-object. Specifically, the model looks like:
class Location < ActiveRecord::Base
...
has_many :things
...
The ActiveAdmin page needs to have a column for this, which I have like this:
column 'Thing Count', :sortable => 'Thing Count' do |location|
location.things.length
end
However, the sorting does not actually work, and I have not been able to figure out a way to make filtering work either. I've tried several variations on:
:filter 'Thing Count'
with no success. Has anyone ever successfully got ActiveAdmin to sort or filter on a count column of sub objects? If so how? Thanks!
ActiveAdmin can only have filters on database columns.
You can do the following:
Make a counter_cache column on the belongs_to side / Thing model.
Make a filter for that column filter :things_count
Recalculate the counter for each Thing.
Example:
def up
add_column :projects, :tasks_count, :integer, :default => 0
Project.reset_column_information
Project.find(:all).each do |p|
Project.update_counters p.id, :tasks_count => p.tasks.length
end
end
def down
remove_column :projects, :tasks_count
end
class Location
has_many :things
def self.all_things
joins(:things).select("locations.id as loc_id, count(things.*) as count").group("locations.id").order("count(things.*)")
end
end
I have a relationship between two models, Idea and Iteration. Each idea can have many iterations. The models looks like this;
# idea.rb
has_many :iterations, dependent: :destroy
# I also use with: :real_time
after_save ThinkingSphinx::RealTime.callback_for(:idea)
# iteration.rb
belongs_to :idea
after_save ThinkingSphinx::RealTime.callback_for(:idea, [:idea])
My indexes for Idea looks like this:
ThinkingSphinx::Index.define :idea, :with => :real_time do
[...]
indexes iterations.title, as: :iteration_titles
indexes iterations.description, as: :iteration_descriptions
has iterations.id, :as => :iteration_ids, type: :integer
[...]
end
And I search like this:
#ideas = #user.ideas.search ThinkingSphinx::Query.escape(params[:search]),
:with => {:team_id => #teams.collect(&:id)},
:page => params[:page],
:per_page => 10,
:order => 'created_at DESC'
Currently, searching for either Iteration title or description returns 0 hits. And I have performed:
rake ts:rebuild
rake ts:regenerate
rake ts:index
Have I missed something?
One of the differences between real-time indices and SQL-backed indices is that with SQL-backed indices, you refer to associations and columns in your index definition, but with real-time indices, you refer to methods.
And while iterations is an instance method within an idea, title, description and id are not methods on the object returned by iterations.
The easiest way to work through this has two parts. Firstly, add instance methods to Idea that return the data you want for the iteration-related fields and attributes:
def iteration_titles
iterations.collect(&:title).join(' ')
end
def iteration_descriptions
iterations.collect(&:description).join(' ')
end
def iteration_ids
iterations.collect &:id
end
And then use those methods in your index definition:
indexes iteration_titles, iteration_descriptions
has iteration_ids, :type => :integer, :multi => true
And then, run rake ts:regenerate to get it all set up (ts:rebuild and ts:index have no meaning for real-time indices).
I have a City model and a Business model.
Business belogs_to :city
Now I want to define a view with a list of only those cities who have a child (in this case a business). Empty cities (means cities which still have no business added) should not be considered. If possible the list should be sorted in a way that the city with most businesses is on the top.
I found a solution like this:
#cities = City.find :all,
:joins => "INNER JOIN businesses ON businesses.city_id = cities.id",
:select => "cities.*, count(businesses.id) businesses_count",
:group => "businesses.city_id HAVING businesses_count > 0",
:order => "businesses_count desc"
This works fine (sorting is not yet done), but as far as I understood this will not work with Rails 3.1 and 3.2 (I use 3.0 now). See http://m.onkey.org/active-record-query-interface
Can anybody let me know how to define my #cities in a way that is ok for Rails 3.1 and 3.2?
Thank you!
#KandadaBoggu:
Great, I very much like your answer 2), thanks!!
Just a comment:
I migrated with
rails generate migration add_businesses_count_to_cities businesses_count:integer
Then I needed to edit the migration:
class AddBusinessesCountToCities < ActiveRecord::Migration
def self.up
add_column :cities, :businesses_count, :integer, :default => 0
City.reset_column_information
City.all.each do |c|
c.update_attribute :businesses_count, c.businesses.count
end
end
def self.down
remove_column :cities, :businesses_count
end
end
This is important to set a default value of 0 and then update the cities with the current number of businesses.
I also added the counter_cache to the child (business) like:
belongs_to :city, :counter_cache => true
This way it works great.
1) Without sorting:
City.joins(:businesses).select("DISTINCT cities.*")
2) Using counter_cache
Add an integer column called business_count to cities table.
class City < ActiveRecord::Base
has_many :businesses, :counter_cache => :business_count
end
Now you can select the cities as follows:
City.where("business_count > 0").order(:business_count)
3) Using group by
City.joins("
( SELECT a.id, COUNT(*) as business_count
FROM cities a, businesses b
WHERE a.id = b.city_id
GROUP BY a.id
) c ON cities.id = c.id ").
select("cities.*, c.business_count AS business_count").
order(:business_count)
If I have something like this:
class Post < ActiveRecord::Base
has_many :comments, :as => :commentable do
def approved
find(:all, :conditions => {:approved => true})
end
end
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
...when I do this, I get 2 hits to the database (not including finding the post :p):
post = Post.first
post.comments #=> [Comment1, Comment2...]
post.comments.approved #=> [Comment1, Comment7...]
It seems like it should just filter the current comments array in memory, no? Is it doing that? Reason I ask is because the console shows SELECT * FROM ... on post.comments.approved, even though I already called post.comments. Shouldn't this be better optimized in ActiveRecord?
The AR executes a new query for any finder calls inside a association extension method.
You can refer to the cached result set by using self.
has_many :comments, :as => :commentable do
def approved
# uses the cached result set
self.select{|c| c.approved == true}
end
end
It's optional, as in some cases you might only want to load the associated objects when needed. If you want them all loaded into memory, you need to explicitly declare the objects you'd like included with the initial query, using the :include flag. Example:
post = Post.find(:first, :include => :comment)
You might have to rewrite your extension to take advantage of the feature... an approach would be to change your "approved" function to iterate through the comments array attached to each post, and return a new array with the nonapproved comments filtered out. The "find" you have defined explicitly goes back to the database.
If your query is really that simple, then what you want is a named scope:
class Comment
named_scope :approved, :conditions => {:approved => true}
end
Then, you can do:
#post.comments.approved.count #=> 1 DB hit!
#post.comments.count #=> Another DB hit - can't reuse same scope
Look at #scope (#named_scope in Rails 2.3).
I have three models that look something like this:
class Bucket < ActiveRecord::Base
has_many :entries
end
class Entry < ActiveRecord::Base
belongs_to :submission
belongs_to :bucket
end
class Submission < ActiveRecord::Base
has_many :entries
belongs_to :user
end
class User < ActiveRecord::Base
has_many :submissions
end
When I retrieve a collection of entries doing something like:
#entries = Entry.find(:all,
:conditions => ['entries.bucket_id = ?', #bucket],
:include => :submission)
The performance is pretty quick although I get a large number of extra queries because the view uses the Submission.user object. However, if I add the user to the :include statement, the performance becomes terrible and it takes over a minute to return a total of 50 entries and submissions spread across 5 users. When I run the associated SQL commands, they complete in well under a second - the SQL query performance is the same from each set of queries.
#entries = Entry.find(:all,
:conditions => ['entries.bucket_id = ?', #bucket],
:include => {:submission => :user})
Why would this second command have such terrible performance compared to the first?
it's because you have a double join in second statement. So the number of result is bigger.
More bigger the result is much slow it's.
I'm not sure if the performance will be much better, but try:
#bucket.entries.find(:all, :include => {:submission => :user})
I'm really only familiar with MySQL, but if your database supports them, indexes will help performance significantly. I usually do this in my migrations like:
def self.up
create_table :entries do |t|
t.references :submission
t.references :bucket
# other fields...
end
add_index :entries, :submission_id
add_index :entries, :bucket_id
end
This ended up being a problem with the serialization/deserialization of the user model in the entire object graph. By caching relevant data on the Entry and Submission models we were able to avoid the lookup to User and saved a considerable amount of time.