Search code examples
ruby-on-railsrails-activerecordarel

How to build a dynamic scope in rails


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.


Solution

  • 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%')