Module: Parse::Core::Querying

Included in:
Object
Defined in:
lib/parse/model/core/querying.rb

Overview

Defines the querying methods applied to a Parse::Object.

Instance Method Summary collapse

Instance Method Details

#all(constraints = { limit: :max }) { ... } ⇒ Array<Parse::Object>

Note:

This method will continually query for records by automatically incrementing the :skip parameter until no more results are returned by the server.

Fetch all matching objects in this collection matching the constraints. This will be the most common way when querying Parse objects for a subclass. When no block is passed, all objects are returned. Using a block is more memory efficient as matching objects are fetched in batches and discarded after the iteration is completed.

Examples:


songs = Song.all( ... expressions ...) # => array of Parse::Objects
# memory efficient for large amounts of records.
Song.all( ... expressions ...) do |song|
    # ... do something with song..
end

Parameters:

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

    a set of Query constraints.

Yields:

  • a block to iterate with each matching object.

Returns:

  • (Array<Parse::Object>)

    an array of matching objects. If a block is passed, an empty array is returned.



211
212
213
214
215
216
# File 'lib/parse/model/core/querying.rb', line 211

def all(constraints = { limit: :max }, &block)
  constraints = constraints.reverse_merge({ limit: :max })
  prepared_query = query(constraints)
  return prepared_query.results(&block) if block_given?
  prepared_query.results
end

#all_as(token, constraints = { limit: :max }, &block) ⇒ Array<Parse::Object>

Convenience wrapper around #all that runs the query under a caller-supplied session token. Equivalent to passing session_token: in the constraints hash, surfaced as a named kwarg so client-mode callers don't have to remember the constraint-key form. Returns nil if token is blank.

Parameters:

Returns:



227
228
229
230
231
# File 'lib/parse/model/core/querying.rb', line 227

def all_as(token, constraints = { limit: :max }, &block)
  tok = token.respond_to?(:session_token) ? token.session_token : token
  return nil if tok.nil? || tok.to_s.empty?
  all(constraints.merge(session_token: tok), &block)
end

#count(constraints = {}) ⇒ Interger

Creates a count request which is more performant when counting objects.

Examples:

# number of songs with a like count greater than 20.
count = Song.count( :like_count.gt => 20 )

Parameters:

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

    a set of Query constraints.

Returns:

  • (Interger)

    the number of records matching the query.

See Also:



339
340
341
# File 'lib/parse/model/core/querying.rb', line 339

def count(constraints = {})
  query(constraints).count
end

#count_distinct(field, constraints = {}) ⇒ Integer

Counts the number of distinct values for a specified field. Uses MongoDB aggregation pipeline to efficiently count unique values.

Examples:

# get count of unique genres for songs with play_count > 100
distinct_genres_count = Song.count_distinct(:genre, :play_count.gt => 100)
# get total number of unique users
unique_users = User.count_distinct(:objectId)

Parameters:

  • field (Symbol|String)

    The name of the field to count distinct values for.

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

    a set of Query constraints.

Returns:

  • (Integer)

    the number of distinct values

See Also:



354
355
356
# File 'lib/parse/model/core/querying.rb', line 354

def count_distinct(field, constraints = {})
  query(constraints).count_distinct(field)
end

#cursor(constraints = {}, limit: 100, order: nil) ⇒ Parse::Cursor

Create a cursor-based paginator for efficiently traversing large datasets. This is more efficient than skip/offset pagination for large result sets.

Examples:

Basic usage

cursor = Song.cursor(limit: 100, order: :created_at.desc)
cursor.each_page do |page|
  process(page)
end

With constraints

cursor = Song.cursor(artist: "Artist Name", limit: 50)
cursor.each { |song| puts song.title }

Parameters:

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

    query constraints to apply

  • limit (Integer) (defaults to: 100)

    number of items per page (default: 100)

  • order (Symbol, Parse::Order) (defaults to: nil)

    the ordering for pagination

Returns:

See Also:



409
410
411
# File 'lib/parse/model/core/querying.rb', line 409

def cursor(constraints = {}, limit: 100, order: nil)
  query(constraints).cursor(limit: limit, order: order)
end

#distinct(field, constraints = {}) ⇒ Array

Finds the distinct values for a specified field across a single collection or view and returns the results in an array.

Examples:

# get a list of unique city names for users who are older than 21.
cities = User.distinct(:city, :age.gt => 21 )

