Question: Adding presence validation on existing records

Question

Adding presence validation on existing records

Answers 1
Added at 2017-01-05 00:01
Tags
Question

We have user records that have an attribute called first_name. Many of these records do no have the first_name attribute filled out and thus it equals nil. We want to introduce a presence validation on this attribute. However we've come across a huge problem. If a user updates their record during any request, that request will fail. This leads to a rather abrasive error that we don't know how to handle.

One solution is to only call the validation when the user is creating a record. This works great but we want to enforce this validation when they are on the profile page and they are attempting to update their profile.

Is there a better way to handle this where we can enforce first name requirements on the update page yet still allow users to update their record without ?

Answers
nr: #1 dodano: 2017-01-05 00:01

Introducing validations on existing data that does not satisfy the new requirements can be problematic. This concept you're after is fundamentally migration-on-write: You've introduce a data migration that happens over time as records are written to, because the migration cannot occur without individual user input. This is one technique for migrating very large data set in zero-downtime environments, or for forcing password resets on users.

Fundamentally, you need to define the conditions in which validation must happen and find a way to test records (on create or update) for that condition. Your condition should select all new records, plus the records being updated in the context where migration is possible.

Once you've defined the condition, you can modify your validation thusly:

validates :first_name, presence: true, if: -> { condition_for_migration }

Ideally the condition should be some field or combination of fields already present in your table that correctly identifies records as ready to be migrated, but this isn't always possible.

Failing that, you could introduce a field specifically for this purpose. You might call it version_number, set all existing records to 1, and then make the default for all new records 2. Your migration might look like this:

# All existing records will have their `version_number` set to the default of 1
add_column :users, :version_number: :integer, null: false, default: 1

# Change the default to 2 for any records created after this point
change_column_default :users, :version_number, 2

You can then use version_number to tell whether validation should take place:

validates :first_name, presence: true, if: -> { version_number >= 2 }

The key is to make sure that, in the context of your profile form, you also update version_number to enable the validation of first_name:

 # app/viws/users/edit.html.haml
 = form_for @user do |f|
   = f.hidden_field :version, value: 2
   = f.input :first_name

In the absence of a real database field for this purpose, you can add a temporary one to your model, which maintains the context only for the lifetime of a particular model instance:

  1. Add an accessor to your model, ie update_from_profile_page
  2. Include that field in the contexts in which you want to require validation
  3. Validate first_name during the creation of any new record
  4. Validate first_name during any update where update_from_profile_page is true

For example:

app/models/user.rb

class User < ActiveRecord::Base
  attr_accessor :update_from_profile_page


  validates :first_name, presence: true, on: :create
  validates :first_name, presence: true, on: :update, if: -> { update_from_profile_page }
end

app/views/user/edit.html.haml (your profile page)

= form_for @user do |f|
  = f.input :first_name

app/controllers/users_controller.rb

def update
  @user = User.find(params[:id])
  @user = update_from_profile_page = true
  @user.update(params.require(:user).permit(:first_name)
end

This is less desirable than finding a concrete business-logic-based reason for conditional validation as it involves introducing a virtual field to your model that has no functional value outside of a single specific case of a form submission.

Source Show
◀ Wstecz