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.



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

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:



683
684
685
686
687
# File 'lib/parse/model/core/actions.rb', line 683

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:



701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
# File 'lib/parse/model/core/actions.rb', line 701

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.



569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/parse/model/core/actions.rb', line 569

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:



622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
# File 'lib/parse/model/core/actions.rb', line 622

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.



977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
# File 'lib/parse/model/core/actions.rb', line 977

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:



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
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
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/parse/model/core/actions.rb', line 423

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

  previous_context = Fiber[TRANSACTION_CONTEXT_KEY]
  transaction_context = { snapshots: {}, created_objects: {} }
  Fiber[TRANSACTION_CONTEXT_KEY] = transaction_context
  original_states = {}
  tracked_objects = []

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

    # Wrap the batch to associate the pre-mutation snapshot with each
    # object that actually participates in the transaction.
    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|
      # Ruby identity is required because all unsaved Parse objects
      # compare equal while their ids are nil.
      if obj.respond_to?(:attributes) && obj.respond_to?(:id) && !original_states.key?(obj.object_id)
        original_states[obj.object_id] = Parse::Core::Actions.capture_transaction_state(
          obj,
          transaction_context,
          include_created: true,
        )
        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, &method_block|
      batch.send(method, *args, &method_block)
    end
    batch_wrapper.define_singleton_method(:respond_to_missing?) do |method, include_private = false|
      batch.respond_to?(method, include_private)
    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

      if responses.all?(&:success?)
        # Match responses to objects using the request tag (Ruby object_id).
        objects_by_id = tracked_objects.each_with_object({}) { |o, h| h[o.object_id] = o }
        batch.requests.zip(responses).each do |request, response|
          next unless request && response && response.success?
          result = response.result
          next unless result.is_a?(Hash)

          obj = objects_by_id[request.tag]
          next unless obj

          obj.instance_variable_set(:@id, result["objectId"]) if result["objectId"]
          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!)
          obj.send(:clear_changes!) if obj.respond_to?(:clear_changes!, true)
        end

        return responses
      end

      error_response = responses.find { |response| !response.success? }
      raise Parse::Error, "Transaction failed: #{error_response.error}"
    rescue Parse::Error => e
      if e.message.include?("251") && attempts < retries
        sleep(0.1 * attempts)
        retry
      end
      raise
    end
  rescue StandardError
    original_states.each_value do |state|
      Parse::Core::Actions.rollback_object_state(state)
    end
    raise
  ensure
    Fiber[TRANSACTION_CONTEXT_KEY] = previous_context
  end
end