How to collect and compare ids? - ruby-on-rails

I have 2 multi selectors in extJS (respondents groups and jsut respondents). Erery respondent have a group (ONLY 1).
here, how i selected my ids ...
I get respondent_group id, for example = 1
respondent_ids = params[:selected_respondents].split(/,/).collect {|r| r.to_i}
here i get respondents ids: 3,4,1
respondent_pure_ids = params[:selected_alone_respondents].split(/,/).collect {|r| r.to_i}
here i find respondents in group (for example i have 10 groups, every group has 1-3 respondents).
respondents = Respondent.find(:all, :conditions => ["respondent_group_id in (?) AND id NOT IN (?)", respondent_ids, session[:user].id])
I find respondents .
respondents_alone = Respondent.find(:all, :conditions => ["id in (?) AND id NOT IN (?)", respondent_pure_ids, session[:user].id])
here i found respondents (i find id where respondent_group = ?) and send them email.
respondents.each do |r|
Notifier.deliver_inquiry_notification()
end
What I want?
I get respondents and respondents_alone id's.
For example respondents = 3 , 4 , 6
respondents_ alone = 3, 5, 6, 8
I have 3 and 6 ids in both. I dont want to dublicate my data . How to check: if respondents ids DOES NOT equals respondent_alone ids I SEND EMAIL else error!

You can use ruby array arithmetic to get the difference between both arrays or to get every entry only once:
a = [3, 4, 6]
b = [3, 5, 6, 8]
a - b # [4] (only a without any element of b, difference)
b - a # [5, 8] (only b without any element of a, difference)
a | b # [3, 4, 6, 5, 8] (complete set of all ids, union)
a & b # [3, 6] (ids same in both arrays, intersection)
With this you can check if some ids are only in one array or in both, e.g. difference is empty or a|b==a&b => both are equal
cf. http://www.ruby-doc.org/core/classes/Array.html

Related

Query in ActiveRecord for objects that contain one or more ids from multiple arrays