Parameters:

  • field

    The name of the field to use for unique aggregation.

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

    a set of Query constraints.

Returns:

  • (Array)

    a list of distinct values

See Also:



367
368
369
# File 'lib/parse/model/core/querying.rb', line 367

def distinct(field, constraints = {})
  query(constraints).distinct(field)
end

#each(constraints = {}) { ... } ⇒ Parse::Object

Note:

You cannot use :created_at as a constraint.

This methods allow you to efficiently iterate over all the records in the collection (lower memory cost) at a minor cost of performance. This method utilizes the created_at field of Parse records to order and iterate over all matching records, therefore you should not use this method if you want to perform a query with constraints against the created_at field or need specific type of ordering. If you need to use :created_at in your constraints, consider using #all or Actions::ClassMethods#save_all

Examples:


post = Post.first
# iterate over all comments matching conditions
Comment.each(post: post) do |comment|
   # ...
end

Parameters:

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

    a set of query constraints.

Yields:

  • a block which will iterate through each matching record.

Returns:

Raises:

  • ArgumentError if :created_at is detected in the constraints argument.

See Also:

  • all
  • Actions.save_all


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

def each(constraints = {}, &block)
  # verify we don't hvae created at as a constraint, otherwise this will not work
  invalid_constraints = constraints.keys.any? do |k|
    (k == :created_at || k == :createdAt) ||
    (k.is_a?(Parse::Operation) && (k.operand == :created_at || k.operand == :createdAt))
  end
  if invalid_constraints
    raise ArgumentError, "[#{self.class}.each] Special method each()" \
                         "cannot be used with a :created_at constraint."
  end
  batch_size = 250
  start_cursor = first(order: :created_at.asc, keys: :created_at)
  constraints.merge! cache: false, limit: batch_size, order: :created_at.asc
  _all_query = query(constraints) # used for reference in loop below
  cursor = start_cursor
  # the exclusion set is a set of ids not to include the next query.
  exclusion_set = []
  loop do
    _q = query(constraints.dup)
    _q.where(:created_at.on_or_after => cursor.created_at)
    # set of ids not to include in the next query. non-performant, but accurate.
    _q.where(:id.nin => exclusion_set) unless exclusion_set.empty?
    results = _q.results # get results

    break cursor if results.empty? # break if no results
    results.each(&block)
    next_cursor = results.last
    # break if we got less than the maximum requested
    break next_cursor if results.count < batch_size
    # break if the next object is the same as the current object.
    break next_cursor if cursor.id == next_cursor.id
    # The exclusion set is used in the case where multiple records have the exact
    # same created_at date (down to the microsecond). This prevents getting the same
    # record in the next query request.
    exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
    results = nil
    cursor = next_cursor
  end
end

#find(*parse_ids, type: :parallel, compact: true, cache: nil, session_token: nil, use_master_key: nil) ⇒ Parse::Object+ Also known as: get

Find objects for a given objectId in this collection. The result is a list (or single item) of the objects that were successfully found. By default, bypasses the cache to ensure fresh data from the server.

Examples:

Object.find "<objectId>"
Object.find "<objectId>", "<objectId>"....
Object.find ["<objectId>", "<objectId>"]
Object.find "<objectId>", cache: true  # opt-in to cache

Parameters:

  • parse_ids (String)

    the objectId to find.

  • type (Symbol) (defaults to: :parallel)

    the fetching methodology to use if more than one id was passed.

    • :parallel : Utilizes parrallel HTTP requests to fetch all objects requested.
    • :batch : This uses a batch fetch request using a contained_in clause.
  • compact (Boolean) (defaults to: true)

    whether to remove nil items from the returned array for objects that were not found.

  • cache (Boolean, Symbol) (defaults to: nil)

    caching mode. Defaults to :write_only when Parse.cache_write_on_fetch is true.

    • :write_only (default) - skip cache read, but update cache with fresh data
    • true - read from and write to cache
    • false - completely bypass cache (no read or write)

Returns:



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

