Module: Parse::Core::Actions::ClassMethods

Defined in:
lib/parse/model/core/actions.rb

Overview

Class methods applied to Parse::Object subclasses.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#raise_on_save_failureBoolean

By default, we return true or false for save and destroy operations. If you prefer to have Parse::Object raise an exception instead, you can tell to do so either globally or on a per-model basis. When a save fails, it will raise a RecordNotSaved.

When enabled, if an error is returned by Parse due to saving or destroying a record, due to your before_save or before_delete validation cloud code triggers, Parse::Object will return the a RecordNotSaved exception type. This exception has an instance method of #object which contains the object that failed to save.

Examples:

# globally across all models
Parse::Model.raise_on_save_failure = true
Song.raise_on_save_failure = true # per-model

# or per-instance raise on failure
song.save!

Returns:

  • (Boolean)

    whether to raise a RecordNotSaved when an object fails to save.



382
# File 'lib/parse/model/core/actions.rb', line 382

attr_writer :raise_on_save_failure

Instance Method Details

#create!(attrs = {}) ⇒ Parse::Object

Creates a new object with the given attributes and saves it. This is equivalent to calling new(attrs).save!.

Examples:

song = Song.create!(title: "New Song", artist: "Artist")

Parameters:

  • attrs (Hash) (defaults to: {})

    the attributes for the new object.

Returns:

Raises:



512
513
514
515
516
# File 'lib/parse/model/core/actions.rb', line 512

def create!(attrs = {})
  obj = new(attrs)
  obj.save!
  obj
end

#create_or_update!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil) ⇒ Parse::Object

Finds the first object matching the query conditions and updates it with the attributes, or creates a new saved object with the attributes. Saves new objects or existing objects with changes. See #first_or_create! for the synchronize-create lock semantics — they apply identically here.

Examples:

Parse::User.create_or_update!({ ..query conditions..}, {.. resource_attrs ..})

Parameters:

  • query_attrs (Hash) (defaults to: {})

    a set of query constraints that also are applied.

  • resource_attrs (Hash) (defaults to: {})

    a set of attribute values to be applied to found objects or used for creation.

  • synchronize (Boolean, Hash, nil) (defaults to: nil)

    override the synchronize-create lock. nil (default) defers to the per-class synchronize_create_default or the module-level Parse.synchronize_create_default. true enables with defaults; false opts out; a Hash enables with custom options merged over Parse.synchronize_create_options.

  • session (String, Parse::User, nil) (defaults to: nil)

    session token (or object answering :session_token) threaded through both the query and the save so the entire find→create flow runs under one auth identity.

  • master_key (Boolean, nil) (defaults to: nil)

    when explicitly false, disables master key for both halves.

Returns:

  • (Parse::Object)

    a Parse::Object, whether found by the query or newly created.

Raises:



530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/parse/model/core/actions.rb', line 530

def create_or_update!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil)
  query_attrs = query_attrs.symbolize_keys
  resource_attrs = resource_attrs.symbolize_keys

  enabled, sync_opts = _resolve_synchronize_flag(synchronize)
  return _create_or_update_unsynchronized!(query_attrs, resource_attrs, session: session, master_key: master_key) unless enabled

  _assert_synchronize_class_allowed!
  options = _merged_synchronize_options(sync_opts)
  session_token = _extract_session_token(session)

  # See #first_or_create! for the partition rationale — strip
  # Parse::Query option keys before lock canonicalization.
  lock_attrs = query_attrs.reject { |k, _| Parse::Query.option_key?(k) }
  _assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)

  Parse::CreateLock.synchronize(
    parse_class: parse_class,
    query_attrs: lock_attrs,
    options: options,
    session_token: session_token,
    master_key: master_key,
  ) do
    obj = _scoped_first(query_attrs, session: session, master_key: master_key)

    if obj.nil?
      obj = self.new query_attrs.merge(resource_attrs)
      begin
        session ? obj.save!(session: session) : obj.save!
      rescue Parse::RecordNotSaved => e
        winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
        raise unless winner
        obj = winner
      rescue Parse::Error::DuplicateRequestError
        # See #first_or_create! — recover the row a retried create
        # already landed (it already carries resource_attrs).
        winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
        raise unless winner
        obj = winner
      end
    end

    if !obj.new? && !resource_attrs.empty?
      has_changes = resource_attrs.any? do |key, value|
        obj.respond_to?(key) && obj.send(key) != value
      end
      if has_changes
        obj.apply_attributes!(resource_attrs, dirty_track: true)
        begin
          session ? obj.save!(session: session) : obj.save!
        rescue Parse::Error::DuplicateRequestError
          # A retried update (PUT) landed but lost its response; re-find
          # the now-updated row and return it.
          winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
          raise unless winner
          obj = winner
        end
      end
    end

    obj
  end
