1

Suppose there is a Rails model with a custom setter/accessor and a uniqueness constraint on the name column:

class Person < ActiveRecord::Base
  validates :name, presence: true, uniqueness: true

  def name=(name)
    # Example transformation only. 
    # Could be substituted for a more complex operation/transformation.
    title_cased = name.titleize 
    self[:name] = title_cased
  end

end

Now, consider the following:

Person.create! name: "John Citizen"
Person.find_or_create_by! name: "john citizen" # Error: validation fails

The find operation will not find any results, since there are no entries that match "john citizen". Then, the create! operation will throw an error as there is already an existing entry "John Citizen" (create! creates a new record and raises an exception if the validation fails).

How do you elegantly prevent such errors from occurring? For loose coupling and encapsulation purposes, is it possible to not transform names (to titlecase, in this case) before I perform operations like find_or_create_by! or other operations like find_by?

EDIT: As @harimohanraj alludes to, the issue seems to be around equivalence. Should the model transparently deal with the understanding/translating input to its boiled-down, canonical state. Or should this be the responsibility of consumers of the class/model?

Also, is active record callbacks a recommended approach to this kind of scenario?

stellarchariot
  • 2,872
  • 1
  • 19
  • 28

3 Answers3

2

If you have defined a custom setter method, the implicit decision that you have made is: values for the name attribute, no matter what form they come in (eg. a user's input in a text field), should be handled in titleized form in your DB. If that's the case, then it makes sense that find_or_create_by! name: 'john citizen' fails! In other words, your custom setter method represents your decision that "John Citizen" and "john citizen" are one and the same.

If you find yourself wanting to store John Citizen and john citizen in your DB, then I would revisit your decision to create a custom setter method. One cool way to achieve "loose coupling" is to put all of the logic that sanitizes data (ex. data from a user filling out a form) into a separate Ruby object.

There isn't much context in the question, so here is a bit of an abstract example to demonstrate what I mean.

# A class to house the logic of sanitizing your parameters
class PersonParamsSanitizer
  # It is initialized with dirty user parameters
  def initialize(params)
    @params = params
  end

  # It spits out neat, titleized params
  def sanitized_params
    {
      name: @params[:name].titleize
    }
  end
end

class PersonController < ApplicationController
  def create
    # Use your sanitizer object to convert dirty user parameters into neat
    # titleized params for your new perons
    sanitized_params = UserParamsSanitizer.new(params).sanitized_params

    person = Person.new(sanitized_params)

    if person.save
      redirect_to person
    else
      render :new
    end
  end
end

This way, you don't override the setter method in your User model, and are free to use find_or_create_by! fearlessly if you so choose!

0

The problem is the find_or_create_by and similar methods are already not tansforming the name... as you say there is no record "john citizen" but to work properly you'd need to titleize it for the find_or_create_by, find_or_create_by!, or find_by

(you don't need this solution for find as that only retrieves record by primary key)

so...

def self.find_or_create_by(options)
  super(rectify_options(options))
end

def self.find_or_create_by!(options)
  super(rectify_options(options))
end

def self.find_by(options)
  super(rectify_options(options))
end

private

def self.rectify_options(options)
  options[:name] = (new.name = options[:name]) if options[:name]
  options
end
SteveTurczyn
  • 36,057
  • 6
  • 41
  • 53
  • Thank you :) I guess this would solve the problem for `find_or_create_by`, but what about other other operations likes `find` or `find_or_create_by!` etc? I wasn't very clear in my question about this (sorry!) – stellarchariot Jan 22 '16 at 00:39
  • Hi, ok, question edited to extend for find_or_create_by! and `find_by` – SteveTurczyn Jan 22 '16 at 06:22
0

You can set a validation to be case-insensitive by using:

class Person < ActiveRecord::Base
  validates :name, 
            presence: true, 
            uniqueness: { case_sensitive: false }
end

However you also need a case-insensitive database index backing it since just using a validation in Rails will lead to race conditions. How to achieve that depends on the RBDMS.

Which leaves the issue of querying. The classic way of performing a intensive search is by WHERE LOWER(name) = LOWER(?). Although Postgres lets you use WHERE ILIKE name = ?.

If you want to encapsulate this into the model which is a good idea you would create a scope:

class Person
  scope :find_by_name, lambda{ |name| where('LOWER(name) = LOWER(?)', name) }
end

However, you cannot use .find_or_create_by! in this case as the query not just a hash. Instead you would call .first_or_create.

Person.find_by_name("John Citizen").first_or_create(attrs)

see also

Community
  • 1
  • 1
max
  • 96,212
  • 14
  • 104
  • 165
  • You could override the ActiveRecord finder methods to use case-insentive queries but its going to be a hacky mess. Instead I would document that the case insentive query method should be used. – max Jan 21 '16 at 00:20
  • Sorry, my question wasn't very clear. My example is contrived, and is not a specific to title casing. Instead of `titleize`, it could be a more complex operation and the output could be significantly different to the input. I've updated my question now. – stellarchariot Jan 22 '16 at 00:38