Module: ActiveRecord::RailsDevsForDataIntegrity

Defined in:
lib/rails_devs_for_data_integrity.rb

Overview

Rails Devs For Data Integrity catches unique key and foreign key violations coming from the MySQLdatabase and converts them into an error on the ActiveRecord object similar to validation errors

class User < ActiveRecord::Base
  handle_unique_key_violation  :user_name, :message => 'is taken"
  handle_foreign_key_violation :primary_email_id, :message => 'is not available'
end

Instead of this nasty MySQL foreign key error:

ActiveRecord::StatementInvalid: Mysql::Error: Cannot add or update a child row:
a foreign key constraint fails (`zoo_development/animals`,
CONSTRAINT `fk_animal_species` FOREIGN KEY (`species_id`)
REFERENCES `species` (`id`) ON DELETE SET NULL ON UPDATE CASCADE)

>> user.errors.on(:user_name)
=> "association does not exist."

Or in the case of a unique key violation:

>> user.errors.on(:primary_email_id)
=> "is a duplicate."

Developers

Install

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object

:nodoc



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/rails_devs_for_data_integrity.rb', line 33

def self.included(base)#:nodoc
  base.send :class_inheritable_hash, :unique_key_check_options
  base.send :class_inheritable_hash, :foreign_key_check_options
  base.send :class_inheritable_hash, :default_violation_messages

  base.send :attr_reader,            :duplicate_exception
  base.send :attr_reader,            :foreign_key_exception

  base.unique_key_check_options   = {}
  base.foreign_key_check_options  = {}
  base.default_violation_messages = {
    :taken           => 'has already been taken',
    :taken_multiple  => "has already been taken for {{context}}",
    :taken_generic   => 'Duplicate field.',
    :foreign_key     => "association does not exist."
  }
  base.extend ClassMethods
end

Instance Method Details

#add_errors_for_violation(violation_type, columns) ⇒ Object



163
164
165
166
167
168
169
170
171
172
# File 'lib/rails_devs_for_data_integrity.rb', line 163

def add_errors_for_violation( violation_type, columns )
  columns = [columns].flatten.compact
  message = error_message_for_violation( violation_type, columns )

  if columns.blank?
    self.errors.add_to_base( message )
  else
    self.errors.add( columns.first, message )
  end
end

#add_foreign_key_error(exception, foreign_key = nil) ⇒ Object

Add a foreign key error message to errors based on the exception



197
198
199
200
# File 'lib/rails_devs_for_data_integrity.rb', line 197

def add_foreign_key_error(exception, foreign_key=nil)
  foreign_key ||= foreign_key_from_error_message( exception )
  add_errors_for_violation( :foreign_key, foreign_key )
end

#add_unique_key_error(exception, columns = nil) ⇒ Object

Add a duplicate error message to errors based on the exception



188
189
190
191
192
193
194
# File 'lib/rails_devs_for_data_integrity.rb', line 188

def add_unique_key_error( exception, columns = nil )
  columns ||= begin
    index = index_for_record_not_unique( exception )
    index.columns if index
  end
  add_errors_for_violation( :unique_key, columns )
end

#create_or_update_with_data_integrity_check(options = {}) ⇒ Object

do a create or update with data integrity check



260
261
262
263
264
# File 'lib/rails_devs_for_data_integrity.rb', line 260

def create_or_update_with_data_integrity_check(options={})
  execute_with_data_integrity_check(self) do
    return create_or_update_without_data_integrity_check
  end
end

#custom_error_message_for_violation(violation_type, columns) ⇒ Object

Custom error messages set with handle_violation



146
147
148
149
150
151
152
153
154
155
# File 'lib/rails_devs_for_data_integrity.rb', line 146

def custom_error_message_for_violation( violation_type, columns )#:nodoc:
  options = send(
    "#{violation_type}_check_options"
  )[ columns.first.to_sym ] if columns.any?

  message = case options[:message]
    when Symbol then Il8n.translate( options[:message] )
    else options[:message]
  end unless options.blank?
end

#default_error_message_for_violation(violation_type, columns) ⇒ Object

:nodoc:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/rails_devs_for_data_integrity.rb', line 128

