Create json document from ActiveRecord and avoid N+1 - ruby-on-rails

I'm trying to create a json array (string actually) based on my db structure. I have the following relationship:
Country > State > City
The way I'm doing it now is very innefficient (N+1):
data = "[" + Country.all.map{ |country|
{
name: country.name,
states: country.states_data
}.to_json
}.join(",") + "]"
Then on the Country model:
def states_data
ret_states = []
states.all.each do |state|
ret_states.push name: state.name, cities: state.cities_data
end
ret_states
end
Then on the State model:
def cities_data
ret_cities = []
cities.all.each do |city|
ret_cities.push name: city.name, population: city.population
end
ret_cities
end
How can I do this more efficiently?

Eager load the states and cities. Just be careful because this could take up a lot of memory for large datasets. See documentation here http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations. Whenever possible I like using joins in addition to includes to fetch all data at once.
#to_json will also serialize Arrays for you, so you don't need to manually add bits of JSON.
Your code from above could be altered like so:
data = Country.joins(:states => :cities).includes(:states => :cities).all.map{ |country|
{
name: country.name,
states: country.states_data
}
}.to_json
But you could also remove the need for the _data methods.
data = Country.joins(:states => :cities).includes(:states => :cities).to_json(
:only => :name,
:include => {
:states => {
:only => :name,
:include => {
:cities => {
:only => [:name, :population]
}
}
}
}
)
That is pretty ugly, so you may want to look into overriding #as_json for each of your models. There is a lot of information about that available on the web.

u can provide the model to be included when converting to json.
country.to_json(:include => {:states => {:include => :cities}})
check http://apidock.com/rails/ActiveRecord/Serialization/to_json for

Related

Rails/mongoid: Advanced querying of arrays

Im stuck with an advanced query in rails. I need a solution that works in mongoid and if possible also active record (probably not possible). I've put together a simplified example below.
Consider the following model:
class Announcement
include Mongoid::Document
field :title, type: String
field :user_group, type: Array
field :year, type: Array
field :tags, type: Array
has_and_belongs_to_many :subjects
before_save :generate_tags
private
def generate_tags
tags = []
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
if self.year.present?
self.year.each { |x| tags << "year_" + x.to_s }
end
self.tags = tags
end
end
Given the tags array of document 1:
["hsc_mathematics", "hsc_chemistry", "year_9"]
And document 2:
["hsc_mathematics", "hsc_chemistry"]
And document 3:
["hsc_mathematics", "hsc_chemistry", "year_9", "year_10"]
And document 4:
["year_9", "year_10"]
Now consider the following model:
class Student < User
include Mongoid::Document
field :year, type: Integer
has_many :subjects
def announcements
tags = []
if self.subjects.present?
self.subjects.each { |x| subjects << x.name.downcase.gsub(" ", "_") }
end
tags << "year_" + self.year.to_s
Announcement.where("user_group" => { "$in" => ["Student", "all_groups"]}).any_of({"tags" => { "$in" => tags }}, {tags: []})
end
end
For the purpose of our example our student has the following tags:
[ "hsc_mathematics", "hsc_physics", "year_10" ]
My query is incorrect as I want to return documents 2, 3 and 4 but not document 1.
I need the query to adhere to the following when returning announcements:
i. If the announcement has subject tags match on any subject
ii. If the announcement has year tags match on any year
iii. If announcement has year and subject tags match on any year and any subject
How would I go about writing this?
EDIT
Im happy to split year out of my tags but im still stuck
Announcement.where("user_group" => { "$in" => ["Student", "all_groups"]}).any_of({"tags" => { "$in" => ["hsc_mathematics", "hsc_physics"] }}, {tags: []}).any_of({"year_at_school" => { "$in" => 10 }}, {year_at_school: []})
So the solution was to adjust my models and use a more organised query rather then an entire tag bank.
Announcement model:
class Announcement
include Mongoid::Document
field :title, type: String
field :user_group, type: Array, default: [""]
field :year, type: Array, default: [""]
field :tags, type: Array, default: [""]
has_and_belongs_to_many :subjects
before_save :generate_tags
private
def generate_tags
tags = []
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
self.tags = tags
end
end
User model:
class Student < User
include Mongoid::Document
field :year, type: Integer
has_many :subjects
def announcements
year = "year_" + self.year.to_s
tags = [""]
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
Announcement.where("user_group" => { "$in" => ["Student", ""] }).and("year" => { "$in" => [year, ""]}).any_in(tags: tags).all.entries
end
end
EDIT: Heres a neater version of the query as suggested
This example also has an expiry field which assumes nil = never expires
Announcement.where(:user_group.in => ["Student", ""], :year.in => [year, ""], :tags.in => tags).any_of({:expires_at.gte => Time.zone.now}, {:expires_at => nil}).all.entries

Where function equivalent for eager loading in rails

I am trying to optimize my code with eager loading, but when ever where function is called, a query is executed in logs.
#votes_list = Vote.joins(:user => :profile).where(:post_id => post.id)
#male_votes = #votes_list.where(:profiles => { :gender => 1 }).count
#female_votes = #votes_list.where(:profiles => { :gender => 2 }).count
I am trying to make few queries after the first one, without need to fetch from database, how to do it?
You want to eagerly load the Users and their Profile for each vote. Then you can select the sub-set of votes in-memory broken down by gender on the profile.
#votes_list = Vote.where(:post_id => post.id, :include => { :user => :profile })
#male_votes = #votes_list.select {|v| v.user.profile.gender == 1}
#female_votes = #votes_list.select {|v| v.user.profile.gender == 2}