def find(*parse_ids, type: :parallel, compact: true, cache: nil, session_token: nil, use_master_key: nil)
  # flatten the list of Object ids.
  parse_ids.flatten!
  parse_ids.compact!
  # determines if the result back to the call site is an array or a single result
  as_array = parse_ids.count > 1
  results = []

  # Default to write-only cache mode - find always gets fresh data
  # but updates cache for future cached reads. Controlled by feature flag.
  if cache.nil?
    cache = Parse.cache_write_on_fetch ? :write_only : false
  end

  # Extract cache option for client requests
  client_opts = { cache: cache }
  # Forward session-token / use_master_key when supplied so client-mode
  # callers can scope a `.find` to a logged-in user without dropping
  # down to the raw `client.fetch_object` form.
  client_opts[:session_token]  = session_token  unless session_token.nil?
  client_opts[:use_master_key] = use_master_key unless use_master_key.nil?
  # The parallel path spawns worker threads via `Parallel.map`. Worker
  # threads don't inherit fiber-local storage from the calling thread,
  # so `Parse.current_session_token` resolved inside the worker would
  # be nil even when the caller is inside a `Parse.with_session(...)`
  # block. Snapshot the ambient here (in the calling thread) and pass
  # it explicitly so each worker sends the right auth.
  if !client_opts.key?(:session_token)
    ambient = Parse.current_session_token
    client_opts[:session_token] = ambient if ambient.is_a?(String) && !ambient.empty?
  end

  if type == :batch
    # use a .in query with the given id as a list
    query = self.class.query(:id.in => parse_ids)
    query.cache = cache
    query.session_token = session_token unless session_token.nil?
    query.use_master_key = use_master_key unless use_master_key.nil?
    results = query.results
  else
    # use Parallel to make multiple threaded requests for finding these objects.
    # The benefit of using this as default is that each request goes to a specific URL
    # which is better than Query request (table scan). This in turn allows for caching of
    # individual objects.
    results = parse_ids.threaded_map do |parse_id|
      next nil unless parse_id.present?
      response = client.fetch_object(parse_class, parse_id, **client_opts)
      next nil if response.error?
      Parse::Object.build response.result, parse_class
    end
  end
  # removes any nil items in the array
  results.compact! if compact

  as_array ? results : results.first
end

#find_cached(*parse_ids, type: :parallel, compact: true) ⇒ Parse::Object+

Find objects with caching enabled. This is a convenience method that calls find with cache: true.

Examples:

Object.find_cached "<objectId>"
Object.find_cached "<objectId>", "<objectId>"....

Parameters:

  • parse_ids (String)

    the objectId(s) to find.

  • type (Symbol) (defaults to: :parallel)

    the fetching methodology (:parallel or :batch).

  • compact (Boolean) (defaults to: true)

    whether to remove nil items from the returned array.

Returns:

See Also:



542
543
544
# File 'lib/parse/model/core/querying.rb', line 542

def find_cached(*parse_ids, type: :parallel, compact: true)
  find(*parse_ids, type: type, compact: compact, cache: true)
end

#first(count = 1) ⇒ Parse::Object+ #first(constraints = {}) ⇒ Parse::Object

Returns the first item matching the constraint.

Overloads:

  • #first(count = 1) ⇒ Parse::Object+

    Examples:

    Object.first(2) # => an array of the first 2 objects in the collection.

    Parameters:

    • count (Interger) (defaults to: 1)

      The number of items to return.

    Returns:

  • #first(constraints = {}) ⇒ Parse::Object

    Returns the first matching object.

    Examples:

    Object.first( :name => "Anthony" )

    Parameters:

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

      a set of Query constraints.

    Returns:



245
246
247
248
249
250
251
252
253
254
255
# File 'lib/parse/model/core/querying.rb', line 245

def first(constraints = {})
  fetch_count = 1
  if constraints.is_a?(Numeric)
    fetch_count = constraints.to_i
    constraints = {}
  end
  constraints.merge!({ limit: fetch_count })
  res = query(constraints).results
  return res.first if fetch_count == 1
  return res.first fetch_count
end

#first_as(token, constraints = {}) ⇒ Parse::Object, ...

Convenience wrapper around #first that runs the query under a caller-supplied session token.

Parameters:

Returns:



263
264
265
266
267
268
269
270
271
272
# File 'lib/parse/model/core/querying.rb', line 263

def first_as(token, constraints = {})
  tok = token.respond_to?(:session_token) ? token.session_token : token
  return nil if tok.nil? || tok.to_s.empty?
  if constraints.is_a?(Numeric)
    # `first(2)` shape — surface kwarg via a synthetic constraints hash
    first({ limit: constraints.to_i, session_token: tok })
  else
    first(constraints.merge(session_token: tok))
  end
end

#last_updated(count = 1) ⇒ Parse::Object+ #last_updated(constraints = {}) ⇒ Parse::Object

Returns the most recently updated object (ordered by updated_at descending).

