Module: Hoodoo::ActiveRecord::Writer

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

Overview

Support mixin for models subclassed from ActiveRecord::Base providing context-aware data writing, allowing service authors to auto-inherit persistence-related features from Hoodoo without changing their own code.

See individual module methods for examples, along with:

Dependency Hoodoo::ActiveRecord::ErrorMapping is also included automatically.

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(model) ⇒ Object

Instantiates this module when it is included.

Example:

class SomeModel < ActiveRecord::Base
  include Hoodoo::ActiveRecord::Writer
  # ...
end
model

The ActiveRecord::Base descendant that is including this module.



47
48
49
50
51
52
53
54
# File 'lib/hoodoo/active/active_record/writer.rb', line 47

def self.included( model )
  unless model == Hoodoo::ActiveRecord::Base
    model.send( :include, Hoodoo::ActiveRecord::ErrorMapping )
    instantiate( model )
  end

  super( model )
end

.instantiate(model) ⇒ Object

When instantiated in an ActiveRecord::Base subclass, all of the Hoodoo::ActiveRecord::Writer::ClassMethods methods are defined as class methods on the including class.

This module depends upon Hoodoo::ActiveRecord::ErrorMapping, so that will be auto-included first if it isn’t already.

model

The ActiveRecord::Base descendant that is including this module.



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/hoodoo/active/active_record/writer.rb', line 66

def self.instantiate( model )
  model.extend( ClassMethods )

  # See instance method "persist_in" for how this gets used.
  #
  model.validate do
    if @nz_co_loyalty_hoodoo_writer_db_uniqueness_violation == true
      errors.add( :base, 'has already been taken' )
    end
  end
end

Instance Method Details

#persist_in(context) ⇒ Object

Instance equivalent of Hoodoo::ActiveRecord::Writer::ClassMethods.persist_in - see that for details. The class method just calls here, having constructed an instance based on the attributes it was given. If you have already built an instance yourself, just call this instance method equivalent instead.

As an instance-based method, the return value and error handling semantics differ from the class-based counterpart. Instead of checking “persisted?”, check the return value of persist_in. This means you can also use persist_in to save a previously persisted, but now updated record, should you so wish.

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

  # ...maybe make other changes to model_instance, then...

  unless model_instance.persist_in( context ).equal?( :success )

    # 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.

Returns a Symbol of :success or :failure indicating the outcome of the same attempt. In the event of failure, the model will be invalid and not persisted; you can read errors immediately and should avoid unnecessarily re-running validations by calling valid? or validate on the instance.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/hoodoo/active/active_record/writer.rb', line 125

def persist_in( context )

  # If this model has an ActiveRecord uniqueness validation, it is
  # still subject to race conditions and MUST be backed by a database
  # constraint. If this constraint fails, try to re-run model
  # validations just in case it was a race condition case; though of
  # course, it could be that there is *only* a database constraint and
  # no model validation. If there is *only* a model validation, the
  # model is ill-defined and at risk.

  # TODO: This flag is nasty but seems unavoidable. Whenever you query
  #       the validity of a record, AR will always clear all errors and
  #       then (re-)run validations. We cannot just add an error to
  #       "base" and expect it to survive. Instead, it's necessary to
  #       use this flag to signal to the custom validator added in the
  #       'self.instantiate' implementation earlier that it should add
  #       an error. Trouble is, when do we clear the flag...?
  #
  #       This solution works but is inelegant and fragile.
  #
  @nz_co_loyalty_hoodoo_writer_db_uniqueness_violation = false

  # First just see if we have any problems saving anyway.
  #
  errors_occurred = begin
    self.transaction( :requires_new => true ) do
      :any unless self.save
    end
  rescue ::ActiveRecord::RecordNotUnique => error
    :duplication
  end

  # If an exception caught a duplication violation then either there is
  # a race condition on an AR-level uniqueness validation, or no such
  # validation at all. Thus, re-run validations with "valid?" and if it
  # still seems OK we must be dealing with a database-only constraint.
  # Set the magic flag (ugh, see earlier) to signal that when
  # validations run, they should add a relevant error to "base".
  #
  if errors_occurred == :duplication
    if self.valid?
      @nz_co_loyalty_hoodoo_writer_db_uniqueness_violation = true
      self.validate
    end
  end

  return errors_occurred.nil? ? :success : :failure
end