Fetching unique records for through relationship - ruby-on-rails

I have following models
class Tale < ActiveRecord::Base
has_many :tale_moral_joins
has_many :morals, through: :tale_moral_joins
has_many :values, through: :morals
the following code returns duplicate ids. I know why, but I want to know how to change that and get only uniqs
tale = Tale.first
tale.association(:values).ids_reader # => [1,2,2,3,3,4]
even if i do
tale.value_ids
the same happens.
I know that the tale first gets the related morals and for each of them the related values. hence duplicate ids. What can be done by way of configuration rather than use .uniq on the returned array

tale.association(:values).ids_reader.uniq
For example
[1,2,2,3,3,4].uniq
This will return => [1, 2, 3, 4]

You can use distinct on the relation to SELECT unique records (SQL uses SELECT DISTINCT):
tale.values.distinct
You can also use pluck with "DISTINCT id":
tale.values.pluck("DISTINCT id")
This avoids an unnecessary GROUP BY statement, of which there can only be one per query.

That should do the trick:
tale = Tale.first
tale.values.group(:id).pluck(:id)
Records will be filtered by the database engine, not the Ruby interpreter. In most cases that is preferable.

Related

Find records with at least one association but exclude records where any associations match condition

In the following setup a customer has many tags through taggings.
class Customer
has_many :taggings
has_many :tags, through: :taggings
end
class Tagging
belongs_to :tag
belongs_to :customer
end
The query I'm trying to perform in Rails with postgres is to Find all customers that have at least one tag but don't have either of the tags A or B.
Performance would need to be taken into consideration as there are tens of thousands of customers.
Please try the following query.
Customer.distinct.joins(:taggings).where.not(id: Customer.joins(:taggings).where(taggings: {tag_id: [tag_id_a,tag_id_b]}).distinct )
Explanation.
Joins will fire inner join query and will make sure you get only those customers which have at least one tag associated with them.
where.not will take care of your additional condition.
Hope this helps.
Let tag_ids is array of A and B ids:
tag_ids = [a.id, b.id]
Then you need to find the Customers, which have either A or B tag:
except_relation = Customer.
joins(:tags).
where(tags: { id: tag_ids }).
distinct
And exclude them from the ones, which have at least one tag:
Customer.
joins(:tags).
where.not(id: except_relation).
distinct
INNER JOIN, produced by .joins, removes Customer without Tag and is a source of dups, so distinct is needed.
UPD: When you need performance, you probably have to change your DB schema to avoid extra joins and indexes.
You can search examples of jsonb tags implementation.
Get ids of tag A and B
ids_of_tag_a_and_b = [Tag.find_by_title('A').id, Tag.find_by_title('B').id]
Find all customers that have at least one tag but don't have either of the tags A or B.
#Customer.joins(:tags).where.not("tags.id in (?)", ids_of_tag_a_and_b)
Customer.joins(:tags).where.not("tags.id = ? OR tags.id = ?", tag_id_1, tag_id_2)

Fatch records on the bases of has_and_belongs_to_many association -Rails

I have a model Lodging which has association has_and_belongs_to_many :amenities Now I want a single query which return all lodgings which has amenities with ids in an array like [2,1,3]
Thanks!
You need to use joins.
Lodging.joins(:amenities).where(amenities: { id: [2, 1, 3] })
It may also be worthwhile to mention that joins, by default, uses an INNER JOIN, which means that doing just Lodging.joins(:amenities) will exclude all lodgings without an any amenities.

Disable Rails STI for certain ActiveRecord queries only