end

#first_or_create(query_attrs = {}, resource_attrs = {}) ⇒ Parse::Object

Finds the first object matching the query conditions, or creates a new unsaved object with the attributes. This method takes the possibility of two hashes, therefore make sure you properly wrap the contents of the input with {}.

Examples:

Parse::User.first_or_create({ ..query conditions..})
Parse::User.first_or_create({ ..query conditions..}, {.. resource_attrs ..})

Parameters:

  • query_attrs (Hash) (defaults to: {})

    a set of query constraints that also are applied.

  • resource_attrs (Hash) (defaults to: {})

    a set of additional attribute values to be applied only if an object was not found.

Returns:

  • (Parse::Object)

    a Parse::Object, whether found by the query or newly created.



398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/parse/model/core/actions.rb', line 398

def first_or_create(query_attrs = {}, resource_attrs = {})
  query_attrs = query_attrs.symbolize_keys
  resource_attrs = resource_attrs.symbolize_keys
  obj = query(query_attrs).first

  if obj.blank?
    # Object not found, create new one with query_attrs + resource_attrs
    merged_attrs = query_attrs.merge(resource_attrs)
    obj = self.new merged_attrs
  end
  # If object exists, return it as-is without any modifications

  obj
end

#first_or_create!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil) ⇒ Parse::Object

Finds the first object matching the query conditions, or creates a new saved object with the attributes. This method is similar to #first_or_create but will also Parse::Core::Actions#save! the object if it was newly created.

When synchronize: is enabled (per-call, per-class via synchronize_create_default, or globally via Parse.synchronize_create_default), the find→create→save sequence is serialized through Parse::CreateLock so concurrent callers with identical query_attrs cannot both create. The lock requires a Moneta cache store (Redis recommended); on a process-local store the lock degrades to a per-key Mutex. A MongoDB unique index on the constrained fields is the correctness floor — on Parse code 137 (DuplicateValue) the wrapper re-queries inside the held lock and returns the winner.

Examples:

obj = Parse::User.first_or_create!({ ..query conditions..})
obj = Parse::User.first_or_create!({ ..query conditions..}, {.. resource_attrs ..})

Per-call lock opt-in

User.first_or_create!({ email: e }, { name: n }, synchronize: true)

Per-call with tuning

User.first_or_create!({ email: e }, {}, synchronize: { ttl: 5, wait: 1.0 })

Auth-context threading

User.first_or_create!({ email: e }, {}, session: current_user.session_token)

Parameters:

  • query_attrs (Hash) (defaults to: {})

    a set of query constraints that also are applied.

  • resource_attrs (Hash) (defaults to: {})

    a set of attribute values to be applied if an object was not found.

  • synchronize (Boolean, Hash, nil) (defaults to: nil)

    override the synchronize-create lock. nil (default) defers to the per-class synchronize_create_default or the module-level Parse.synchronize_create_default. true enables with defaults; false opts out; a Hash enables with custom options merged over Parse.synchronize_create_options.

  • session (String, Parse::User, nil) (defaults to: nil)

    session token (or object answering :session_token) threaded through both the query and the save so the entire find→create flow runs under one auth identity.

  • master_key (Boolean, nil) (defaults to: nil)

    when explicitly false, disables master key for both halves.

Returns:

  • (Parse::Object)

    a Parse::Object, whether found by the query or newly created.

Raises:

See Also:



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/parse/model/core/actions.rb', line 451

def first_or_create!(query_attrs = {}, resource_attrs = {}, synchronize: nil, session: nil, master_key: nil)
  query_attrs = query_attrs.symbolize_keys
  resource_attrs = resource_attrs.symbolize_keys

  enabled, sync_opts = _resolve_synchronize_flag(synchronize)
  return _first_or_create_unsynchronized!(query_attrs, resource_attrs, session: session, master_key: master_key) unless enabled

  _assert_synchronize_class_allowed!
  options = _merged_synchronize_options(sync_opts)
  session_token = _extract_session_token(session)

  # Split query_attrs into the constraint subset (what
  # determines lock identity) and the query-shape options
  # (`:cache`, `:limit`, `:order`, ACL helpers, …) that
  # `Parse::Query#conditions` absorbs as query parameters.
  # Without this, a caller passing the documented `cache:
  # 30.seconds` escape hatch alongside their constraints
  # tripped `Parse::CreateLock.canonicalize_value` on the
  # `ActiveSupport::Duration` — see 4.4.2 changelog. The
  # original `query_attrs` is still forwarded to
  # `_scoped_first` below; `conditions()` extracts the option
  # keys on the find side, so the cache TTL still applies.
  lock_attrs = query_attrs.reject { |k, _| Parse::Query.option_key?(k) }
  _assert_lock_attrs_have_constraints!(query_attrs, lock_attrs)

  Parse::CreateLock.synchronize(
    parse_class: parse_class,
    query_attrs: lock_attrs,
    options: options,
    session_token: session_token,
    master_key: master_key,
  ) do
    obj = _scoped_first(query_attrs, session: session, master_key: master_key)
    next obj if obj

    obj = self.new query_attrs.merge(resource_attrs)
    begin
      session ? obj.save!(session: session) : obj.save!
      obj
    rescue Parse::RecordNotSaved => e
      winner = _recover_from_duplicate_value(e, query_attrs, session: session, master_key: master_key)
      raise unless winner
      winner
    rescue Parse::Error::DuplicateRequestError
      # A transparently-retried create landed but lost its response;
      # server idempotency rejected the replay. Re-find the row the
      # original attempt created and return it.
      winner = _recover_from_duplicate_request(query_attrs, session: session, master_key: master_key)
      raise unless winner
      winner
    end
  end
