Module: Hoodoo::ActiveRecord::Writer::ClassMethods

Defined in:
lib/hoodoo/active/active_record/writer.rb

Overview

Collection of class methods that get defined on an including class via Hoodoo::ActiveRecord::Writer::included.

Instance Method Summary collapse

Instance Method Details

#persist_in(context, attributes) ⇒ Object

Overview

Service authors SHOULD use this method when persisting data with ActiveRecord if there is a risk of duplication constraint violation of any kind. This will include a violation on the UUID of a resource if you support external setting of this value via the body of a create call containing the id field, injected by Hoodoo as the result of an authorised use of the X-Resource-UUID HTTP header.

Services often run in highly concurrent environments and uniqueness constraint validations with ActiveRecord cannot protect against race conditions in such cases. IT works at the application level; the check to see if a record exists with a duplicate value in some given column is a separate operation from that which stores the record subsequently. As per the Rails Guides entry on the uniqueness validation at the time of writing:

guides.rubyonrails.org/active_record_validations.html#uniqueness

“It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index on both columns in your database.”

You MUST always use a uniqueness constraint at the database level and MAY additionally use ActiveRecord validations for a higher level warning in all but race condition edge cases. If you then use this persist_in method to store records, all duplication cases will be handled elegantly and reported as a generic.invalid_duplication error. In the event that a caller has used the X-Deja-Vu HTTP header, Hoodoo will take such an error and transform it into a non-error 204 HTTP response; so by using persist_in, you also ensure that your service participates successfully in this process without any additional coding effort. You get safe concurrency and protection against the inherent lack of idempotency in HTTP POST operations via any must-be-unique fields (within your defined scope) automatically.

Using this method for data storage instead of plain ActiveRecord save or save! will also help your code auto-inherit any additional future write-related enhancements in Hoodoo should they arise, without necessarily needing service code changes.

Example

class Unique < ActiveRecord::Base
  include Hoodoo::ActiveRecord::Writer
  validates :unique_code, :presence => true, :uniqueness => true
end

The migration to create the table for the Unique model MUST have a uniqueness constraint on the unique_code field, e.g.:

def change
  add_column :uniques, :unique_code, :null => false
  add_index :uniques, [ :unique_code ], :unique => true
end

Then, inside the implementation class which uses the above model, where you have (say) written private methods mapping_of which maps context.request.body to an attributes Hash for persistence and rendering_of which uses Hoodoo::Presenters::Base.render_in to properly render a representation of your resource, you would write:

def create( context )
  attributes = mapping_of( context.request.body )
  model_instance = Unique.persist_in( context, attributes )

  unless model_instance.persisted?

    # Error condition. If you're using the error handler mixin
    # in Hoodoo::ActiveRecord::ErrorMapping, do this:
    #
    context.response.add_errors( model_instance.platform_errors )
    return # Early exit

  end

  # ...any other processing...

  context.response.set_resource( rendering_of( context, model_instance ) )
end

Parameters

context

Hoodoo::Services::Context instance describing a call context. This is typically a value passed to one of the Hoodoo::Services::Implementation instance methods that a resource subclass implements.

attributes

Attributes hash to be passed to this model class’s constructor, via self.new( attributes ).

See also the Hoodoo::ActiveRecord::Writer#persist_in instance method equivalent of this class method.

Nested transaction note

Ordinarily an exception in a nested transaction does not roll back. ActiveRecord wraps all saves in a transaction “out of the box”, so the following construct could have unexpected results…

Model.transaction do
  instance.persist_in( context )
end

…if instance.valid? runs any SQL queries - which is very likely. PostgreSQL, for example, would then raise an exception; the inner transaction failed, leaving the outer one in an aborted state:

PG::InFailedSqlTransaction: ERROR:  current transaction is
aborted, commands ignored until end of transaction block

ActiveRecord provides us with a way to define a transaction that does roll back via the requires_new: true option. Hoodoo thus protects callers from the above artefacts by ensuring that all saves are wrapped in an outer transaction that causes rollback in any parents. This sidesteps the unexpected behaviour, but service authors might sometimes need to be aware of this if using complex transaction behaviour along with persist_in.

In pseudocode, the internal implementation is:

self.transaction( :requires_new => true ) do
  self.save
end


311
312
313
314
315
316
# File 'lib/hoodoo/active/active_record/writer.rb', line 311

def persist_in( context, attributes )
  instance = self.new( attributes )
  instance.persist_in( context )

  return instance
end