I have Rails 5.2 project with three models:
class Post
has_many :post_tags
has_many :tags, through: :post_tags
end
class PostTags
belongs_to :post
belongs_to :tag
end
class Tags
has_many :post_tags
has_many :posts, through: :post_tags
end
I have the a number of arrays of tag ids, e.g:
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
array_3 = [9, 11, 13]
I want a query that will return posts that are tagged with at least one tag with an id from each of the arrays.
For instance, imagine I have a post with the following tag ids:
> post = Post.find(1)
> post.tag_ids
> [4, 8]
If I ran the query with array_1 and array_2 it would return this post. However if I ran it with array_1, array_2 and array_3 it would not return this post.
I attempted this with the following query:
Post.joins(:tags).where('tags.id IN (?) AND tags.id IN (?)', array_1, array_2)
But this does not return the post.
What should the query be to return the post?
Any help would be greatly appreciated!
Since you've tagged this question with postgresql you can perform the query you want using the intersect keyword. Unfortunately, activerecord doesn't natively support intersect so you'll have to build sql to use this method.
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
query = [array_1, array_2].map do |tag_ids|
Post.joins(:tags).where(tags: { id: tag_ids }).to_sql
end.join(' intersect ')
Post.find_by_sql(query)
Edit:
We can use subqueries to return the posts and maintain the activerecord relation.
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
Post
.where(post_tags: PostTag.where(tag_id: array_1))
.where(post_tags: PostTag.where(tag_id: array_2))
For bonus points, you can turn where(post_tag: PostTag.where(tag_id: array_1)) into a scope on Posts and chain as many of them as you'd like.
As mentioned by #NikhilVengal. You should be able to use intersection of 3 scoped queries like so
scopes = [array_1,array_2,array_3].map do |arr|
Post.joins(:post_tags).where(PostTag.arel_table[:tag_id].in(arr)).arel
end
subquery = scopes.reduce do |memo,scope|
# Arel::Nodes::Intersect.new(memo,scope)
memo.intersect(scope)
end
Post.from(Arel::Nodes::As.new(subquery,Post.arel_table))
This should return Post objects that are the intersection of the 3 queries.
Alternatively we can create 3 joins
joins = [array_1,array_2,array_3].map.with_index do |arr,idx|
alias = PostTag.arel_table.alias("#{PostTag.arel_table.name}_#{idx}")
Arel::Nodes::InnerJoin.new(
alias,
Arel::Nodes::On.new(
Post.arel_table[:id].eq(alias.arel_table[:post_id])
.and(alias.arel_table[:tag_id].in(arr))
)
)
end
Post.joins(joins).distinct
This will create 3 Inner joins with the Post table each being with the PostTag table filtered to the specific tag_ids ensuring that the Post will only show up if it exists in all 3 lists.
The use of AND in your where condition is checking for values that intersect (both arrays contain the same values).
array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
And will return results with id: 5 since it's in both arrays.
Using an OR will get you what you need. Either one of these should work for you:
Post.joins(:tags).where('tags.id IN (?) OR tags.id IN (?)', array_1, array_2)
OR
Post.joins(:tags).where(tags: { id: array_1 + array_2 })
my idea is that you could group by posts.id and sum all its tags position in the input array position, suppose you query with 3 group_tags then you have the result like this:
post_id group_tags_1 group_tags_2 group_tags_3 ....
1 2 0 0
2 1 1 1
so the final result is the Post with id 2 since it has at least one tag from each group.
def self.by_group_tags(group_tags)
having_enough_tags = \
group_tags.map do |tags|
sanitize_sql_array(["SUM(array_position(ARRAY[?], tags.id::integer)) > 0", tags])
end
Post
.joins(:tags)
.group("posts.id")
.having(
having_enough_tags.join(" AND ")
)
end
# Post.by_group_tags([[1,2], [3,4]])
# Post.by_group_tags([[1,2], [3,4], [5,6,7]])
update:
if you want to go to further chain and should not be effected by group, then just simple return a Relation that wrap all post ids you query from by_group_tags, such as a where as below
class Post
def self.by_group_tags(group_tags)
# ...
end
def self.scope_by_group_tags(group_tags)
post_ids = Post.by_group_tags(group_tags).pluck(:id)
Post.where(id: post_ids)
end
end
# Post.scope_by_group_tags([[1,2], [3,4]]).first(10)
the drawback: call query the same Posts twice.

Selecting a record where all associations are present

I have a user that has_many levels. Let's say I have levels 1, 2, 3, and 4.
How do I search for users that have associations with levels 2 AND 3 but not 4?
I haven't gotten for myself. I wanted to focus first on the part before 'but' however the line I came up with:
User.includes(:levels).where(levels: {id: [2, 3]})
returns also users that has only association with level 2, and not only the users with both levels 2 and 3.
Below will work if the order of ids obtained is as [2,3] only. For [3, 2] it will show no records. If the object are in sequence then it should work.
User.includes(:levels).map(&:levels).select { |l| l if l.ids == [2, 3] }
User.includes(:levels).where(levels: {id: 2}).where(levels: {id: 3})

Find model records by ID in the order the array of IDs were given