Not returning an array - Rails model function

I have a method in my model Post like this:
def self.post_template
posts = Post.all
result = []
posts.each do |post|
single_post = {}
single_post['comment_title'] = post.comment.title
single_post['comment_content'] = post.comment.content
result << single_post
end
# return the result
result
end
In one of my rake tasks, I call the function:
namespace :post do
task :comments => :environment do
comments = Post.post_template
puts comments
end
end
In the console, the return value isn't an Array; instead, it prints all the hashes separated by a newline:
{ 'comment_title' => 'stuff', 'comment_content' => 'content' }
{ 'comment_title' => 'stuff', 'comment_content' => 'content' }
{ 'comment_title' => 'stuff', 'comment_content' => 'content' }
However, when I run this in my rails console, I get the expected behavior:
> rails c
> comments = Post.post_template
-- [{ 'comment_title' => 'stuff', 'comment_content' => 'content' },
{ 'comment_title' => 'stuff', 'comment_content' => 'content' }]
Needless to say, I'm pretty confused and would love any sort of guidance! Thank you.
EDIT:
Seems rake tasks simply print out arrays like this, but when I set the result of my array into another hash, it does not seem to maintain the integrity of the array:
namespace :post do
task :comments => :environment do
comments = Post.post_template
data = {}
data['messages'] = comments
end
end
I'm using Mandrill (plugin for Mailchimp) to create these messages and it throws an error saying that what I'm passing in isn't an Array.
I think that's just how rake prints arrays. Try this:
task :array do
puts ["First", "Second"]
end
Now:
> rake array
First
Second

Where to put constants in Rails

I have a few constants which are arrays that I don't want to create databse records for but I don't know where to store the constants without getting errors.
For example
CONTAINER_SIZES = [["20 foot"],["40 foot"]]
Where can I store this so all models and controller have access to this?
I will write my way to you.
class User < ActiveRecord::Base
STATES = {
:active => {:id => 100, :name => "active", :label => "Active User"},
:passive => {:id => 110, :name => "passive", :label => "Passive User"},
:deleted => {:id => 120, :name => "deleted", :label => "Deleted User"}
}
# and methods for calling states of user
def self.find_state(value)
if value.class == Fixnum
Post::STATES.collect { |key, state|
return state if state.inspect.index(value.to_s)
}
elsif value.class == Symbol
Post::STATES[value]
end
end
end
so i can call it like
User.find_state(:active)[:id]
or
User.find_state(#user.state_id)[:label]
Also if i want to load all states to a select box and if i don't want some states in it (like deleted state)
def self.states(arg = nil)
states = Post::STATES
states.delete(:deleted)
states.collect { |key, state|
if arg.nil?
state
else
state[arg]
end
}
end
And i can use it now like
select_tag 'state_id', User.states.collect { |s| [s[:label], s[:id]] }
I put them directly in the model class.
class User < ActiveRecord::Base
USER_STATUS_ACTIVE = "ACT"
USER_TYPES = ["MANAGER","DEVELOPER"]
end

Filtering ActiveRecord queries in rails

I'm used to Django where you can run multiple filter methods on querysets, ie Item.all.filter(foo="bar").filter(something="else").
The however this is not so easy to do in Rails. Item.find(:all, :conditions => ["foo = :foo", { :foo = bar }]) returns an array meaning this will not work:
Item.find(:all, :conditions => ["foo = :foo", { :foo = 'bar' }]).find(:all, :conditions => ["something = :something", { :something = 'else' }])
So I figured the best way to "stack" filters is to modify the conditions array and then run the query.
So I came up with this function:
def combine(array1,array2)
conditions = []
conditions[0] = (array1[0]+" AND "+array2[0]).to_s
conditions[1] = {}
conditions[1].merge!(array1[1])
conditions[1].merge!(array2[1])
return conditions
end
Usage:
array1 = ["foo = :foo", { :foo = 'bar' }]
array2 = ["something = :something", { :something = 'else' }]
conditions = combine(array1,array2)
items = Item.find(:all, :conditions => conditions)
This has worked pretty well. However I want to be able to combine an arbitrary number of arrays, or basically shorthand for writing:
conditions = combine(combine(array1,array2),array3)
Can anyone help with this? Thanks in advance.
What you want are named scopes:
class Item < ActiveRecord::Base
named_scope :by_author, lambda {|author| {:conditions => {:author_id => author.id}}}
named_scope :since, lambda {|timestamp| {:conditions => {:created_at => (timestamp .. Time.now.utc)}}}
named_scope :archived, :conditions => "archived_at IS NOT NULL"
named_scope :active, :conditions => {:archived_at => nil}
end
In your controllers, use like this:
class ItemsController < ApplicationController
def index
#items = Item.by_author(current_user).since(2.weeks.ago)
#items = params[:archived] == "1" ? #items.archived : #items.active
end
end
The returned object is a proxy and the SQL query will not be run until you actually start doing something real with the collection, such as iterating (for display) or when you call Enumerable methods on the proxy.
I wouldn't do it like you proposed.
Since find return an array, you can use array methods to filter it, on example:
Item.find(:all).select {|i| i.foo == bar }.select {|i| i.whatever > 23 }...
You can also achive what you want with named scopes.
You can take a look at Searchlogic. It makes it easier to use conditions on
ActiveRecord sets, and even on Arrays.
Hope it helps.
You can (or at least used to be able to) filter like so in Rails:
find(:all, :conditions => { :foo => 'foo', :bar => 'bar' })
where :foo and :bar are field names in the active record. Seems like all you need to do is pass in a hash of :field_name => value pairs.

Resources