Rails - Serialize related data - sql

I've got two models:
class Continent < ActiveRecord::Base
has_many :countries
end
class Country < ActiveRecord::Base
belongs_to :continent
end
I created controller like:
class ContinentsController < ApplicationController
def index
#continents = Continent.all
render json: #continents
end
end
and serializer:
class ContitnentSerializer < ActiveModel::Serializer
attributes :name, :countries
end
Here my issue begins. I'd like to serialize only countries with given condition where value comes from HTTP GET params. E.g country inside serializer should be displayed only if population is more than params[:population]. The problem is inside serializer we don't have access to params to examine that.
[
{
name: 'Europe'
countries: [
{
name: 'Italy',
population: 1000000
}
]
},
{
name: 'Africa'
countries: [
]
}
]
I've tried to join table with condition but it seems be not working.
#continents = Continent.all.joins("LEFT JOIN countries ON countries.continent_id = continents.id AND countries.population > #{params[:population]}")

Create a scope and call the scope with param value from controller:
scope :population_more_than, ->(population) {all.joins("LEFT JOIN countries ON countries.continent_id = continents.id AND countries.population > ?", population)}
Now call it from controller instead of Continent.all
Continent.population_more_than(params[:population])

You can try
#continents = Continent.all
#continents.num_population = params[:population]
render json: #continents.to_json(methods: :countries_with_population_gt)
in your Continent model
attr_accessor :num_population
def countries_with_population_gt(num_population=0)
countries.where('population > ?', #num_population)
end

Basically, you need to select only Continents that fall under specific rule. If this is a frequently used filter, then I would go with the Babar's suggestion and create a scope.
If this is a one time selection, then I prefer simply do filtering right there without cluttering up my models with non-frequently used scopes.
Continent.joins(:countries).where("countries.population > :population", population: params[:population])
# Or event shorter
Continent.joins(:countries).where("countries.population > :population", params)

Related

ActiveRecord: How to order and retrieve records in a greatest-n-per-group situation

I'm stuck with a classic greatest-n-per-group problem, where a cat can have many kittens, but I'm usually just interested in the youngest.
I already do know how to build a scope and a has_one relation for the Cat.
My question: Is there a way to...
list all cats' names together with their youngest kittens' names...
while at the same time ordering them by their respective youngest kitten's name...
...using just a single SELECT under the hood?
What I got so far:
class Cat < ApplicationRecord
has_many :kittens
has_one :youngest_kitten, -> { merge(Kitten.youngest) }, foreign_key: :cat_id, class_name: :Kitten
scope :with_youngest_kittens, lambda {
joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
end
class Kitten
belongs_to :cat
scope :youngest, lambda {
joins(Kitten.younger_kittens_sql("kittens.cat_id"))
.where(younger_kittens: { id: nil })
}
def self.younger_kittens_sql(cat_field_name)
%{
LEFT OUTER JOIN kittens AS younger_kittens
ON younger_kittens.cat_id = #{cat_field_name}
AND younger_kittens.created_at > kittens.created_at
}
end
end
When I run Cat.with_latest_kittens.order('kittens.name').map(&:name) everything looks fine: I get all the cats' names with just a single SELECT.
But when I run Cat.with_latest_kittens.order('kittens.name').map {|cat| cat.youngest_kitten.name}, I get the right result too, but a superfluous additional SELECT per cat is executed. Which is just logical, because the with_youngest_kittens doesn't know it should populate youngest_kitten. Is there a way to tell it or am I going about this all wrong?
I think adding an includes to your :with_youngest_kittens scope will fix the problem. Try changing the scope to
scope :with_youngest_kittens, lambda {
includes(:youngest_kitten)
.joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
This should prevent Rails from making a separate database query for every kitten.
I found a solution that produces no extra SELECT, however it is quite ugly, so I'll actually go for localarrow's solution as it's more readable!
I thought I'd still post it for the sake of completeness (If someone needs the few ms extra performance):
First I add custom tailored select fields for each kitten column to the Cat.with_youngest_kitten scope:
scope :with_youngest_kittens, lambda {
kitten_columns = Kitten
.column_names
.map { |column_name| "kittens.#{column_name} AS `youngest_kittens.#{column_name}`" }
.join(', ')
joins(:kittens)
.joins(Kitten.latest_outer_join_sql("cats.id"))
.where(later_kittens: { id: nil })
.select("cats.*, #{kitten_columns}")
}
Then I override the has_one youngest_kitten relation with a method, that retrieves those custom selects and calls super if no data has been retrieved:
def youngest_kitten
return super if self[:'youngest_kittens.id'].nil?
kitten_hash = Hash[Kitten.column_names.collect { |column_name| [column_name, self[:"youngest_kittens.#{column_name}"]] }]
kitten_hash[:cat] = self
Kitten.new(kitten_hash)
end

Group By multiple column for sql count statement , in rails environment

I have two Relations (class)
class RecommendedForType < ActiveRecord::Base
attr_accessible :description, :name
has_many :recommended_for_type_restaurants
end
And
class RecommendedForTypeRestaurant < ActiveRecord::Base
attr_accessible :recommended_for_type_id, :restaurant_id, :user_id
belongs_to :restaurant
belongs_to :user
belongs_to :recommended_for_type
def self.get_count(rest_id)
r = RecommendedForTypeRestaurant.where(restaurant_id: rest_id)
#result = r.includes(:recommended_for_type)
.select("recommended_for_type_id, recommended_for_types.name")
.group ("recommended_tor_type_id, recommended_for_types.name")
.count
end
end
if I call
RecommendedForTypeRestaurant.get_count(1) # get stat of restaurant_id: 1
I get
{"dinner"=>1, "fast food"=>1, "lunch"=>3, "romantic dinner"=>1}
My goal is to get both id and name of RecommendTypeFor in the return result as well. currently i can only make it return either id or name. Something like this
{{"id" => 1, "name" => "dinner", "count" => 1}, {"id" =>2, "name" => "lunch", "count" => 3} }
For sure i can do another round of sql once i get id or name to create that hash but i think that not the efficient way. Appreciate any help or suggestion :)
You need to group separately, try the following group.
.group ("recommended_tor_type_id", "recommended_for_types.name").count
I believe that if you remove the .count at the end and change the select to:
.select("count(*), recommended_for_type_id, recommended_for_types.name")
You will get an array of models that will have the attributes you need and the count.
You should be able to test it out in the console by do something like this:
r = RecommendedForTypeRestaurant.where(restaurant_id: rest_id)
#result = r.includes(:recommended_for_type)
.select("recommended_for_type_id, recommended_for_types.name, count(*)")
.group ("recommended_tor_type_id, recommended_for_types.name")
#result.each do |r|
puts r.recommended_for_type_id
puts r.name
puts r.count
end
Hope it helps!

Rails 3: As json with include option does not takes into account as_json redefinition for included association

I've got two models.
Class ModelA < ActiveRecord::Base
has_many :model_bs
end
Class ModelB < ActiveRecord::Base
belongs_to :model_a
def as_json(options = {})
{
:whatever => 'hello world'
}
end
end
When I call model_a.as_json(:include => :model_b), I want it to return a json which includes all model_bs, which it does, but employing my as_json redefinition, which it does not as it just uses the default one. Is there any way to use my own method rather than the original one? Thanks
In Rails 3, as_json method invokes serializable_hash to obtain the attributes hash. And they share the same 'options' parameter. In your case, overwritting serializable_hash would give the expected result.
def serializable_hash(options = {})
{:whatever => 'hello world'}
end
But, My suggestion is that instead of overwriting the convention, operate on the result of "super", which is like:
def serializable_hash(options = {})
hash = super
has[:name] = "hello world"
hash
end

Rails assigning names to variables

I'm building a user ranking system, and am trying to assign user.rank values with a name.
I wanted to define something like this in my User model and then be able to reference it when displaying each user's rank, but this probably isn't the best way:
class User < ActiveRecord::Base
RANK_NAMES = {
'Peasant' => (0..75),
'Craftsman' => (76..250),
'Vassal' => (251..750),
'Noble' => (750..1500),
'Monarch' => (1501..999999)
}
Perhaps it would be better to define a method in a controller or helper like:
if user.rank == 0..75
rank_name = "Peasant"
elsif...
But not sure how to do that. Anyone have any thoughts? I'm not even sure what to call what it is I'm trying to do, thus making it difficult to research on my own.
It could be something even as simple as this, assuming user.rank exists.
class User < ActiveRecord::Base
...
def rank_name
case self.rank
when 0..75
'Peasant'
when 76..250
'Craftsman'
when 251..750
'Vassal'
when 750..1500
'Noble'
when 1501..999999
'Monarch'
end
end
...
end
If rank_name is specific to the User, I'd make it a method of User.
You could try something like below. It might give you some ideas.
class User
RANKS = [
{:name => 'Peasant', :min => 0, :max => 75},
{:name => 'Craftsman', :min => 76, :max => 250}
# ...
]
attr_accessor :rank
def rank_name
# TODO what happens if rank is out of range of all ranks or rank is nil
# or not an integer
User::RANKS[rank_index][:name]
end
private
def rank_index
User::RANKS.index { |r| (r[:min]..r[:max]).include? #rank }
end
end
user = User.new
user.rank = 76
puts user.rank_name # -> Craftsman

Help doing a dynamic sort?

I have a notifications table which contains different types of notifications for different events.
Inside the table is a notifications_type:string column that contains the type of notification, i.e. "foo" or "bar" or "oof"
I want the user to be able to select what notifications they want to display, so there are checkboxes below the result that correspond to prefs_display_foo:boolean, prefs_display_bar:boolean in the User model.
What is an elegant way for me to set the :conditions in the find to properly display the sorted results? Also, currently I have it as a method in the User model, but how would I do it as a has_many :notifications, :conditions => .....
You could try this
leave the has_many without any conditions and then create a named_scope to handle the preferences, this code is untested
class User
has_many :notifications
def notification_preferences
list = []
list << 'Foo' if prefs_display_foo
list << 'Bar' if prefs_display_bar
# etc...
return list
end
end
class Notification
belongs_to :user
named_scope :preferred,
lambda { |preferences| {
:conditions => ["notifications_type IN (?)", preferences]
}
}
end
# ...
user = User.find(1)
notifications = user.notifications.preferred(user.notification_preferences)