Overloads:

  • #last_updated(count = 1) ⇒ Parse::Object+

    Examples:

    Object.last_updated(5) # => an array of the 5 most recently updated objects.

    Parameters:

    • count (Integer) (defaults to: 1)

      The number of items to return.

    Returns:

  • #last_updated(constraints = {}) ⇒ Parse::Object

    Returns the most recently updated object matching constraints.

    Examples:

    Object.last_updated(status: "active") # => most recently updated active object
    Object.last_updated(:user.eq => user, limit: 3) # => 3 most recently updated for user

    Parameters:

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

      a set of Query constraints. Supports a :limit key to override the default limit of 1.

    Returns:

    • (Parse::Object)

      the most recently updated object matching constraints.



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

def last_updated(constraints = {})
  fetch_count = 1
  if constraints.is_a?(Numeric)
    fetch_count = constraints.to_i
    constraints = {}
  else
    # Allow limit to be specified in constraints hash
    fetch_count = constraints.delete(:limit) || 1
  end
  constraints.merge!({ limit: fetch_count, order: :updated_at.desc })
  res = query(constraints).results
  return res.first if fetch_count == 1
  return res.first fetch_count
end

#latest(count = 1) ⇒ Parse::Object+ #latest(constraints = {}) ⇒ Parse::Object

Returns the most recently created object (ordered by created_at descending).

Overloads:

  • #latest(count = 1) ⇒ Parse::Object+

    Examples:

    Object.latest(3) # => an array of the 3 most recently created objects.

    Parameters:

    • count (Integer) (defaults to: 1)

      The number of items to return.

    Returns:

  • #latest(constraints = {}) ⇒ Parse::Object

    Returns the most recently created object matching constraints.

    Examples:

    Object.latest(category: "news") # => most recent object in news category
    Object.latest(:user.eq => user, limit: 5) # => 5 most recent for user

    Parameters:

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

      a set of Query constraints. Supports a :limit key to override the default limit of 1.

    Returns:

    • (Parse::Object)

      the most recently created object matching constraints.



288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/parse/model/core/querying.rb', line 288

def latest(constraints = {})
  fetch_count = 1
  if constraints.is_a?(Numeric)
    fetch_count = constraints.to_i
    constraints = {}
  else
    # Allow limit to be specified in constraints hash
    fetch_count = constraints.delete(:limit) || 1
  end
  constraints.merge!({ limit: fetch_count, order: :created_at.desc })
  res = query(constraints).results
  return res.first if fetch_count == 1
  return res.first fetch_count
end

#literal_where(conditions = {}) ⇒ self

Parameters:

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

    a set of constraints for this query.

Returns:

  • (self)

See Also:



126
127
128
# File 'lib/parse/model/core/querying.rb', line 126

def literal_where(conditions = {})
  query.where(conditions)
end

#newest(constraints = {}) ⇒ Array<Parse::Object>

Find objects matching the constraint ordered by the descending created_at date.

Parameters:

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

    a set of Query constraints.

Returns:



374
375
376
377
378
379
# File 'lib/parse/model/core/querying.rb', line 374

def newest(constraints = {})
  constraints.merge!(order: :created_at.desc)
  _q = query(constraints)
  _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
  _q
end

#oldest(constraints = {}) ⇒ Array<Parse::Object>

Find objects matching the constraint ordered by the ascending created_at date.

Parameters:

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

    a set of Query constraints.

Returns:



384
385
386
387
388
389
# File 'lib/parse/model/core/querying.rb', line 384

def oldest(constraints = {})
  constraints.merge!(order: :created_at.asc)
  _q = query(constraints)
  _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
  _q
end

#query(constraints = {}) ⇒ Parse::Query Also known as: where

Creates a new Query with the given constraints for this class.

Examples:

# assume Post < Parse::Object
query = Post.query(:updated_at.before => DateTime.now)

Returns:

  • (Parse::Query)

    a new query with the given constraints for this Parse::Object subclass.



117
118
119
# File 'lib/parse/model/core/querying.rb', line 117

def query(constraints = {})
  Parse::Query.new self.parse_class, constraints
end

#scope(name, body) ⇒ Symbol

This feature is a small subset of the ActiveRecord named scopes feature. Scoping allows you to specify commonly-used queries which can be referenced as class method calls and are chainable with other scopes. You can use every Query method previously covered such as where, includes and limit.

class Article < Parse::Object property :published, :boolean scope :published, -> { query(published: true) } end

