Where function equivalent for eager loading in rails - ruby-on-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}

Related

Mongoid - two query conditions to both be met in an embedded doc?

I have two models
class User
include Mongoid::Document
include Mongoid::Timestamps
field :username
embeds_many :user_tags
end
class UserTag
include Mongoid::Document
field :name
field :like_count, :type => Integer, :default => 0
embedded_in :user
end
I want to query all the users that have the user_tag named "nyc" and where the user_tag "nyc" has a like_count > 10. I've tried the following:
users = User.where('user_tags.name' => "nyc").and('user_tags.like_count' => {'$gte' => 10 })
Logically this does what it's supposed to do, but not what I need it to do. It returns users that have the user_tag "nyc" and have any user_tag with a like_count >= 10. I need users that have the user_tag "nyc" and where the user_tag "nyc"'s like_count is >= 10.
How do I do that? I'm running mongoid 4.0.2.
Actually your query is not correct for the purpose you are trying to achieve. It translates to the following MongoDB query:
db.users.find({'user_tags.name': 'nyc' }, {'user_tags.like_count': {$gte: 10}})
It means that MongoDB will find all documents with both criteria. Mongoid is returning you the same data, as MongoDB.
What you need instead is the following MongoDB query:
db.users.find({ user_tags: {
$elemMatch: {
name: 'nyc',
like_count: { $gte: 10 }
}
}})
With Mongoid you can write:
User.where(user_tags: {
'$elemMatch' => {
name: 'nyc',
like_count: { '$gte' => 10 }
}
}).count
Maybe you should write something like this:
users = User.where('user_tags.name' => "nyc", 'user_tags.like_count' => {'$gte' => 10 })
Mongoid will try to find Documents which satisfies both conditions.
You can try this
users = User.where('user_tags.name' => "nyc").where('user_tags.like_count' => {'$gte' => 10 }).all
or
users = User.where('user_tags.name' => "nyc", 'user_tags.like_count' => {'$gte' => 10 }).all

Create json document from ActiveRecord and avoid N+1

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

Multiple LIKE matches with Arel & MetaWhere

I'm transitioning an application written in Rails-2.3 with SearchLogic to Rails-3.0 with Arel and MetaWhere, and I'm running into an operation that I don't know how the write.
The old code was:
if params[:city] && params[:city].respond_to?(:each)
users = users.person_address_city_like_any(params[:city])
end
what this did was run a LIKE match against each item in the params[:city] array.
This is easy enough in MetaWhere when there's only one search term:
users = users.where(:person => { :address => { :city.matches => '%city1%' } })
but how would I write this with an arbitrary number of cities?
Try:
users = users.where(:person => { :address => { :city.matches_any => ['%city1%','%city2%'] } })

rails active record - complicated conditions clause

this works:
ids = [1,2]
varietals = Varietal.find(:all, :conditions => [ "id IN (?)",ids])
But what I want to do is that plus have a condition of: deleted => false
varietals = Varietal.find(:all, :conditions =>{ :deleted => false})
any ideas?
am i going to have to use find_by_sql?
I would handle this with a named_scope to communicate intent and foster re-use:
named_scope :undeleted,
:conditions => { :deleted => false }
Then you can simply use:
varietals = Varietal.undeleted.find([1,2])
You can do it a few ways, but this is the most straight forward:
varietals = Varietal.find( [1,2], :conditions => { :deleted => false })
You can see in the docs that the first parameter of find can take an integer or an array.
ids = [1,2]
varietals = Varietal.find(:all, :conditions => {:id => ids, :deleted => false})
This should work, haven't tested it though.
From the docs:
An array may be used in the hash to
use the SQL IN operator:
Student.find(:all, :conditions => { :grade => [9,11,12] })

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