Rails optional association query - ruby-on-rails

i've been trying to solve this issue for some time now with no success :(
i have 2 model classes - ConfigurationKey and ConfigurationItem, as follows:
class ConfigurationKey < ActiveRecord::Base
has_many :configuration_items
# this class also has a 'name' attribute
end
class ConfigurationItem < ActiveRecord::Base
belongs_to :app
belongs_to :configuration_key
end
i would like to fetch all of the ConfigurationKeys that have a specific 'name' attribute, along with a filtered subset of their associated ConfigurationItems, in one single query.
i used the following command:
configuration_key = ConfigurationKey.includes(:configuration_items).where(name: key_name, configuration_items: { app: [nil, app] })
but the ConfigurationKeys that don't have any associated ConfigurationItems are not returned.
i thought the the 'includes' clause, or the explicit use of 'LEFT OUTER JOIN' would make it work, but it didn't :/
is there any possible way to do this, or do i have to use 2 queries - one to get all of the relevant ConfigurationKeys, and another in order to get all of the relevant ConfigurationItems?
thanks ;)

Using includes with where clause in rails 4.2 generates a LEFT OUTER JOIN query.
Please take a look at the generated sql in rails console.
$ rails c
> ConfigurationKey.includes(:configuration_items).where(name: key_name, configuration_items: { app: [nil, app] })
# Sql is displayed...
Probably, you'll see LEFT OUTER JOINed sql, and if so, it's correct.
Mind you, what you get via ActiveRecord is NOT equals to results from sql. It returns you DISTINCT results from its sql.
So, I think it's impossible to make a success in one single query.

Related

Exclude 'nil' row from ActiveRecord query with a HAVING clause where no results are returned

I'm building a series of methods in a model to use as scopes. When one of these uses a having clause and that query returns no results, I get an instance of the model returned where all fields are nil, and that breaks most code I'd like to use these scopes in.
The below is a highly simplified example to demonstrate my issue.
class Widget < ActiveRecord::Base
attr_accessible :name
has_many :components
def without_components
joins(:components).group('widgets.id')having('COUNT(components.id) = 0')
end
def without_components_and_remove_nil
without_components.select{|i| i.id} # Return objects where i.id is not nil
end
end
Calling Widget.without_components if all Widgets have components assigned returns the non-desirable:
[{id: nil, name: nil, user_id: nil}]
But if I call Widget.without_components_and_remove_nil it converts the ActiveRecord::Relation object that would be returned into an Array, so I can't chain it with other scopes as I need to do.
Is there a way of changing the scopes so that either the nil row is excluded if it appears, or is there a modification that could be made to my ActiveRecord query to allow for this to work?
There were two issues I needed to resolve to get that scope working; one not related to my original question, though the scope I presented above wouldn't have been fixed without it:
First, and directly dealing with the question at hand, since Rails 3.1 you can do this:
.
Widget.where(id: Widget.joins(:components).group('widgets.id').having('COUNT(components.id)'))
Second, the joins(:components) part of it wasn't going to work with the having('COUNT(components.id = 0)') because joins performs an inner join, thus there would never be any results for that query. I had to replace the joins with joins("LEFT OUTER JOIN components ON components.widget_id = widgets.id").

ActiveRecord query array intersection?