end

#save_all(constraints = {}) { ... } ⇒ Boolean

Note:

You cannot use :updated_at as a constraint.

Auto save all objects matching the query constraints. This method is meant to be used with a block. Any objects that are modified in the block will be batched for a save operation. This uses the updated_at field to continue to query for all matching objects that have not been updated. If you need to use :updated_at in your constraints, consider using Querying#all or Querying#each

Examples:


post = Post.first
Comments.save_all( post: post) do |comment|
  # .. modify comment ...
  # it will automatically be saved
end

Parameters:

  • constraints (Hash) (defaults to: {})

    a set of query constraints.

Yields:

  • a block which will iterate through each matching object.

Returns:

  • (Boolean)

    true if all saves succeeded and there were no errors.



806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'lib/parse/model/core/actions.rb', line 806

def save_all(constraints = {}, &block)
  invalid_constraints = constraints.keys.any? do |k|
    (k == :updated_at || k == :updatedAt) ||
    (k.is_a?(Parse::Operation) && (k.operand == :updated_at || k.operand == :updatedAt))
  end
  if invalid_constraints
    raise ArgumentError,
      "[#{self}] Special method save_all() cannot be used with an :updated_at constraint."
  end

  force = false
  batch_size = 250
  iterator_block = nil
  if block_given?
    iterator_block = block
    force ||= false
  else
    # if no block given, assume you want to just save all objects
    # regardless of modification.
    force = true
  end
  # Only generate the comparison block once.
  # updated_comparison_block = Proc.new { |x| x.updated_at }

  anchor_date = Parse::Date.now
  constraints.merge! :updated_at.on_or_before => anchor_date
  constraints.merge! cache: false
  # oldest first, so we create a reduction-cycle
  constraints.merge! order: :updated_at.asc, limit: batch_size
  update_query = query(constraints)
  #puts "Setting Anchor Date: #{anchor_date}"
  cursor = nil
  has_errors = false
  loop do
    results = update_query.results

    break if results.empty?

    # verify we didn't get duplicates fetches
    if cursor.is_a?(Parse::Object) && results.any? { |x| x.id == cursor.id }
      warn "[#{self}.save_all] Unbounded update detected with id #{cursor.id}."
      has_errors = true
      break cursor
    end

    results.each(&iterator_block) if iterator_block.present?
    # we don't need to refresh the objects in the array with the results
    # since we will be throwing them away. Force determines whether
    # to save these objects regardless of whether they are dirty.
    batch = results.save(merge: false, force: force)

    # faster version assuming sorting order wasn't messed up
    cursor = results.last
    # slower version, but more accurate
    # cursor_item = results.max_by(&updated_comparison_block).updated_at
    # puts "[Parse::SaveAll] Updated #{results.count} records updated <= #{cursor.updated_at}"

    break if results.count < batch_size # we didn't hit a cap on results.
    if cursor.is_a?(Parse::Object)
      update_query.where :updated_at.gte => cursor.updated_at

      if cursor.updated_at.present? && cursor.updated_at > anchor_date
        warn "[#{self}.save_all] Reached anchor date  #{anchor_date} < #{cursor.updated_at}"
        break cursor
      end
    end

    has_errors ||= batch.error?
  end
  not has_errors
end

#transaction(retries: 5) {|Parse::BatchOperation| ... } ⇒ Array<Parse::Response>

Execute a set of operations as an atomic transaction. All operations will be executed in sequence, and if any fail, the entire transaction will be rolled back.

Examples:

Basic transaction

