module NoBrainer::Criteria::FirstOrCreate
Public Instance Methods
first_or_create(create_params={}, save_options={}, &block)
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 4 def first_or_create(create_params={}, save_options={}, &block) _first_or_create(create_params, save_options.merge(:save_method => :save?), &block) end
first_or_create!(create_params={}, save_options={}, &block)
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 8 def first_or_create!(create_params={}, save_options={}, &block) _first_or_create(create_params, save_options.merge(:save_method => :save!), &block) end
upsert(attrs, save_options={})
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 12 def upsert(attrs, save_options={}) _upsert(attrs, save_options.merge(:save_method => :save?, :update => true)) end
upsert!(attrs, save_options={})
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 16 def upsert!(attrs, save_options={}) _upsert(attrs, save_options.merge(:save_method => :save!, :update => true)) end
Private Instance Methods
_first_or_create(create_params, save_options, &block)
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 42 def _first_or_create(create_params, save_options, &block) raise "Cannot use .raw() with .first_or_create()" if raw? if block && block.arity == 1 raise "When passing a block to first_or_create(), you must pass a block with no arguments.\n" + "The passed block must return a hash of additional attributes for create()" end save_method = save_options.delete(:save_method) should_update = save_options.delete(:update) where_params = extract_where_params() # Note that we are not matching a subset of the keys on the uniqueness # validators; we need an exact match on the keys. keys = where_params.keys unless get_model_unique_fields.include?(keys.sort) # We could do without a uniqueness validator, but it's much preferable to # have it, so that we don't conflict with others create(), not just others # first_or_create(). raise "Please add the following uniqueness validator for first_or_create():\n" + "class #{model}\n" + case keys.size when 1 then " field :#{keys.first}, :uniq => true" when 2 then " field :#{keys.first}, :uniq => {:scope => :#{keys.last}}" else " field :#{keys.first}, :uniq => {:scope => #{keys[1..-1].inspect}}" end + "\nend" end unless model.is_root_class? || (model.superclass.fields.keys & keys).empty? # We can't allow the parent to share the keys we are matching on. # Consider this case: # - Base has the field :name, :uniq => true declared # - A < Base # - B < Base # - A.create(:name => 'x'), # - B.where(:name => 'x').first_or_create # We are forced to return nil, or raise. parent = model parent = parent.superclass while parent.superclass < NoBrainer::Document && !(parent.superclass.fields.keys & keys).empty? raise "A polymorphic problem has been detected: The fields `#{keys.inspect}' are defined on `#{parent}'.\n" + "This is problematic as first_or_create() could return nil in some cases.\n" + "Either 1) Only define `#{keys.inspect}' on `#{model}', \n" + "or 2) Query the superclass, and pass :_type in first_or_create() as such:\n" + " `#{parent}.where(...).first_or_create(:_type => \"#{model}\")'." end # We don't want to access create_params yet, because invoking the block # might be costly (the user might be doing some API call or w/e), and # so we want to invoke the block only if necessary. new_instance = model.new(where_params) lock_key_name = model._uniqueness_key_name_from_params(where_params) new_instance._lock_for_uniqueness_once(lock_key_name) old_instance = self.first if old_instance if should_update old_instance.assign_attributes(create_params) old_instance.__send__(save_method, save_options) end return old_instance end create_params = block.call if block create_params = create_params.symbolize_keys keys_in_conflict = create_params.keys & where_params.keys keys_in_conflict = keys_in_conflict.reject { |k| create_params[k] == where_params[k] } unless keys_in_conflict.empty? raise "where() and first_or_create() were given conflicting values " + "on the following keys: #{keys_in_conflict.inspect}" end if create_params[:_type] # We have to recreate the instance because we are given a _type in # create_params specifying a subclass. We'll have to transfert the lock # ownership to that new instance. new_instance = model.model_from_attrs(create_params).new(where_params).tap do |i| i.locked_keys_for_uniqueness = new_instance.locked_keys_for_uniqueness end end new_instance.assign_attributes(create_params) new_instance.__send__(save_method, save_options) return new_instance ensure new_instance.try(:unlock_unique_fields) end
_upsert(attrs, save_options)
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 22 def _upsert(attrs, save_options) # Note: we don't do a full cast of user_to_cast because where() takes care # of it. We just need to locate a suitable uniqueness validator, for which # we just need to translate keys. attrs = Hash[attrs.map { |k,v| model.association_user_to_model_cast(k.to_sym, v) }] unique_keys = get_model_unique_fields.detect { |keys| keys & attrs.keys == keys } return where(attrs.slice(*unique_keys)).__send__(:_first_or_create, attrs, save_options) if unique_keys # We can't do an upsert :( Let see if we can fail on a validator first... instance = model.new(attrs) unless instance.valid? case save_options[:save_method] when :save! then raise NoBrainer::Error::DocumentInvalid, instance when :save? then return instance end end raise "Could not find a uniqueness validator for the following keys: `#{attrs.keys.inspect}'." end
extract_where_params()
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 133 def extract_where_params() where_clauses = finalized_criteria.options[:where_ast] unless where_clauses.is_a?(NoBrainer::Criteria::Where::MultiOperator) && where_clauses.op == :and raise "Please use a query of the form `.where(...).first_or_create(...)'" end Hash[where_clauses.clauses.map do |c| unless c.is_a?(NoBrainer::Criteria::Where::BinaryOperator) && c.op == :eq && c.key_modifier == :scalar # Ignore params on the subclass type, we are handling this case directly # in _first_or_create() next if c.key_path == [:_type] raise "Please only use equal constraints in your where() query when using first_or_create()" end raise "You may not use nested hash queries with first_or.create()" if c.key_path.size > 1 [c.key_path.first.to_sym, c.value] end.compact].tap { |h| raise "Missing where() clauses for first_or_create()" if h.empty? } end
get_model_unique_fields()
click to toggle source
# File lib/no_brainer/criteria/first_or_create.rb, line 155 def get_model_unique_fields [[model.pk_name]] + model.unique_validators .flat_map { |validator| validator.attributes.map { |attr| [attr, validator] } } .map { |f, validator| [f, *validator.scope].map(&:to_sym).sort } end