I'm trying to figure out the count of certain types of articles. I have a very inefficient query:
Article.where(status: 'Finished').select{|x| x.tags & Article::EXPERT_TAGS}.size
In my quest to be a better programmer, I'm wondering how to make this a faster query. tags is an array of strings in Article, and Article::EXPERT_TAGS is another array of strings. I want to find the intersection of the arrays, and get the resulting record count.
EDIT: Article::EXPERT_TAGS and article.tags are defined as Mongo arrays. These arrays hold strings, and I believe they are serialized strings. For example: Article.first.tags = ["Guest Writer", "News Article", "Press Release"]. Unfortunately this is not set up properly as a separate table of Tags.
2nd EDIT: I'm using MongoDB, so actually it is using a MongoWrapper like MongoMapper or mongoid, not ActiveRecord. This is an error on my part, sorry! Because of this error, it screws up the analysis of this question. Thanks PinnyM for pointing out the error!
Since you are using MongoDB, you could also consider a MongoDB-specific solution (aggregation framework) for the array intersection, so that you could get the database to do all the work before fetching the final result.
See this SO thread How to check if an array field is a part of another array in MongoDB?
Assuming that the entire tags list is stored in a single database field and that you want to keep it that way, I don't see much scope of improvement, since you need to get all the data into Ruby for processing.
However, there is one problem with your database query
Article.where(status: 'Finished')
# This translates into the following query
SELECT * FROM articles WHERE status = 'Finished'
Essentially, you are fetching all the columns whereas you only need the tags column for your process. So, you can use pluck like this:
Article.where(status: 'Finished').pluck(:tags)
# This translates into the following query
SELECT tags FROM articles WHERE status = 'Finished'
I answered a question regarding general intersection like queries in ActiveRecord here.
Extracted below:
The following is a general approach I use for constructing intersection like queries in ActiveRecord:
class Service < ActiveRecord::Base
belongs_to :person
def self.with_types(*types)
where(service_type: types)
end
end
class City < ActiveRecord::Base
has_and_belongs_to_many :services
has_many :people, inverse_of: :city
end
class Person < ActiveRecord::Base
belongs_to :city, inverse_of: :people
def self.with_cities(cities)
where(city_id: cities)
end
# intersection like query
def self.with_all_service_types(*types)
types.map { |t|
joins(:services).merge(Service.with_types t).select(:id)
}.reduce(scoped) { |scope, subquery|
scope.where(id: subquery)
}
end
end
Person.with_all_service_types(1, 2)
Person.with_all_service_types(1, 2).with_cities(City.where(name: 'Gold Coast'))
It will generate SQL of the form:
SELECT "people".*
FROM "people"
WHERE "people"."id" in (SELECT "people"."id" FROM ...)
AND "people"."id" in (SELECT ...)
AND ...
You can create as many subqueries as required with the above approach based on any conditions/joins etc so long as each subquery returns the id of a matching person in its result set.
Each subquery result set will be AND'ed together thus restricting the matching set to the intersection of all of the subqueries.

Can you use association extensions on an object with custom finder_sql?

I was under the impression that the custom_finder option in ActiveRecord meant that you got association extensions like .where and .order for free, for instance:
class Dog < ActiveRecord::Base
has_many :fleas, class_name: 'Flea',
finder_sql: "select fleas.* from fleas where pet_type = 'dog'"
end
granted this is not a great example as the 'finder_sql' is so trivial however it illustrates the point.
Generated SQL
I would expect the following
#dog = Dog.find(5)
#dog.fleas.where('num_legs > 2')
to generate
"select fleas.* from fleas where pet_type = 'dog' AND num_legs > 2
i.e. custom finder_sql + where clause
however what it actually generates is
"SELECT "base_posts".* FROM "fleas" WHERE "fleas"."dog_id" = 5 AND (num_legs > 2)
i.e. it completely ignores the custom finder_sql and tries to join the fleas to the current dog.
If the custom finder_sql doesn't cause the association extensions to respect it then what's the point in it - it could just be a method on the object...
This is true. I think that custom finder is the legacy of the previous generation of finders in Rails (before Arel && AR::Relation) and its presence in associations today is only for backward compatibility (imho). Anyway, it does not fit the ideology of AR::Relation (extensions).
By now, we have .find_by_sql() and we have :finder_sql as options for associations.
Actually now, when association has :finder_sql option, it is only used for the formation of records collection (using .find_by_sql). Association creates the AR::Relation object, loads collection (.target) and delegating AR::Relation method calls to relation object, which however was not aware of any custom SQL statements and knows only primary SQL expression. This is why:
#dog.fleas.class
#=> Array
#dog.fleas.to_sql # delegated to AR::Relation
returns primary expression "SELECT "base_posts".* FROM..." and
#dog.fleas.where("...") # delegated to AR::Relation
imposes new conditions as if there is no custom finder.
On the other hand, since .find_by_sql always returns an array, to impose new conditions could be possible using Array's methods.
#dog.fleas.select {|r| ... } # delegated to Array

Datamapper: Sorting results through association

