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.



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

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:



423
424
425
426
427
# File 'lib/parse/model/core/actions.rb', line 423

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:



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
# File 'lib/parse/model/core/actions.rb', line 441

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
      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)
        session ? obj.save!(session: session) : obj.save!
      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.



316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/parse/model/core/actions.rb', line 316

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:



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/parse/model/core/actions.rb', line 369

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



670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
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
# File 'lib/parse/model/core/actions.rb', line 670

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:



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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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
# File 'lib/parse/model/core/actions.rb', line 144

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,
        attributes: obj.attributes.dup,
        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|
        obj = state[:object]
        obj.instance_variable_set(:@attributes, state[:attributes])
        obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
        obj.instance_variable_set(:@id, state[:id])
        # Restore change tracking state
        obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
        obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
      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|
      obj = state[:object]
      obj.instance_variable_set(:@attributes, state[:attributes])
      obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
      obj.instance_variable_set(:@id, state[:id])
      # Restore change tracking state
      obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
      obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
    end

    raise e
  end
end