Module: Parse::Core::Fetching
- Included in:
- Object
- Defined in:
- lib/parse/model/core/fetching.rb
Overview
Defines the record fetching interface for instances of Parse::Object.
Constant Summary collapse
- NON_SERIALIZABLE_IVARS =
Non-serializable instance variables that should be excluded from Marshal.
-
@fetch_mutex: Mutex objects cannot be marshalled
-
@client: HTTP client objects contain non-serializable connections
-
[:@fetch_mutex, :@client].freeze
Instance Method Summary collapse
-
#autofetch!(key, source_info: nil) ⇒ Boolean
Autofetches the object based on a key that is not part Properties::BASE_KEYS.
-
#fetch(return_object = nil, keys: nil, includes: nil, preserve_changes: false) ⇒ Object
Fetches the object from the Parse data store.
-
#fetch!(keys: nil, includes: nil, preserve_changes: false, **opts) ⇒ self
Force fetches and updates the current object with the data contained in the Parse collection.
-
#fetch_cache!(keys: nil, includes: nil, preserve_changes: false, **opts) ⇒ self
Fetches the object with explicit caching enabled.
-
#fetch_json(keys: nil, includes: nil) ⇒ Hash?
Returns raw JSON data from the server without updating the current object.
-
#fetch_object ⇒ Parse::Object
Fetches the Parse object from the data store and returns a Parse::Object instance.
-
#prepare_for_dirty_tracking!(key) ⇒ void
Prepares object for dirty tracking by fetching if needed.
Instance Method Details
#autofetch!(key, source_info: nil) ⇒ Boolean
Autofetches the object based on a key that is not part Properties::BASE_KEYS. If the key is not a Parse standard key, and the current object is in a Pointer state or was selectively fetched, then fetch the data related to this record from the Parse data store. Uses a mutex for thread safety to prevent race conditions in multi-threaded contexts.
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 |
# File 'lib/parse/model/core/fetching.rb', line 427 def autofetch!(key, source_info: nil) key = key.to_sym # Autofetch if object is a pointer OR was selectively fetched # Skip if autofetch is disabled for this instance needs_fetch = pointer? || has_selective_keys? return unless needs_fetch && !autofetch_disabled? && key != :acl && !Parse::Properties::BASE_KEYS.include?(key) && respond_to?(:fetch) # Capture caller stack BEFORE mutex for better error tracebacks # Filter out internal parse-stack frames to show where user code accessed the field caller_stack = caller.reject { |frame| frame.include?("/lib/parse/") } # Use mutex for thread-safe check-and-fetch pattern fetch_mutex.synchronize do # Double-check inside mutex (another thread may have fetched) return if !pointer? && !has_selective_keys? is_pointer_fetch = pointer? # Track for N+1 detection if enabled if is_pointer_fetch && Parse.warn_on_n_plus_one # Check for source info in the registry (set by belongs_to getter) n_plus_one_source = Parse::NPlusOneDetector.lookup_source(self) source_class = source_info&.dig(:source_class) || n_plus_one_source&.dig(:source_class) || self.class.name association = source_info&.dig(:association) || n_plus_one_source&.dig(:association) || key Parse::NPlusOneDetector.track_autofetch( source_class: source_class, association: association, target_class: self.class.name, object_id: id, ) end # If autofetch_raise_on_missing_keys is enabled, raise an error instead of fetching # This helps developers identify where they need to add keys to their queries if Parse.autofetch_raise_on_missing_keys error = Parse::AutofetchTriggeredError.new(self.class, id, key, is_pointer: is_pointer_fetch) error.set_backtrace(caller_stack) raise error end # Log info about autofetch being triggered (conditional on warn_on_query_issues) if Parse.warn_on_query_issues if is_pointer_fetch puts "[Parse::Autofetch] Fetching #{self.class}##{id} - pointer accessed field :#{key} (silence with Parse.warn_on_query_issues = false)" else puts "[Parse::Autofetch] Fetching #{self.class}##{id} - field :#{key} was not included in partial fetch (silence with Parse.warn_on_query_issues = false)" end end # Autofetch always preserves changes - it's an implicit background operation # that shouldn't discard user modifications send :fetch, keys: nil, includes: nil, preserve_changes: true end end |
#fetch ⇒ self #fetch(return_object) ⇒ self, Hash #fetch(keys:, includes:, preserve_changes:) ⇒ self
Fetches the object from the Parse data store. Unlike fetchIfNeeded, this always fetches from the server and updates the local object with fresh data.
302 303 304 305 306 307 308 309 310 |
# File 'lib/parse/model/core/fetching.rb', line 302 def fetch(return_object = nil, keys: nil, includes: nil, preserve_changes: false) # Handle legacy signature: fetch(true) or fetch(false) if return_object == false return fetch_json(keys: keys, includes: includes) end # For fetch(), fetch(true), or fetch(keys: ..., includes: ..., preserve_changes: ...) fetch!(keys: keys, includes: includes, preserve_changes: preserve_changes) self end |
#fetch!(keys: nil, includes: nil, preserve_changes: false, **opts) ⇒ self
Force fetches and updates the current object with the data contained in the Parse collection. The changes applied to the object are not dirty tracked. By default, bypasses cache reads but updates the cache with fresh data (write-only mode). This ensures you always get fresh data while keeping the cache updated for future reads.
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 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 |
# File 'lib/parse/model/core/fetching.rb', line 78 def fetch!(keys: nil, includes: nil, preserve_changes: false, **opts) # Default to write-only cache mode - fetch fresh data but update cache # This can be disabled globally with Parse.cache_write_on_fetch = false unless opts.key?(:cache) opts[:cache] = Parse.cache_write_on_fetch ? :write_only : false end # Normalize keys and includes arrays once at the start for performance keys_array = keys.present? ? Array(keys) : nil includes_array = includes.present? ? Array(includes) : nil # Build formatted keys once (reused for query and tracking) formatted_keys = keys_array&.map { |k| Parse::Query.format_field(k) } # Validate keys against model fields if validation is enabled # Skip validation if warnings are disabled (nothing to report) if keys_array && Parse.validate_query_keys && Parse.warn_on_query_issues validate_fetch_keys(keys_array) end # Build query parameters for partial fetch query = {} query[:keys] = formatted_keys.join(",") if formatted_keys query[:include] = includes_array.map(&:to_s).join(",") if includes_array response = client.fetch_object(parse_class, id, query: query.presence, **opts) if response.error? puts "[Fetch Error] #{response.code}: #{response.error}" # Raise appropriate error based on response code case response.code when 101 # Object not found raise Parse::Error::ProtocolError, "Object not found" else raise Parse::Error::ProtocolError, response.error end end # Handle empty results gracefully - clear the object rather than error result = response.result if result.nil? || (result.is_a?(Array) && result.empty?) # Mark object as deleted and clear the ID @_deleted = true @id = nil clear_changes! return self end # Handle case where result is an Array (e.g., batch operations or certain API responses) # This is unexpected for single-object fetch but handled defensively if result.is_a?(Array) warn "[Parse::Fetch] Unexpected array response for fetch_object (id: #{id}). This may indicate an API issue." result = result.find { |r| r.is_a?(Hash) && (r["objectId"] == id || r["id"] == id) } if result.nil? warn "[Parse::Fetch] Object #{id} not found in array response - marking as deleted" @_deleted = true @id = nil clear_changes! return self end end # If we successfully fetched data, ensure the object is not marked as deleted @_deleted = false # Capture dirty fields and their local values BEFORE applying server data dirty_fields = {} if respond_to?(:changed) begin changed_attrs = changed if changed_attrs.respond_to?(:each) changed_attrs.each do |attr| # Only capture if object responds to the attribute getter if respond_to?(attr) begin dirty_fields[attr.to_sym] = send(attr) rescue NoMethodError => e # Skip this attribute if its getter raises NoMethodError warn "[Parse::Fetch] Skipping dirty field :#{attr}: #{e.}" end end end end rescue NoMethodError => e # Handle ActiveModel 8.x compatibility issues where `changed` method itself fails # due to unexpected state (e.g., after transaction rollback) warn "[Parse::Fetch] Warning: changed tracking unavailable: #{e.}" end end # Determine if this is a partial fetch is_partial_fetch = keys_array.present? if is_partial_fetch # Build the new fetched keys list (top-level keys only, without nested paths) # Reuse formatted_keys instead of calling format_field again new_keys = formatted_keys.map { |k| k.split(".").first.to_sym } new_keys << :id unless new_keys.include?(:id) new_keys << :objectId unless new_keys.include?(:objectId) new_keys.uniq! # If already selectively fetched, merge with existing keys if has_selective_keys? @_fetched_keys = (@_fetched_keys + new_keys).uniq else @_fetched_keys = new_keys end # Parse keys with dot notation into nested fetched keys and merge new_nested_keys = Parse::Query.parse_keys_to_nested_keys(keys_array) if new_nested_keys.present? if @_nested_fetched_keys.present? # Merge nested keys new_nested_keys.each do |field, nested| @_nested_fetched_keys[field] ||= [] @_nested_fetched_keys[field] = (@_nested_fetched_keys[field] + nested).uniq end else @_nested_fetched_keys = new_nested_keys end end else # Full fetch - clear partial fetch tracking @_fetched_keys = nil @_nested_fetched_keys = nil end # Apply attributes from server (only keys in result get updated) apply_attributes!(result, dirty_track: false) begin clear_changes! rescue => e # Log the error for debugging purposes warn "[Parse::Fetch] Warning: clear_changes! failed: #{e.class}: #{e.}" # If clear_changes! fails, manually reset change tracking @changed_attributes = {} if instance_variable_defined?(:@changed_attributes) @mutations_from_database = nil if instance_variable_defined?(:@mutations_from_database) @mutations_before_last_save = nil if instance_variable_defined?(:@mutations_before_last_save) end # Handle previously dirty fields based on preserve_changes setting dirty_fields.each do |attr, local_value| attr_sym = attr.to_sym # Skip base fields (id, objectId, created_at, updated_at) - they should always accept server values next if Parse::Properties::BASE_KEYS.include?(attr_sym) # Determine the remote field name for this attribute remote_field = self.field_map[attr_sym]&.to_s || attr.to_s # Check if this field was in the server response (i.e., was fetched) field_in_response = result.key?(remote_field) || result.key?(attr.to_s) if field_in_response # Field was fetched from server current_server_value = send(attr) if preserve_changes # Re-apply local value - ActiveModel will mark dirty if value differs setter = "#{attr}=" send(setter, local_value) if respond_to?(setter) else # Default behavior: accept server value, warn if local value was different if current_server_value != local_value puts "[Parse::Fetch] Field :#{attr} had unsaved changes that were discarded (local: #{local_value.inspect}, server: #{current_server_value.inspect}). Use preserve_changes: true to keep local changes." end # Server value is already applied, nothing more to do end else # Field was NOT fetched - always preserve dirty state # Use will_change! to mark as dirty since clear_changes! cleared the flag will_change_method = "#{attr}_will_change!" send(will_change_method) if respond_to?(will_change_method) end end self end |
#fetch_cache!(keys: nil, includes: nil, preserve_changes: false, **opts) ⇒ self
Fetches the object with explicit caching enabled. This is a convenience method that calls fetch! with cache: true. Use this when you want to leverage cached responses for better performance.
270 271 272 |
# File 'lib/parse/model/core/fetching.rb', line 270 def fetch_cache!(keys: nil, includes: nil, preserve_changes: false, **opts) fetch!(keys: keys, includes: includes, preserve_changes: preserve_changes, cache: true, **opts) end |
#fetch_json(keys: nil, includes: nil) ⇒ Hash?
Returns raw JSON data from the server without updating the current object.
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/parse/model/core/fetching.rb', line 316 def fetch_json(keys: nil, includes: nil) query = {} if keys.present? keys_array = Array(keys).map { |k| Parse::Query.format_field(k) } query[:keys] = keys_array.join(",") end if includes.present? includes_array = Array(includes).map(&:to_s) query[:include] = includes_array.join(",") end response = client.fetch_object(parse_class, id, query: query.presence) return nil if response.error? response.result end |
#fetch_object ⇒ Parse::Object
Fetches the Parse object from the data store and returns a Parse::Object instance. This is a convenience method that calls fetch.
335 336 337 |
# File 'lib/parse/model/core/fetching.rb', line 335 def fetch_object fetch end |
#prepare_for_dirty_tracking!(key) ⇒ void
This method returns an undefined value.
Prepares object for dirty tracking by fetching if needed. Must be called BEFORE will_change! to prevent autofetch from wiping dirty state.
When will_change! captures the old value by calling the getter, it may trigger autofetch if the object is a pointer. That autofetch calls clear_changes! which wipes the dirty tracking state will_change! is trying to set up.
By fetching first, the object is no longer a pointer, so will_change! can proceed without triggering another fetch.
For selective fetch objects, this also marks the field as fetched to prevent autofetch during will_change!‘s getter call.
502 503 504 505 506 507 508 509 510 511 512 513 |
# File 'lib/parse/model/core/fetching.rb', line 502 def prepare_for_dirty_tracking!(key) # Fetch before will_change! to prevent clear_changes! interference if pointer? && !autofetch_disabled? autofetch!(key) end # Mark selective fetch fields as fetched to prevent autofetch during will_change! if has_selective_keys? && !field_was_fetched?(key) @_fetched_keys ||= [] @_fetched_keys << key unless @_fetched_keys.include?(key) end end |