This is the same as defining your own class method for the query.

class Article < Parse::Object def self.published query(published: true) end end

You can also chain scopes and pass parameters. In addition, boolean and enumerated properties have automatically generated scopes for you to use.

class Article < Parse::Object scope :published, -> { query(published: true) }

property :comment_count, :integer
property :category
property :approved, :boolean

scope :published_and_commented, -> { published.where :comment_count.gt => 0 }
scope :popular_topics, ->(name) { published_and_commented.where category: name }

end

simple scope

Article.published # => where published is true

chained scope

Article.published_and_commented # published is true and comment_count > 0

scope with parameters

Article.popular_topic("music") # => popular music articles

equivalent: where(published: true, :comment_count.gt => 0, category: name)

automatically generated scope

Article.approved(category: "tour") # => where approved: true, category: 'tour'

If you would like to turn off automatic scope generation for property types, set the option :scope to false when declaring the property.

Parameters:

  • name (Symbol)

    the name of the scope.

  • body (Proc)

    the proc related to the scope.

Returns:

  • (Symbol)

    the name of the singleton method created.

Raises:

  • ArgumentError if body parameter does not respond to call



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/parse/model/core/querying.rb', line 64

def scope(name, body)
  unless body.respond_to?(:call)
    raise ArgumentError, "The scope body needs to be callable."
  end

  name = name.to_sym
  if respond_to?(name, true)
    puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
  end

  define_singleton_method(name) do |*args, &block|
    if body.arity.zero?
      res = body.call
      res.conditions(*args) if args.present?
    else
      res = body.call(*args)
    end

    _q = res || query

    if _q.is_a?(Parse::Query)
      klass = self
      _q.define_singleton_method(:method_missing) do |m, *args, &chained_block|
        if klass.respond_to?(m, true)
          # must be a scope
          klass_scope = klass.send(m, *args)
          if klass_scope.is_a?(Parse::Query)
            # merge constraints
            add_constraints(klass_scope.constraints)
            # if a block was passed, execute the query, otherwise return the query
            return chained_block.present? ? results(&chained_block) : self
          end # if
          klass = nil # help clean up ruby gc
          return klass_scope
        end
        klass = nil # help clean up ruby gc
        return results.send(m, *args, &chained_block)
      end
    end

    Parse::Query.apply_auto_introspection!(_q)

    return _q if block.nil?
    _q.results(&block)
  end
end

#subscribe(where: {}, fields: nil, session_token: nil, client: nil) ⇒ Parse::LiveQuery::Subscription

Subscribe to real-time updates for objects in this collection. Uses Parse LiveQuery WebSocket connection to receive push notifications when objects are created, updated, deleted, or enter/leave the query results.

Examples:

Basic subscription (all objects)

subscription = Song.subscribe
subscription.on(:create) { |song| puts "New song: #{song.title}" }
subscription.on(:update) { |song, original| puts "Updated!" }
subscription.on(:delete) { |song| puts "Deleted!" }

Subscribe with query constraints

subscription = Song.subscribe(where: { artist: "Beatles" })
subscription.on_create { |song| puts "New Beatles song!" }

With field filtering

subscription = User.subscribe(where: { status: "online" }, fields: ["name", "avatar"])
subscription.on_update { |user| puts "User changed: #{user.name}" }

With session token for ACL-aware subscriptions

subscription = PrivateData.subscribe(session_token: current_user.session_token)

Parameters:

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

    query constraints for the subscription

  • fields (Array<String>) (defaults to: nil)

    specific fields to watch for changes (nil = all fields)

  • session_token (String) (defaults to: nil)

    session token for ACL-aware subscriptions

  • client (Parse::LiveQuery::Client) (defaults to: nil)

    custom LiveQuery client (optional)

Returns:

See Also:



441
442
443
444
445
446
447
448
449
450
# File 'lib/parse/model/core/querying.rb', line 441

def subscribe(where: {}, fields: nil, session_token: nil, client: nil)
  # Fall through to the ambient set by `Parse.with_session` / `Parse.login`
  # so a caller wrapping a region with `with_session(user) { Klass.subscribe ... }`
  # gets an ACL-aware subscription without re-threading the token.
  if session_token.nil?
    ambient = Parse.current_session_token
    session_token = ambient if ambient.is_a?(String) && !ambient.empty?
  end
  query(where).subscribe(fields: fields, session_token: session_token, client: client)
end