I'm working on a Rails 3.2 app that uses Datamapper as its ORM. I'm looking for a way to sort a result set by an attribute of the associated model. Specifically I have the following models:
class Vehicle
include DataMapper::Resource
belongs_to :user
end
class User
include DataMapper::Resource
has n, :vehicles
end
Now I want to be able to query the vehicles and sort them by the name of the driver. I tried the following but neither seems to work with Datamapper:
> Vehicle.all( :order => 'users.name' )
ArgumentError: +options[:order]+ entry "users.name" does not map to a property in Vehicle
> Vehicle.all( :order => { :users => 'name' } )
ArgumentError: +options[:order]+ entry [:users, "name"] of an unsupported object Array
Right now I'm using Ruby to sort the result set post-query but obviously that's not helping performance any, also it stops me from further chaining on other scopes.
I spent some more time digging around and finally turned up an old blog which has a solution to this problem. It involves manually building the ordering query in DataMapper.
From: http://rhnh.net/2010/12/01/ordering-by-a-field-in-a-join-model-with-datamapper
def self.ordered_by_vehicle_name direction = :asc
order = DataMapper::Query::Direction.new(vehicle.name, direction)
query = all.query
query.instance_variable_set("#order", [order])
query.instance_variable_set("#links", [relationships['vehicle'].inverse])
all(query)
end
This will let you order by association and still chain on other scopes, e.g.:
User.ordered_by_vehicle_name(:desc).all( :name => 'foo' )
It's a bit hacky but it does what I wanted it to do at least ;)
Note: I'm not familiar with DataMapper and my answer might not be within the standards and recommendations of using DataMapper, but it should hopefully give you the result you're looking for.
I've been looking through various Google searches and the DataMapper documentation and I haven't found a way to "order by assocation attribute". The only solution I have thought of is "raw" SQL.
The query would look like this.
SELECT vehicles.* FROM vehicles
LEFT JOIN users ON vehicles.user_id = users.id
ORDER BY users.name
Unfortunately, from my understanding, when you directly query the database you won't get the Vehicle object, but the data from the database.
From the documentation: http://datamapper.org/docs/find.html. It's near the bottom titled "Talking directly to your data-store"
Note that this will not return Zoo objects, rather the raw data straight from the database
Vehicle.joins(:user).order('users.name').all
or in Rails 2.3,
Vehicle.all(:joins => "inner join users on vehicles.user_id = user.id", :order => 'users.name')

Join and select multiple column

I'm trying to select multiple columns after doing a join. I couldn't find a way to do so using ActiveRecord without writing SQL between quotation marks in the query (Thing I'd like to avoid)
Exploring Arel, I've found I could select multiple columns using "project", however I'm not quite sure if I should use Arel directly or if there was a way to achieve the same with AR.
These is the code in Arel:
l = Location.arel_table
r = Region.arel_table
postcode_name_with_region_code = l.where(l[:id].eq(location_id)).join(r).on(l[:region_id].eq(r[:id])).project(l[:postcode_name], r[:code])
After running this query I'd like to return something along the lines of:
(Pseudo-code)
"#{:postcode_name}, #{:code}"
Is there a way to achieve the same query using AR?
If I stick to Arel, how can I get the values out of the SelectManager class the above query returns.
Thanks in advance,
Using AR, without writing any SQL and assuming your models and associations are:
models/region.rb
class Region < ActiveRecord::Base
has_many :locations
end
model/location.rb
class Location < ActiveRecord::Base
belongs_to :region
end
You can certainly do:
postcode_name_with_region_code = Location.where(:id=>location_id).includes(:region).collect{|l| "#{l.postcode_name}, #{l.region.code}"}
This will do the query and then use Ruby to format your result (note that it will return an array since I'm assuming there could be multiple records returned). If you only want one item of the array, you can use the array.first method to reference it.
You could also eager load the association and build your string from the result:
my_loc = Location.find(location_id, :include=>:region)
postcode_name_with_region_code = "#{my_loc.postcode_name}, #{my_loc.region.code}"
predicate = Location.where(:id=>location_id).includes(:region)
predicate.ast.cores.first.projections.clear
predicate.project(Location.arel_table[:postcode_name], Region.arel_table[:code])

Resources