UR::Object(3pm)
๐ UR::Object ๐
okay bestie, buckle UP, because we're talking about UR::Object(3pm) โ transactional, queryable, process-independent entities. yes it's a mouthful, no it's not scary, i promise. it's giving "the coolest base class you've never met" โจ
๐ธ NAME ๐ธ
UR::Object โ transactional, queryable, process-independent entities. that's the whole vibe. ๐ซถ
๐ SYNOPSIS ๐
make a new object in the current context and get it back, like:
$elmo = Acme::Puppet->create(
name => 'Elmo',
father => $ernie,
mother => $bigbird,
jobs => [$dance, $sing],
favorite_color => 'red',
);
plain accessors work the typical way, no surprises here:
$color = $elmo->favorite_color();
changes happen inside a transaction in the current context, bestie:
$elmo->favorite_color('blue');
non-scalar (has_many) properties get a whole lil buffet of accessors:
@jobs = $elmo->jobs();
$jobs = $elmo->job_arrayref();
$set = $elmo->job_set();
$iter = $elmo->job_iterator();
$job = $elmo->add_job($snore);
$success = $elmo->remove_job($sing);
now query the current context to find stuff:
$existing_obj = Acme::Puppet->get(name => 'Elmo');
# same reference as $existing_obj
@existing_objs = Acme::Puppet->get(
favorite_color => ['red','yellow'],
);
# this will not get elmo because his favorite color is now blue
@existing_objs = Acme::Puppet->get(job => $snore);
# this will return $elmo along with other puppets that snore,
# though we haven't saved the change yet..
save our changes:
UR::Context->current->commit;
too many puppets...:
$elmo->delete;
$elmo->play; # this will throw an exception now
$elmo = Acme::Puppet->get(name => 'Elmo'); # this returns nothing now
just kidding:
UR::Context->current->rollback; # not a database rollback, an in-memory undo
all is well:
$elmo = Acme::Puppet->get(name => 'Elmo'); # back again!
๐ DESCRIPTION ๐
okay so, UR::Objects are transactional, queryable, representations of entities. they exist to keep a clean separation between the physical reference sitting in your program and the logical entity that reference is actually about, and they do it all through one well-defined interface. slay. โจ
UR uses that separation to handle I/O for you, automatically. it gives you a query API, and it manages the difference between the state of entities in your application and their state in external persistence systems. the goal is to do all that transparently, keeping I/O logic orthogonal to "business logic", and hopefully making code around I/O completely unnecessary to write at all for most programs. no cap. ๐ฅ
instead of explicitly constructing and serializing/deserializing objects, the application layer just asks the current "context" for objects, based on their characteristics. the context is the one doing all the work: database connections, object state changes, references, relationships, in-memory transactions, queries and caching, all in tunable ways. main character energy, honestly. ๐
accessors fabricate references lazily, only as needed, through that same query API โ so objects behave the way a developer would traditionally expect in most cases. the whole goal of UR::Object is that your application doesn't have to do data management. just ask for what you want, use it, and let it go. ๐ฆ
and yes, UR::Objects support full reflection and meta-programming. its meta-object layer is fully self-bootstrapping (most of the classes UR is composed of are themselves UR::Objects), so the class data can introspect itself โ to the point where even classes can be created within transactions and discarded. literally iconic.
๐ INHERITANCE ๐
UR::ModuleBase Basic error, warning, and status messages for modules in UR.
UR::Object This class - general OO transactional OO features
๐ง WRITING CLASSES ๐ง
want the narrative version of how to write classes? go read UR::Manual::Tutorial. it's the storytime version. ๐
want the complete reference? that's UR::Manual::WritingClasses. the whole encyclopedia.
and for the meta-object API, see UR::Object::Type.
here's a simple example, declaring the class we used up above:
class Acme::Puppet {
id_by => 'name',
has_optional => [
father => { is => 'Acme::Puppet' },
mother => { is => 'Acme::Puppet' },
jobs => { is => 'Acme::Job', is_many => 1 },
]
};
you can also declare that exact same API but hand over extra internal details so the database mapping happens exactly how you want it:
class Acme::Puppet {
id_by => 'name',
has_optional => [
father => { is => 'Acme::Puppet', id_by => 'father_id' },
mother => { is => 'Acme::Puppet', id_by => 'mother_id' },
],
has_many_optional => [
job_assignments => { is => 'Acme::PuppetJob', im_its => 'puppet' },
jobs => { is => 'Acme::Job', via => 'job_assignments', to => 'job' },
]
};
โญ CONSTRUCTING OBJECTS โญ
new objects come back from create() and get(), and both of them delegate to the current context for all object construction. the context is the one actually doing it, they're just the messenger. ๐
create()will always create something new โ or it returnsundefif that identity is already known to be in use. no duplicates, periodt.get()lets the context decide internally whether to hand back a cached reference for the logical entities you asked for, or to construct brand new objects by loading data from the outside. it's a vibe check the context runs on your behalf.
๐ METHODS ๐
the examples below use $obj wherever an actual object reference is needed, and SomeClass wherever the class name can be used. in some cases we keep the synopsis going for a deeper illustration. ๐
๐ Base API ๐
get
$obj = SomeClass->get($id);
$obj = SomeClass->get(property1 => value1, ...);
@obj = SomeClass->get(property1 => value1, ...);
@obj = SomeClass->get('property1 operator1' => value1, ...);
query the current context for objects. ๐
it turns the parameters you passed in into a UR::BoolExpr and returns all objects of the given class that match. the current context decides whether the request can be fulfilled without hitting external queries. data gets loaded lazily from the underlying database(s) as needed to fulfill the request. it's giving efficient. ๐
in the simplest case โ asking for an object by id that's already cached โ the call to get() is an immediate hash lookup, and it is very fast. zoom zoom.
see UR::Manual::Queries, or peek at UR::Object::Set, UR::BoolExpr, and UR::Context for the details.
heads up bestie: if you call this in scalar context and more than one object matches the parameters you gave,get()will raise an exception throughdie. one match only, or it's drama. ๐
create
$obj = SomeClass->create(
property1 => $value1,
properties2 => \@values2,
);
create a new entity in the current context and get a reference back. โจ
the only property actually required to create an object is the "id" โ and even that is only required for objects that don't autogenerate their own ids. subclasses can override that requirement to be more restrictive if they want to be picky. ๐
if entities of this type persist in an underlying context, the entity won't show up there until commit. (i.e. no insert happens until just before a real database commit). and ngl, the object in question doesn't need to pass its own constraints at the moment it's initially created โ but it must be fully valid before the transaction which created it commits. no exceptions, literally.
delete
$obj->delete
deletes an object in the current context. ๐๏ธ
the $obj reference will be garbage collected at the discretion of the Perl interpreter, as soon as possible. and any attempt to use that reference after delete() is called will result in an exception. so don't do that. ๐
if the represented entity was loaded from the parent context (i.e. persistent database objects), it will not be deleted from that context (the database) until commit is called. that commit call does both the delete and the commit, presuming the complete save works across all involved data sources.
and if the transaction rolls back, the deleted object gets re-created in the current context, and a fresh reference will later be returnable by get(). see the documentation on UR::Context for details on how deleted objects are remembered and removed later from the database, and how deleted objects are re-constructed on STM rollback. yes, it remembers. iconic memory. ๐ง
copy
$obj->copy(%overrides)
copies the existing $obj by copying the values of all direct properties โ except for ID properties โ onto a newly created object of the same type. you can hand it a list of params and values as overrides for the existing values, or to specify an ID. fresh object, same energy. ๐ฆ
class
$class_name = $obj->class;
$class_name = SomeClass->class;
returns the name of the class of the object in question. see __meta__ below for the class meta-object. ๐
id
$id = $obj->id;
the unique identifier of the object within its class. ๐ซ
- for database-tracked entities this is the primary key value, or a composite blob containing the primary key values for multi-column primary keys.
- for regular objects private to the process, the default id embeds the hostname, process ID, and a timestamp to uniquely identify the
UR::Context::Processobject which is its final home.
when inheritance is involved beneath UR::Object, the 'id' may identify the object within the super-class as well. it's also possible for an object to have a different id upon sub-classification. wild, right? ๐
๐ Accessors ๐
every relationship declared in the class definition results in at least one accessor being generated for that class. free accessors, we love to see it. ๐
identity properties are read-only, while non-identity properties are read-write โ unless is_mutable is explicitly set to false.
assigning an invalid value is allowed temporarily, but the current transaction will be in an invalid state until it's corrected, and it will not be commitable. so like, fix it bestie.
the return value of an accessor when it mutates the object is the value of the property after the mutation has occurred. it tells you where things landed, not where they were.
single-value property accessors
by default, properties are expected to return a single value. one property, one value. simple. ๐
NAME
regular accessors have the same name as the property, exactly as declared, and they also work as mutators the way you'd commonly expect:
$value = $obj->property_name;
$obj->property_name($new_value);
when the property is declared with id_by, instead of recording the reference it records the id of the object automatically โ such that both will return different values after either one changes. sneaky but so smart. ๐ง
multi-value property accessors
when a property is declared with the "is_many" flag, a whole variety of accessors becomes available on the object. see "UR::Manual::WritingClasses" for more details on all the ways you can declare relationships between objects when writing classes. ๐ธ
using the example from the synopsis:
NAMEs (the property name pluralized) โจ
A has_many relationship gets declared using the plural form of the relationship name, okay? And the class gets an accessor generated that hands you back the whole list of property values. You can use it plain, or with extra filters on top โ both are valid:
@jobs = $elmo->jobs();
@fun_jobs = $elmo->jobs(is_fun => 1);
The singular name is what the rest of the accessors use... ๐
NAME (the property name in singular form) ๐
This one returns one item out of the group, and you gotta specify it in parameters. And if more than one item matches? Exception time, thrown via die():
$job = $elmo->job(name => 'Sing');
$job = $elmo->job(is_fun => 1);
# die: too many things are fun for Elmo
NAME_list ๐ธ
The default accessor is also hanging out as *_list. Works with or without extra filters, same energy:
@jobs = $elmo->job_list();
@fun_jobs = $elmo_>job_list(is_fun => 1);
NAME_set ๐
Hands you back a UR::Object::Set value representing the values, via *_set:
$set = $elmo->job_set();
$set = $elmo->job_set(is_hard => 1);
NAME_iterator ๐ฆ
Makes a fresh iterator for the set of property values, via *_iterator:
$iter = $elmo->job_iterator();
$iter = $elmo->job_iterator(is_fun => 1, -order_by => ['name]);
while($obj = $iter->next()) { ... }
add_NAME ๐
Drop a new item into the set of values with add_*:
$added = $elmo->add_job($snore);
There's a variation on this that'll construt the item and add it in one go. This second form of add_* would automatically identify that the line items also reference the order, and it establishes the correct converse relationship for you automatically. No cap:
@lines = $order->lines;
# 2 lines, for instance
$line = $order->add_line(
product => $p,
quantity => $q,
);
print $line->num;
# 3, if the line item has a multi-column primary key with auto_increment on the 2nd column called num
remove_NAME ๐
Items can be pulled back out of the assigned group, in a way that's symetrical with how they get added:
$removed = $elmo->remove_job($sing);
โจ Extended API โจ
These methods are available on any class defined by UR, bestie. They're convenience methods wrapping around UR::Context, UR::Object::Set, UR::BoolExpr, UR::Object::View, UR::Observer and Mock::Object. It's giving โจquality of lifeโจ.
create_iterator ๐ฅ
$iter = SomeClass->create_iterator(
property1 => $explicit_value,
property2 => \@my_in_clause,
'property3 like' => 'some_pattern_with_%_as_wildcard',
'property4 between' => [$low,$high],
);
while (my $obj = $iter->next) {
...
}
Takes the same sort of parameters as get(), but returns a UR::Object::Iterator for the matching objects instead.
The next() method hands back one object from the resulting set every time you call it, and undef once the results have been exhausted. ๐ซถ
UR::Object::Iterator instances are normal object references in the current process โ they are not context-oriented UR::Objects. They vanish upon dereference, and you cannot retrieve them by querying the context. (๐ gone, reduced to atoms.)
When you're using an iterator, the system attempts to return objects matching the params at the time the iterator is created, even if those objects do not match the params at the time they're returned from next(). Consider this case:
# many objects in the DB match this
my $iter = SomeClass->create_iterator(job => 'cleaner');
my $an_obj = SomeClass->get(job => 'cleaner', id => 1);
$an_obj->job('messer-upper'); # This no longer matches the iterator's params
my @iter_objs;
while (my $o = $iter->next) {
push @iter_objs, $o;
}
At the end, @iter_objs will contain several objects, including the object with id 1 โ even though its job is no longer 'cleaner'. Wild, right? ๐ญ
However, if an object matching the iterator's params is deleted between the time the iterator is created and the time next() would return that object, then next() will throw an exception. So like, don't do that.
define_set ๐ง
$set = SomeClass->define_set(
property1 => $explicit_value,
property2 => \@my_in_clause,
'property3 like' => 'some_pattern_with_%_as_wildcard',
'property4 between' => [$low,$high],
);
@subsets = $set->group_by('property3','property4');
@some_members = $subsets[0]->members;
Takes the same sort of parameters as get(), but returns a set object.
Sets are lazy (iconic behaviour honestly) and only query underlying databases as much as necessary. At any point in time the members() method returns all matches to the specified parameters.
See UR::Object::Set for details.
define_boolexpr ๐
$bx = SomeClass->define_boolexpr(
property1 => $explicit_value,
property2 => \@my_in_clause,
'property3 like' => 'some_pattern_with_%_as_wildcard',
'property4 between' => [$low,$high],
);
$bx->evaluate($obj1); # true or false?
Takes the same sort of parameters as get(), but returns a UR::BoolExpr object.
The boolean expression can be used to evaluate other objects to see if they match the given condition. The "id" of the object embeds the complete "where clause", and as a semi-human-readable blob, such is reconstitutable from it. ๐
See UR::BoolExpr for details on how to use this to do advanced work on defining sets, comparing objects, creating query templates, adding object constraints, etc.
add_observer ๐
$o = $obj1->add_observer(
aspect => 'someproperty'
callback => sub { print "change!\n" },
);
$obj1->property1('new value');
# observer callback fires....
$o->delete;
Adds an observer to an object, monitoring one or more of its properties for changes. ๐
The specified callback is fired upon property changes which match the observation request.
See UR::Observer for details.
create_mock ๐
$mock = SomeClass->create_mock(
property1 => $value,
method1 => $return_value,
);
Creates a mock object using using the class meta-data for "SomeClass" via Mock::Object.
Useful for test cases. (Test girlies, this one's for you. ๐)
๐ฎ Meta API ๐ฎ
The following methods let the application interrogate UR for information about the object in question. Nosy? Maybe. Useful? Absolutely.
__meta__ โญ
$class_obj = $obj->__meta__();
Returns the class metadata object for the given object's class. Class objects are from the class UR::Object::Type, and they hold information about the class' properties, data source, relationships to other classes, etc.
__extend_namespace__ ๐
package Foo::Bar;
class Foo::Bar { has => ['stuff','things'] };
sub __extend_namespace__ {
my $class = shift;
my $ext = shift;
return class {$class . '::' . $ext} { has => ['more'] };
}
Dynamically generate new classes under a given namespace. This gets called automatically by UR::ModuleLoader when an unidentified class name is used. (It just knows. ๐)
If Foo::Bar::Baz is not a UR class, and this occurs:
Foo::Bar::Baz->some_method()
Then this is called:
Foo::Bar->__extend_namespace__("Baz")
If it returns a new class meta, the code will proceed on as though the class had always existed. Main character behaviour, honestly.
If Foo::Bar does not exist, the above will be called recursively:
Foo->__extend_namespace__("Bar")
If Foo::Bar, whether loaded or generated, cannot extend itself for "Baz", the loader will go up the tree before giving up. This means a top-level module could dynamically define classes for any given class name used under it:
Foo->__extend_namespace__("Bar::Baz")
__errors__ ๐ญ
@tags = $obj->__errors__()
Return a list of UR::Object::Tag values describing the issues which would prevent a commit in the current transaction.
The base implementation checks the validity of an object by applying any constraints layed out in the class โ such as making sure any non-optional properties contain values, numeric properties contain numeric data, and properties with enumerated values only contain valid values.
Sub-classes can override this method to add additional validity checking. Go off. ๐
__display_name__ ๐
$text = $obj->__display_name__;
# the class and id of $obj, by default
$text = $line_item->__display_name__($order);
Stringifies an object. Some classes may choose to actually overload the stringification operator with this method. Even if they do not, this method will still attempt to identify this object in text form. The default returns the class name and id value of the object within a string.
It can be overridden to do a more nuanced job. The class might also choose to overload the stringification operator itself with this method, but even if it doesn not, the system will presume this method can be called directly on an object for reasonable stringificaiton. ๐ซถ
__context__ ๐ฆ
$c = $self->__context__;
Return the UR::Context for the object reference in question.
In UR, a "context" handles the connections between objects, instead of relying on having objects directly reference each other. This allows an object to have a relationship with a large number of other logical entities, without having a "physical" reference present within the process in question.
All attempts to resolve non-primitive attribute access go through the context.
๐ Extension API ๐
These methods are primarily of interest for debugging, for test cases, and internal UR development.
They are likely to change before the 1.0 release. Consider this your vibe check warning, bestie. โ ๏ธ
__signal_change__ ๐
Called by all mutators to tell the current context about a state change.
__changes__ ๐
@tags = $obj->__changes__()
@tags = $obj->__changes__('prop1', 'prop2', ...)
Return a list of changes present on the object _directly. This is really only useful internally, because the boundary of the object is internal/subjective. Callers may also request only changes to particular properties.
Changes to objects' properties are tracked by the system. If an object has been changed since it was defined or loaded from its external data source, then changed() will return a list of UR::Object::Tag objects describing which properties have been changed.
Like, work is in-progress on an API that would let you ask for exactly the chunk of changes currently in effect in the current transaction that would impact the return value of a given list of properties. Bestie, this would be directly usable by a view/observer. Not done yet though, so don't come for me. ๐ง
__define__
This one is used internally to "virtually load" stuff. It literally just asserts that things already existed externally and then acts like they were just loaded... โจ It's what happens for classes defined in the source code (which is the default, btw) via the class {} magic, instead of in some database like we'd do for regular objects.
__strengthen__
$obj->__strengthen__();
Marks this object as unloadable by the object cache pruner. ๐ก๏ธ
Okay so here's the vibe: UR objects are normally tracked by the current Context for the whole life of the application, but the programmer can specify a limit to cache size, and in that case old, unchanged objects get periodically pruned from the cache. If you call strengthen() on an object, it's effectively locked in the cache and will not be considered for pruning. Periodt. ๐
See UR::Context for more info about the pruning mechanism.
is_strengthened
Check if an object has been stengthened, __stengthen__. (yes the spelling is like that, no I will not be fixing it ๐ซถ)
__weaken__
$obj->__weaken__();
Gives a lil hint ๐ to the object cache pruner that this instance is not going to be used in the application in the future, and should be removed with preference when pruning the cache.
is_weakened
Check if an object has been weakened, __weaken__.
DESTROY
Perl calls this method on any object before garbage collecting it. Your application should never call it explicitly, okay? ๐ซ
The DESTROY handler is overridden in UR::Object. So if you override it in a subclass, be sure to call $self->SUPER::DESTROY() before exiting your override, or errors will occur. Like, actual errors. Not the fun kind. ๐
๐ ERRORS, WARNINGS and STATUS MESSAGES ๐
When an error occurs that's "exceptional", the API will throw an exception via die(). Simple as that. ๐ค
But in some cases, when the possibility of failure is "not-exceptional", the method will simply return false. In scalar context that's undef. In list context it's an empty list. No drama. ๐
And when there's ambiguity about whether something is an error or not โ get() for instance, might simply match zero items, ...or fail to understand your parameters โ an exception is used. Yeah, that one's on purpose.
error_message
The standard way to convey that an error has occurred is to set ->error_message() on the object. This will propagate to the class, and through its inheritance. It's much like DBI's errstr method, which affects the handle on which it was called, its source handle, and the DBI package itself. Family tree of blame, honestly. ๐ณ
warning_message
Calls to warning_message also record themselves on the object in question, and its class(es).
They also emit a standard Perl warn(), which will invoke $SIG{__WARN__}. So heads up if you've got a handler hanging around. โ ๏ธ
status_message
Calls to status_message are also recorded on the object in question. They can be monitored through hooks, as can the other messages. ๐
See UR::ModuleBase for more information.
๐ SEE ALSO ๐
UR,UR::Object::Type,UR::ContextUR::Maual::Tutorial,UR::Manual::WritingClasses,UR::Manual::Queries,UR::Manual::TransactionsUR::ObjectDeprecatedcontains additional methods which are deprecated in the API.
And that's the whole page, bestie, we made it ๐ This one's from perl v5.32.1, dated 2022-01-17, and it lives at UR::Object(3pm) โ go forth and object-orient iconically. ๐โจ
๐ฆ where this page comes from
- debian bookworm: libur-perl 0.470+ds-2 ยท Artistic-1.0-Perl OR GPL-1.0-or-later
- ubuntu jammy: libur-perl 0.470+ds-2 ยท Artistic-1.0-Perl OR GPL-1.0-or-later
- ubuntu noble: libur-perl 0.470+ds-2 ยท Artistic-1.0-Perl OR GPL-1.0-or-later
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms