How to build a dynamic scope in rails

48 views Asked by At

How do i construct a dynamic scope search given a variable-length array of elements to exclude, as in:

class Participant < ApplicationRecord

scope exclude_names, -> (['%name1%', '%name2%', '%name3%', ...]) {
  where.not(Participant.arel_table[:name_search].matches('%name1%').or(
   Participant.arel_table[:name_search].matches('%name2%').or(
     Participant.arel_table[:name_search].matches('%name3%').or(
...
}

but done dynamically as the name_list is of variable length.

2

There are 2 answers

0
mechnicov On BEST ANSWER

I suggest to use does_not_match method and accumulate AND conditions iterating through excluded names. You also don't need call explicitly class name inside the scope, because it is class method

class Participant < ApplicationRecord
  scope :exclude_names, ->(*names_to_exclude) {
    query = names_to_exclude.reduce(nil) do |q, name|
      condition = arel_table[:name_search].does_not_match("%#{name}%")
      q&.and(condition) || condition
    end

    where(query)
  }
end

After that you can call this scope

Participant.exclude_names('name1')
# SELECT * FROM participants
# WHERE name_search NOT LIKE '%name1%'

Participant.exclude_names('name1', 'name2')
# SELECT * FROM participants
# WHERE name_search NOT LIKE '%name1%'
# AND name_search NOT LIKE '%name2%'

Participant.exclude_names(%w[name1 name2])
# SELECT * FROM participants
# WHERE name_search NOT LIKE '%name1%'
# AND name_search NOT LIKE '%name2%'

Of course you can use OR like in your question, in this case it will be like this

class Participant < ApplicationRecord
  scope :exclude_names, ->(*names_to_exclude) {
    query = names_to_exclude.reduce(nil) do |q, name|
      condition = arel_table[:name_search].matches("%#{name}%")
      q&.or(condition) || condition
    end

    where.not(query)
  }
end

After that you can call this scope, compare with previous queries

Participant.exclude_names('name1')
# SELECT * FROM participants
# WHERE NOT (name_search LIKE '%name1%')

Participant.exclude_names('name1', 'name2')
# SELECT * FROM participants
# WHERE NOT (name_search LIKE '%name1%' OR name_search LIKE '%name2%')


Participant.exclude_names(%w[name1 name2])
# SELECT * FROM participants
# WHERE NOT (name_search LIKE '%name1%' OR name_search LIKE '%name2%')
0
Siim Liiser On

Create all the conditions in a loop and then combine them with or-s into your condition.

scope :exclude_names, -> (names) do
  clauses = names.map do |name|
    Participant.arel_table[:name_search].matches(name)
  end
  condition = clauses.inject(:or)
  where.not(condition)
end