I have a query to get the IDs of people in a particular order, say:
ids = [1, 3, 5, 9, 6, 2]
I then want to fetch those people by Person.find(ids)
But they are always fetched in numerical order, I know this by performing:
people = Person.find(ids).map(&:id)
=> [1, 2, 3, 5, 6, 9]
How can I run this query so that the order is the same as the order of the ids array?
I made this task more difficult as I wanted to only perform the query to fetch people once, from the IDs given. So, performing multiple queries is out of the question.
I tried something like:
ids.each do |i|
person = people.where('id = ?', i)
But I don't think this works.
Editor's note:
As of Rails 5, find returns the records in the same order as the provided IDs (docs).
Note on this code:
ids.each do |i|
person = people.where('id = ?', i)
There are two issues with it:
First, the #each method returns the array it iterated on, so you'd just get the ids back. What you want is a collect
Second, the where will return an Arel::Relation object, which in the end will evaluate as an array. So you'd end up with an array of arrays. You could fix two ways.
The first way would be by flattening:
ids.collect {|i| Person.where('id => ?', i) }.flatten
Even better version:
ids.collect {|i| Person.where(:id => i) }.flatten
A second way would by to simply do a find:
ids.collect {|i| Person.find(i) }
That's nice and simple
You'll find, however, that these all do a query for each iteration, so not very efficient.
I like Sergio's solution, but here's another I would have suggested:
people_by_id = Person.find(ids).index_by(&:id) # Gives you a hash indexed by ID
ids.collect {|id| people_by_id[id] }
I swear that I remember that ActiveRecord used to do this ID ordering for us. Maybe it went away with Arel ;)
As I see it, you can either map the IDs or sort the result. For the latter, there already are solutions, though I find them inefficient.
Mapping the IDs:
ids = [1, 3, 5, 9, 6, 2]
people_in_order = ids.map { |id| Person.find(id) }
Note that this will cause multiple queries to be executed, which is potentially inefficient.
Sorting the result:
ids = [1, 3, 5, 9, 6, 2]
id_indices = Hash[ids.map.with_index { |id,idx| [id,idx] }] # requires ruby 1.8.7+
people_in_order = Person.find(ids).sort_by { |person| id_indices[person.id] }
Or, expanding on Brian Underwoods answer:
ids = [1, 3, 5, 9, 6, 2]
indexed_people = Person.find(ids).index_by(&:id) # I didn't know this method, TIL :)
people_in_order = indexed_people.values_at(*ids)
Hope that helps
If you have ids array then it is as simple as -
Person.where(id: ids).sort_by {|p| ids.index(p.id) }
OR
persons = Hash[ Person.where(id: ids).map {|p| [p.id, p] }]
ids.map {|i| persons[i] }
With Rails 5, I've found that this approach works (with postgres, at least), even for scoped queries, useful for working with ElasticSearch:
Person.where(country: "France").find([3, 2, 1]).map(&:id)
=> [3, 2, 1]
Note that using where instead of find does not preserve the order.
Person.where(country: "France").where(id: [3, 2, 1]).map(&:id)
=> [1, 2, 3]
There are two ways to get entries by given an array of ids. If you are working on Rails 4, dynamic method are deprecated, you need to look at the Rails 4 specific solution below.
Solution one:
Person.find([1,2,3,4])
This will raise ActiveRecord::RecordNotFound if no record exists
Solution two [Rails 3 only]:
Person.find_all_by_id([1,2,3,4])
This will not cause exception, simply return empty array if no record matches your query.
Based on your requirement choosing the method you would like to use above, then sorting them by given ids
ids = [1,2,3,4]
people = Person.find_all_by_id(ids)
# alternatively: people = Person.find(ids)
ordered_people = ids.collect {|id| people.detect {|x| x.id == id}}
Solution [Rails 4 only]:
I think Rails 4 offers a better solution.
# without eager loading
Person.where(id: [1,2,3,4]).order('id DESC')
# with eager loading.
# Note that you can not call deprecated `all`
Person.where(id: [1,2,3,4]).order('id DESC').load
You can get users sorted by id asc from the database and then rearrange them in the application any way you want. Check this out:
ids = [1, 3, 5, 9, 6, 2]
users = ids.sort.map {|i| {id: i}} # Or User.find(ids) or another query
# users sorted by id asc (from the query)
users # => [{:id=>1}, {:id=>2}, {:id=>3}, {:id=>5}, {:id=>6}, {:id=>9}]
users.sort_by! {|u| ids.index u[:id]}
# users sorted as you wanted
users # => [{:id=>1}, {:id=>3}, {:id=>5}, {:id=>9}, {:id=>6}, {:id=>2}]
The trick here is sorting the array by an artificial value: index of object's id in another array.
I here summarise the solutions, plus adding recent (9.4+) PostgreSQL-specific solution. The following is based on Rails 6.1 and PostgreSQL 12. Though I mention solutions for earlier versions of Rails and PostgreSQL, I haven't actually tested them with earlier versions.
For reference, this question "ORDER BY the IN value list" gives various ways of sorting/ordering with the database.
Here, I assume the model is guaranteed to have all the records specified by the Array of IDs, ids. Otherwise, an exception like ActiveRecord::RecordNotFound may be raised (or may not, depending on the way).
What does NOT work
Person.where(id: ids)
The order of the returned Relation is either arbitrary or that of the numerical values of the primary IDs; whichever, it usually does not agree with that of ids.
Simple solution to get an Array
(Rails 5+ only(?))
Person.find ids
which returns a Ruby Array of Person models in the order of the given ids.
A downside is you cannot further modify the result with SQL.
In Rails 3, the following is the way apparently, though this may not work (certainly does not in Rails 6) in the other versions of Rails.
Person.find_all_by_id ids
Pure Ruby solution to get an Array
Two ways. Either works regardless of Rails versions (I think).
Person.where(id: ids).sort_by{|i| ids.index(i.id)}
Person.where(id: ids).index_by(&:id).values_at(*ids)
which returns a Ruby Array of Person models in the order of the given ids.
DB-level solution to get a Relation
All of the following return Person::ActiveRecord_Relation, to which you can apply more filters if you like.
In the following solutions, all records are preserved, including those whose IDs are not included in the given array ids. You can filter them out any time by adding where(id: ids) (this sort of flexibility is a beauty of ActiveRecord_Relation).
For any Database
Based on user3033467's answer but updated to work with Rails 6 (which has disabled some features with order() due to a security concern; see "Updates for SQL Injection in Rails 6.1" by Justin for the background).
order_query = <<-SQL
CASE musics.id
#{ids.map.with_index { |id, index| "WHEN #{id} THEN #{index}" } .join(' ')}
ELSE #{ids.length}
END
SQL
Person.order(Arel.sql(order_query))
For MySQL specific
From Koen's answer (I haven't tested it).
Person.order(Person.send(:sanitize_sql_array, ['FIELD(id, ?)', ids])).find(ids)
For PostgreSQL specific
PostgreSQL 9.4+
join_sql = "INNER JOIN unnest('{#{ids.join(',')}}'::int[]) WITH ORDINALITY t(id, ord) USING (id)"
Person.joins(join_sql).order("t.ord")
PostgreSQL 8.2+
Based on Jerph's answer, but LEFT JOIN is replaced with INNER JOIN:
val_ids = ids.map.with_index.map{|id, i| "(#{id}, #{i})"}.join(", ")
Person.joins("INNER JOIN (VALUES #{val_ids}) AS persons_id_order(id, ordering) ON persons.id = persons_id_order.id")
.order("persons_id_order.ordering")
To get lower-level objects
The following is solutions to get lower-level objects.
In a vast majority of cases, the solutions described above must be superior to these, but am putting there here for the sake of completeness (and record before I found better solutions)…
In the following solutions, the records that do not match IDs in ids are filtered out, unlike the solutions described in the previous section (where all records can be chosen to be preserved).
To get an ActiveRecord::Result
This is a solution to get ActiveRecord::Result with PostgreSQL 9.4+.
ActiveRecord::Result is similar to an Array of Hash.
str_sql = "select persons.* from persons INNER JOIN unnest('{#{ids.join(',')}}'::int[]) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord;"
Person.connection.select_all(str_sql)
Person.connection.exec_query returns the same (alias?).
To get a PG::Result
This is a solution to get PG::Result with PostgreSQL 9.4+. Very similar to above, but replace exec_query with execute (the first line is identical to the solution above):
str_sql = "select persons.* from persons INNER JOIN unnest('{#{ids.join(',')}}'::int[]) WITH ORDINALITY t(id, ord) USING (id) ORDER BY t.ord;"
Person.connection.execute(str_sql)
Old question, but the sorting can be done by ordering using the SQL FIELD function. (Only tested this with MySQL.)
So in this case something like this should work:
Person.order(Person.send(:sanitize_sql_array, ['FIELD(id, ?)', ids])).find(ids)
Which results in the following SQL:
SELECT * FROM people
WHERE id IN (1, 3, 5, 9, 6, 2)
ORDER BY FIELD(id, 1, 3, 5, 9, 6, 2)
Most of the other solutions don't allow you to further filter the resulting query, which is why I like Koen's answer.
Similar to that answer but for Postgres, I add this function to my ApplicationRecord (Rails 5+) or to any model (Rails 4):
def self.order_by_id_list(id_list)
values_clause = id_list.each_with_index.map{|id, i| "(#{id}, #{i})"}.join(", ")
joins("LEFT JOIN (VALUES #{ values_clause }) AS #{ self.table_name}_id_order(id, ordering) ON #{ self.table_name }.id = #{ self.table_name }_id_order.id")
.order("#{ self.table_name }_id_order.ordering")
end
The query solution is from this question.
This is most efficiently handled in SQL via ActiveRecord and not in Ruby.
ids = [3,1,6,7,12,2]
Post.where(id: ids).order("FIELD(id, #{ids.join(',')})")
This simple solution costs less than joining on values:
order_query = <<-SQL
CASE persons.id
#{ids.map.with_index { |id, index| "WHEN #{id} THEN #{index}" } .join(' ')}
ELSE #{ids.length}
END
SQL
Person.where(id: ids).order(order_query)
To get the IDs of people in a particular order, say: ids = [1, 3, 5, 9, 6, 2]
In older version of rails, find and where fetch data in numerical order, but rails 5 fetch data in the same order in which you query it
Note: find preserve the order and where don't preserve it
Person.find(ids).map(&:id)
=> [1, 3, 5, 9, 6, 2]
Person.where(id: ids).map(&:id)
=> [1, 2, 3, 5, 6, 9]
But they are always fetched in numerical order, I know this by performing:
I tried the answers recommending the FIELD method on Rails6 but was encountering errors. However, I discovered that all one has to do is wrap the sql in Arel.sql().
# Make sure it's a known-safe values.
user_ids = [3, 2, 1]
# Before
users = User.where(id: user_ids).order("FIELD(id, 2, 3, 1)")
# With warning.
# After
users = User.where(id: user_ids).order(Arel.sql("FIELD(id, 2, 3, 1)"))
# No warning
[1] https://medium.com/#mitsun.chieh/activerecord-relation-with-raw-sql-argument-returns-a-warning-exception-raising-8999f1b9898a
Use find:
Thing.find([4, 2, 6])
For Rails 7:
Thing.where(id: [4, 2, 6]).in_order_of(:id, [4, 2, 6])
See https://hashrocket.com/blog/posts/return-results-using-a-specific-order-in-rails