Parse::Object.transaction do |batch|
  user = User.first
  user.username = "new_username"
  batch.add(user)

  post = Post.new(author: user, title: "New Post")
  batch.add(post)
end

Using the block return for automatic batching

results = Parse::Object.transaction do
  user1 = User.first
  user1.score = 100

  user2 = User.first(username: "player2")
  user2.score = 200

  [user1, user2]  # Return array of objects to save
end

Parameters:

  • retries (Integer) (defaults to: 5)

    number of times to retry on transaction conflict (error 251)

Yields:

Returns:

Raises:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/parse/model/core/actions.rb', line 238

def transaction(retries: 5, &block)
  raise ArgumentError, "Block required for transaction" unless block_given?

  batch = Parse::BatchOperation.new(nil, transaction: true)

  # Store original state of objects for rollback
  original_states = {}
  tracked_objects = []

  # Wrap the batch to capture objects being added
  batch_wrapper = Object.new
  batch_wrapper.define_singleton_method(:is_a?) do |klass|
    klass == Parse::BatchOperation || super(klass)
  end
  batch_wrapper.define_singleton_method(:kind_of?) do |klass|
    klass == Parse::BatchOperation || super(klass)
  end
  batch_wrapper.define_singleton_method(:instance_of?) do |klass|
    klass == Parse::BatchOperation
  end
  batch_wrapper.define_singleton_method(:add) do |obj|
    # Store original state when object is first added to transaction.
    # Use obj.object_id (Ruby identity) as the key because Parse::Object#hash
    # and #eql? treat all unsaved objects (nil id) as equal, which would cause
    # only the first unsaved object to be tracked.
    if obj.respond_to?(:attributes) && obj.respond_to?(:id) && !original_states.key?(obj.object_id)
      original_states[obj.object_id] = {
        object: obj,
        property_values: Parse::Core::Actions.snapshot_property_values(obj),
        changed_attributes: obj.instance_variable_get(:@changed_attributes)&.dup || {},
        id: obj.id,
        mutations_from_database: obj.instance_variable_get(:@mutations_from_database),
        mutations_before_last_save: obj.instance_variable_get(:@mutations_before_last_save),
      }
      tracked_objects << obj
    end
    batch.add(obj)
  end

  # Forward other methods to the real batch
  batch_wrapper.define_singleton_method(:method_missing) do |method, *args, &block|
    batch.send(method, *args, &block)
  end

  result = yield(batch_wrapper)

  # If block returns objects, add them to batch
  if result.respond_to?(:change_requests)
    batch_wrapper.add(result)
  elsif result.is_a?(Array)
    result.each { |obj| batch_wrapper.add(obj) if obj.respond_to?(:change_requests) }
  end

  # Submit with retry logic for transaction conflicts
  attempts = 0
  begin
    attempts += 1
    responses = batch.submit

    # Check for success
    if responses.all?(&:success?)
      # Update tracked objects with data from successful responses
      # Match responses to objects using the request tag (Ruby object_id)
      # Build hash lookup once for O(n) instead of O(n²) linear search
      objects_by_id = tracked_objects.each_with_object({}) { |o, h| h[o.object_id] = o }
      requests = batch.requests
      requests.zip(responses).each do |request, response|
        next unless request && response && response.success?
        result = response.result
        next unless result.is_a?(Hash)

        # Find the object matching this request's tag
        obj = objects_by_id[request.tag]
        next unless obj

        # Update object with response data (objectId, createdAt, updatedAt)
        if result["objectId"]
          obj.instance_variable_set(:@id, result["objectId"])
        end
        if result["createdAt"]
          obj.instance_variable_set(:@created_at, Parse::Date.parse(result["createdAt"]))
        end
        if result["updatedAt"]
          obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["updatedAt"]))
        elsif result["createdAt"]
          obj.instance_variable_set(:@updated_at, Parse::Date.parse(result["createdAt"]))
        end

        # Apply any additional attributes returned by beforeSave hooks
        obj.set_attributes!(result) if obj.respond_to?(:set_attributes!)

        # Clear change tracking since save was successful
        obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
      end

      return responses
    else
      # Find first error
      error_response = responses.find { |r| !r.success? }

      # Rollback local object states
      original_states.each_value do |state|
        Parse::Core::Actions.rollback_object_state(state)
      end

      raise Parse::Error, "Transaction failed: #{error_response.error}"
    end
  rescue Parse::Error => e
    # Retry on transaction conflict (error code 251)
    if e.message.include?("251") && attempts < retries
      sleep(0.1 * attempts) # Exponential backoff
      retry
    end

    # Rollback local object states on final failure
    original_states.each_value do |state|
      Parse::Core::Actions.rollback_object_state(state)
    end

    raise e
  end
end