Skip to content

class Athena::ORM::Proxy(T)
inherits Athena::ORM::Entity #

Lazy ToOne wrapper for owning-side associations.

By default, loading an entity also loads the entity each of its AORMA::OneToOne and AORMA::ManyToOne fields points to. Typing an owning-side field as AORM::Proxy(Target)? instead of Target? makes it lazy: the field holds a proxy that only knows the target's identifier, and loads the target the first time it's needed.

@[AORMA::Entity]
class User < AORM::Entity
  # ...

  @[AORMA::OneToOne]
  property avatar : AORM::Proxy(Avatar)? = nil
end

user = em.find! User, 1 # Doesn't query the user's avatar
avatar = user.avatar.not_nil!

avatar.loaded? # => false
avatar.id      # => 10, still not loaded
avatar.url     # => "https://example.com/avatar.png", loads the avatar
avatar.loaded? # => true

The target entity's methods can be called on the proxy directly; any method the proxy doesn't define itself is forwarded to #inner, which loads the target on first use. #id is the exception: when the target's identifier field is named id, it's read from the proxy without loading anything.

Since the field holds a proxy, raw entity assignment (user.avatar = some_avatar) requires an explicit wrap: user.avatar = AORM::Proxy(Avatar).wrap(some_avatar). The wrapped entity is persisted, and cascades, like one assigned to a plain Target? field.

Identity#

The proxy is a separate object from the entity it loads. Methods every object has, such as ==, hash, same? and is_a?, act on the proxy itself rather than being forwarded. Code that needs the raw Target reference (to pass to a function whose signature expects it, or to do an is_a? check against a subclass) calls #inner explicitly.

Object identity is NOT preserved across a load: once the proxy loads, AORM::EntityManager#find returns the loaded Target rather than the proxy, so proxy.same?(em.find(Avatar, proxy.id)) is false. Stale proxy references stay functional, delegating to the loaded entity.

Note

Owning-side ToOne only. Fields mapped with mapped_by are always loaded along with their owner, and typing one as a proxy raises when the entity's metadata is built.

Todo

Fields typed as a plain Target? load each missing target with its own query once the owner's query has finished, rather than batching them.

Constructors#

.wrap(value : T) : self#

Wraps an already-loaded entity. The resulting proxy is loaded? from the start and never issues a SELECT.

user.avatar = AORM::Proxy(Avatar).wrap avatar
View source

Class methods#

.wrap(value : Proxy(T)) : Proxy(T)#

Idempotent: passing through an existing proxy returns it unchanged.

View source

Methods#

#id#

Allows reading the #id of a proxy without triggering a load.

This only applies to an identifier field named id; if the target has no such field, this raises. Reading an identifier field with any other name, such as proxy.code, loads the target like any other forwarded method.

View source

#inner : T#

Returns the target entity, loading it on the first call.

Raises if the target no longer exists in the database.

View source

#inspect(io : IO) : Nil#

Appends a String representation of this object which includes its class name, its object address and the values of all instance variables.

class Person
  def initialize(@name : String, @age : Int32)
  end
end

Person.new("John", 32).inspect # => #<Person:0x10fd31f20 @name="John", @age=32>
View source

#loaded? : Bool#

Returns true if the target entity has been loaded, or the proxy was created by .wrap.

View source