Find same values between an array and an active record variable in ruby

Should be an easy one and might have over complicated the title somewhat.
I have a variable that contains records:
#record = Records.all
and an array that holds some task_id's:
#array #has for e.g. [1,2,3]
What i want to do check the task_id column of the list of records in #records to see if they contain any of the numbers in the array. If they do then i want those numbers to be put into another array.
i know this is simple but i keep getting confused along the way as im quite new to ruby syntax.
This should work for you:
#records.map(&:task_id) & #array
This builds the intersection of the two lists (task_ids and array). You can try this example at the console (I hope this helps to clear up how it works):
irb(main):008:0> a = [1,2,3,4]
=> [1, 2, 3, 4]
irb(main):009:0> b = [3,4,5,6]
=> [3, 4, 5, 6]
irb(main):010:0> a & b
=> [3, 4]

Rails - :order by calculated data

I have multiple instances of data displayed in tables that need to be sorted - much of this data is calculated from the table and isn't just a raw value in the table.
A simple example: Column A = User.visits / User.membership_term
I'm using sortable table columns: http://railscasts.com/episodes/228-sortable-table-columns.
I've tried putting the calculation in the controller and adding a class method to my model but neither seems to work. How can I sort by a calculated field?
You can always use the basic sort method:
irb(main):001:0> a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb(main):002:0> a.sort {|d,e| (d - 3).abs <=> (e - 3).abs}
=> [3, 2, 4, 1, 5] # sort by closest distance away from the number 3
So you can sort your array of active records using similar ways.

Resources