def default_error_message_for_violation( violation_type, columns )#:nodoc:
  message_key = if violation_type == :foreign_key
    :foreign_key
  else
    case columns.length
      when 0 then :taken_generic
      when 1 then :taken
      else :taken_multiple
    end
  end
  I18n.translate(
    :"activerecord.errors.messages.#{message_key}",
    :default => default_violation_messages[ message_key ],
    :context => columns.slice(1..-1).join('/')
  )
end

#error_message_for_violation(violation_type, columns) ⇒ Object

Return the error message



158
159
160
161
# File 'lib/rails_devs_for_data_integrity.rb', line 158

def error_message_for_violation( violation_type, columns )#:nodoc:
  message = custom_error_message_for_violation( violation_type, columns )
  message ||= default_error_message_for_violation( violation_type, columns )
end

#execute_with_data_integrity_check(record = nil, &block) ⇒ Object

Executes the block and traps data integrity violations Populates the record errors objects with an appropriate message if such violation occurs

Example

def save_safe
 execute_with_data_integrity_check(self) { save }
end


249
250
251
252
253
254
255
256
257
# File 'lib/rails_devs_for_data_integrity.rb', line 249

def execute_with_data_integrity_check(record = nil, &block)
  @duplicate_exception = nil
  @foreign_key_exception = nil
  yield record
  true
rescue ActiveRecord::StatementInvalid => exception
  handle_data_integrity_error(exception, record)
  return false
end

#foreign_key_from_error_message(exception) ⇒ Object

Return the foreign key name from the foreign key exception



203
204
205
206
207
# File 'lib/rails_devs_for_data_integrity.rb', line 203

def foreign_key_from_error_message(exception)
  if (match = exception.to_s.match(/^Mysql::Error.*foreign key constraint fails.*FOREIGN KEY\s*\(`?([\w_]*)`?\)/))
    return match[1].dup
  end
end

#handle_data_integrity_error(exception, record = nil, &block) ⇒ Object

If exception is a unique key violation or a foreign key error, excute the block if it exists. If not and a record exists, add the appropriate error messages. Reraise any exceptions that are not data integrity violation errors Sometimes better to use execute_with_data_integrity_check block

exception - Exception thrown from save (insert or update) record - The activerecord object to add errors

Example

def save_safe

record = self
save

rescue ActiveRecord::StatementInvalid => exception

handle_data_integrity_error(exception, record)

end



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/rails_devs_for_data_integrity.rb', line 225

def handle_data_integrity_error(exception, record=nil, &block)
  @duplicate_exception = exception if exception.to_s =~ /^Mysql::Error: Duplicate entry/
  @foreign_key_exception = exception if exception.to_s =~ /^Mysql::Error.*foreign key constraint fails /

  if @duplicate_exception || @foreign_key_exception
    if block
      yield
    elsif record
      record.add_unique_key_error(exception)   if @duplicate_exception
      record.add_foreign_key_error(exception)  if @foreign_key_exception
      record
    end
  else
    raise exception
  end
end

#index_for_record_not_unique(exception) ⇒ Object

:nodoc:



174
175
176
177
178
179
180
181
182
183
184
# File 'lib/rails_devs_for_data_integrity.rb', line 174

def index_for_record_not_unique(exception) #:nodoc:
  case exception.message
    when /Duplicate entry.*for key (\d+)/
      index_position = $1.to_i
      # minus two b/c mysql message is one-based + rails excludes primary key index from indexes list
      ActiveRecord::Base.connection.indexes(self.class.table_name)[index_position - 2]
    when /Duplicate entry.*for key '(\w+)'/
      index_name = $1
      ActiveRecord::Base.connection.indexes(self.class.table_name).detect { |i| i.name == index_name }
  end
end

#save_with_data_integrity_check!(*args) ⇒ Object

save! with data integrity check RecordNotSaved will be thrown by save! before converting to the standard validation error ActiveRecord::RecordInvalid



269
270
271
272
273
274
# File 'lib/rails_devs_for_data_integrity.rb', line 269

def save_with_data_integrity_check!(*args)
  save_without_data_integrity_check!(*args)
rescue ActiveRecord::RecordNotSaved => e
  raise ActiveRecord::RecordInvalid.new(self) if @duplicate_exception||@foreign_key_exception
  raise e
end