Remove extra white spaces.all

Ever added the uniqueness validation to a model

class Person < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end

but just adding the above alone will not prevent the duplicates

Soon you will start seeing duplicates and you’ll wonder how’s that happening. After spending some time (or hours) debugging you will notice that users are adding extra white spaces to bypass the uniqueness validation. Smart hn!!

Bagel & Cream Cheese
Bagel & Cream  Cheese
Bagel  & Cream Cheese

Means now you have to take care of extra white space, and in many models. Lets create a module to handle that.

module StripedColumns
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    StripedColumnsDefaultOptions = { squeeze: false }
    
    # add has_striped_columns as a class method
    def has_striped_columns(options = StripedColumnsDefaultOptions)
      options[:squeeze] = { string: true, text: true } if options[:squeeze] == true

      before_validation do
        self.class.columns.each do |column| 
          # only run for String or Text columns
          next unless column.type.in? %i(string text)
          # make sure value is not nil
          next unless value = self.send(column.name)
          # run :if condition passed as options
          next unless options[:if].to_proc.call(self) if options[:if]
          
          # check if value is a string
          if value.kind_of?(String)
            # remove white spaces around the text
            value = value.strip
            # grab the value after running the options
            value = apply_striped_columns_options(column, value, options)
            # assign the new value to the column
            self.send("#{column.name}=", value)
          end
        end
      end
    end
  end

private

  def apply_striped_columns_options(column, string, options)
    if squeeze_opts = options[:squeeze]
      if (squeeze_opts[:string] == true && column.type == :string) ||
        (squeeze_opts[:text] == true && column.type == :text)
        # remove extra white spaces between words
        string = string.squeeze(" ")
      end
    end
    string
  end
end

# add it to ActiveRecord::Base to make it available to all the models
ActiveRecord::Base.include(StripedColumns)

That little module will handle all the extra white space issues

class Person < ApplicationRecord
  # has_striped_columns                    # remove spaces around
  # has_striped_columns, if: :is_true?     # remove spaces around only if is true
  # has_striped_columns, squeeze: true     # remove spaces around + remove extra space between words
  # has_striped_columns, squeeze: :string  # remove spaces around + remove extra space between words for string column only
  # has_striped_columns, squeeze: :text    # remove spaces around + remove extra space between words for text column only

  validates :name, uniqueness: { case_sensitive: false }
end

Most recent record

We have two models Sensor and Reading.

class Sensor < ApplicationRecord
  has_many :readings
end
class Reading < ApplicationRecord
  # integer :status
  # float :value
  # integer :sensor_id
end

And we want to select the latest reading for a sensor. We can do

Sensor.find(params[:id]).readings.last # default ordering id DESC

Now if we have 10 sensors and every sensor has over 10,000 readings and we want to display the latest reading for all the sensors.

Doing this will generate n+1 queries

# controller
@sensors = Sensor.all

# view
@sensors.each do |sensor|
  sensor.reading.last
....

and we don’t to want eager load all 10K records

Sensor.includes(:readings)

Instead we can create a has_one association in the Sensor model

# Note: DISTINCT ON is only available in Postgres

class Sensor < ApplicationRecord
  has_many :readings
  has_one :latest_reading, -> { select("DISTINCT ON(sensor_id) *").order("sensor_id, id DESC") }, class_name: 'Reading'
end

Now we can eager load only the latest readings 😉

Sensor.includes(:latest_reading)