I can disable STI for a complete model but I'd like to just disable it when doing certain queries. Things were working swell in Rails 3.2, but I've upgraded to Rails 4.1.13 and a new thing is happening that's breaking the query.
I have a base class called Person with a fairly complex scope.
class Person < ActiveRecord::Base
include ActiveRecord::Sanitization
has_many :approvals, :dependent => :destroy
has_many :years, through: :approvals
scope :for_current_or_last_year, lambda {
joins(:approvals).merge(Approval.for_current_or_last_year) }
scope :for_current_or_last_year_latest_only, ->{
approvals_sql = for_current_or_last_year.select('person_id AS ap_person_id, MAX(year_id) AS max_year, year_id AS ap_year_id, status_name AS ap_status_name, active AS ap_active, approved AS ap_approved').group(:id).to_sql
approvals_sql = select("*").from("(#{approvals_sql}) AS ap, approvals AS ap2").where('ap2.person_id = ap.ap_person_id AND ap2.year_id = ap.max_year').to_sql
select("people.id, ap_approved, ap_year_id, ap_status_name, ap_active").
joins("JOIN (#{approvals_sql}) filtered_people ON people.id =
filtered_people.person_id").uniq
}
end
And inheriting classes called Member and Staff. The only thing related to this I had to comment out to get my tests to pass with Rails 4. It may be the problem, but uncommenting it hasn't helped in this case.
class Member < Person
#has_many :approvals, :foreign_key => 'person_id', :dependent => :destroy, :class_name => "MemberApproval"
end
The problem happens when I do the query Member.for_current_or_last_year_latest_only
I get the error unknown column 'people.type'
When I look at the SQL, I can see the problem line but I don't know how to remove it or make it work.
Member.for_current_or_last_year_latest_only.to_sql results in.
SELECT DISTINCT people.id, ap_approved, ap_year_id, ap_status_name, ap_active
FROM `people`
JOIN (SELECT * FROM (SELECT person_id AS ap_person_id, MAX(year_id) AS max_year, year_id AS ap_year_id, status_name AS ap_status_name, active AS ap_active, approved AS ap_approved
FROM `people` INNER JOIN `approvals` ON `approvals`.`person_id` = `people`.`id`
WHERE `people`.`type` IN ('Member') AND ((`approvals`.`year_id` = 9 OR `approvals`.`year_id` = 8))
GROUP BY `people`.`id`) AS ap, approvals AS ap2
WHERE `people`.`type` IN ('Member') AND (ap2.person_id = ap.ap_person_id AND ap2.year_id = ap.max_year)) filtered_people ON people.id = filtered_people.person_id
WHERE `people`.`type` IN ('Member')
If I remove people.type IN ('Member') AND from the beginning of the second to last WHERE clause the query runs successfully. And btw, that part isn't in the query generated from the old Rails 3.2 code, neither is the one above it (only the last one matches the Rails 3.2 query). The problem is, that part is being generated from rails Single Table Inheritance I assume, so I can't just delete it from my query. It's not the only place that is getting added into the original query, but that's the only one that is causing it to break.
Does anybody have any idea how I can either disable STI for only certain queries or add something to my query that will make it work? I've tried putting people.type in every one of the SELECT queries to try and make it available but to no avail.
Thanks for taking the time to look at this.
I was apparently making this harder than it really was...I just needed to add unscoped to the front of the two approval_sql sub-queries. Thanks for helping my brain change gears.

Rails query a has_many :through conditionally with multiple ids

I'm trying to build a filtering system for a website that has locations and features through a LocationFeature model. Basically what it should do is give me all the locations based on a combination of feature ids.
So for example if I call the method:
Location.find_by_features(1,3,4)
It should only return the locations that have all of the selected features. So if a location has the feature_ids [1, 3, 5] it should not get returned, but if it had [1, 3, 4, 5] it should. However, currently it is giving me Locations that have either of them. So in this example it returns both, because some of the feature_ids are present in each of them.
Here are my models:
class Location < ActiveRecord::Base
has_many :location_features, dependent: :destroy
has_many :features, through: :location_features
def self.find_by_features(*ids)
includes(:features).where(features: {id: ids})
end
end
class LocationFeature < ActiveRecord::Base
belongs_to :location
belongs_to :feature
end
class Feature < ActiveRecord::Base
has_many :location_features, dependent: :destroy
has_many :locations, through: :location_features
end
Obviously this code isn't working the way I want it to and I just can't get my head around it. I've also tried things such as:
Location.includes(:features).where('features.id = 5 AND features.id = 9').references(:features)
but it just returns nothing. Using OR instead of AND give me either again. I also tried:
Location.includes(:features).where(features: {id: 9}, features: {id: 1})
but this just gives me all the locations with the feature_id of 1.
What would be the best way to query for a location matching all the requested features?
When you do an include it makes a "pseudo-table" in memory which has all the combinations of table A and table B, in this case joined on the foreign_key. (In this case there's already a join table included (feature_locations), to complicate things.)
There won't be any rows in this table which satisfy the condition features.id = 9 AND features.id = 1. Each row will only have a single features.id value.
What i would do for this is forget about the features table: you only need to look in the join table, location_features, to test for the presence of specific feature_id values. We need a query which will compare feature_id and location_id from this table.
One way is to get the features, then get a collection of arrays if associated location_ids (which just calls the join table), then see which location ids are in all of the arrays: (i've renamed your method to be more descriptive)
#in Location
def self.having_all_feature_ids(*ids)
location_ids = Feature.find_all_by_id(ids).map(&:location_ids).inject{|a,b| a & b}
self.find(location_ids)
end
Note1: the asterisk in *ids in the params means that it will convert a list of arguments (including a single argument, which is like a "list of one") into a single array.
Note2: inject is a handy device. it says "do this code between the first and second elements in the array, then between the result of this and the third element, then the result of this and the fourth element, etc, till you get to the end. In this case the code i'm doing between the two elements in each pair (a and b) is "&" which, when dealing with arrays, is the "set intersection operator" - this will return only elements which are in both pairs. By the time you've gone through the list of arrays doing this, only elements which are in ALL arrays will have survived. These are the ids of locations which are associated with ALL of the given features.
EDIT: i'm sure there's a way to do this with a single sql query - possibly using group_concat - which someone else will probably post shortly :)
I would do this as a set of subqueries. You can actually also do it as a scope if you wish.
scope :has_all_features, ->(*feature_ids) {
where( ( ["locations.id in (select location_id from location_features where feature_id=?)"] * feature_ids.count).join(' and '), *feature_ids)
}

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.

Resources