Provided by: libur-perl_0.470+ds-3_all 

NAME
UR::Manual::Tutorial - Step-by-step guide to building a set of classes for a simple database schema
Overview
We'll use the familiar "Music Database" example used in many ORM tutorials:
Our database has the following basic entities and relationships:
• One artist can have many CDs
• One CD belongs to one artist
• one CD can have many tracks
• one track belongs to one CD
The "ur" command-line program
The tool for working with UR from the command line is 'ur' . It is installed with the UR module suite.
Just type "ur" and hit enter, to see a list of valid ur commands:
> ur
Sub-commands for ur:
init NAMESPACE [DB] initialize a new UR app in one command
define ... define namespaces, data sources and classes
describe CLASSES-OR-MODULES show class properties, relationships, meta-data
update ... update parts of the source tree of a UR namespace
list ... list objects, classes, modules
sys ... service launchers
test ... tools for testing and debugging
The "ur" command works a lot like the "svn" command: it is the entry point for a list of other
subordinate commands.
• Typing something like "ur browser" will run the browser tool.
• Typing something like "ur define" will give another list, of even more granular commands which are
under "ur define":
> ur define
Sub-commands for ur define:
namespace NSNAME create a new namespace tree and top-level module
db URI NAME add a data source to the current namespace
class --extends=? [NAMES] Add one or more classes to the current namespace
At any point, you can put '--help' as a command line argument and get some (hopefully) helpful
documentation.
In many cases, the output also resembles svn's output where the first column is a character like 'A' to
represent something being added, 'D' for deleted, etc.
(NOTE: The "ur" command, uses the Command API, an API for objects which follow the command-pattern. See
UR::Command for more details on writing tools like this.
Define a UR Namespace
A UR namespace is the top-level object that represents your data's class structure in the most general
way. For this new project, we'll need to create a new namespace, perhaps within a testing directory.
ur define namespace Music
And you should see output like this:
A Music (UR::Namespace)
A Music::Vocabulary (UR::Vocabulary)
A Music::DataSource::Meta (UR::DataSource::Meta)
A Music/DataSource/Meta.sqlite3-dump (Metadata DB skeleton)
showing that it created 3 classes for you, Music, Music::Vocabulary and Music::DataSource::Meta, and
shows what classes those inherit from. In addition, it has also created a file to hold your metadata.
Other parts of the documentation give a more thorough description of Vocabulary and Metadata classes.
Define a Data Source
A UR DataSource is an object representing the location of your data. It's roughly analogous to a Schema
class in DBIx::Class, or the "Base class" in Class::DBI.
Note: Because UR can be used with objects which do NOT live in a database, using a data source is
optional, but is the most common case.
Most ur commands operate in the context of a Namespace, including the one to create a datasource, so you
need to be within the Music's Namespace's directory:
cd Music
and then define the datasource. We specify the data source's type as a sub-command, and the name with the
--dsname argument. For this example, we'll use a brand new SQLite database. For some other, perhaps
already existing database, give its connect string instead.
ur define db dbi:SQLite:/var/lib/music.sqlite3 Example
which generates this output:
A Music::DataSource::Example (UR::DataSource::SQLite,UR::Singleton)
...connecting...
....ok
and creates a symlink to the database at:
Music/DataSource/Example.sqlite3
and shows that it created a class for your data source called Music::DataSource::Example, which inherits
from UR::DataSource::SQLite. It also created an empty database file and connected to it to confirm that
everything is OK.
Create the database tables
Here are the table creation statements for our example database. Put them into a file with your favorite
editor and call it example-db.schema.txt:
CREATE TABLE artist (
artist_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE cd (
cd_id INTEGER NOT NULL PRIMARY KEY,
artist_id INTEGER NOT NULL CONSTRAINT CD_ARTIST_FK REFERENCES artist(artist_id),
title TEXT NOT NULL,
year INTEGER
);
CREATE TABLE track (
track_id INTEGER NOT NULL PRIMARY KEY,
cd_id INTEGER NOT NULL CONSTRAINT TRACK_CD_FK REFERENCES cd(cd_id),
title TEXT NOT NULL
);
This new SQLite data source assumes the database file will have the pathname
Music/DataSource/Example.sqlite3. You can populate the database schema like this:
sqlite3 DataSource/Example.sqlite3 < example-db.schema.txt
Create your data classes
Now we're ready to create the classes that will store your data in the database.
You could write those classes by hand, but it's easiest to start with an autogenerated group built from
the database schema:
ur update classes-from-db
is the command that performs all the magic. You'll see it go through several steps:
1. Find all the defined datasources within the current namespace
2. Query the data sources about what tables, columns, constraints and foreign keys are present
3. Load up all the classes in the current namespace
4. Figure out what the differences are between the database schema and the class structure
5. Alter the class metadata to match the database schema
6. Use the new class metadata to write headers on the Perl module files in the namespace
There will now be a Perl module for each database table. For example, in Cd.pm:
package Music::Cd;
use strict;
use warnings;
use Music;
class Music::Cd {
table_name => 'CD',
id_by => [
cd_id => { is => 'INTEGER' },
],
has => [
artist => { is => 'Music::Artist', id_by => 'artist_id', constraint_name => 'CD_ARTIST_FK' },
artist_id => { is => 'INTEGER' },
title => { is => 'TEXT' },
year => { is => 'INTEGER', is_optional => 1 },
],
schema_name => 'Example',
data_source => 'Music::DataSource::Example',
};
1;
The first few lines are what you would see in any Perl module. The keyword "class" tells the UR system to
define a new class, and lists the properties of the new class. Some of the important parts are that
instances of this class come from the Music::DataSource::Example datasource, in the table 'CD'. This
class has 4 direct properties (cd_id, artist_id, title and year), and one indirect property (artist).
Instances are identified by the cd_id property.
Methods are automatically created to match the property names. If you have an instance of a CD, say $cd,
you can get the value of the title with "$cd->title". To get back the artist object that is related to
that CD, "$cd->artist".
CRUD (Create, Read, Update, Delete)
Create
Creating new object instances is done with the create method; its arguments are key-value pairs of
properties and their values.
#!/usr/bin/perl
use strict;
use Music;
my $obj1 = Music::Artist->create(name => 'Elvis');
my $obj2 = Music::Artist->create(name => 'The Beatles');
UR::Context->commit();
And that's it. After this script runs, there will be 2 rows in the Artist table.
Just a short aside about that last line... All the changes to your objects while the program runs
(creates, updates, deletes) exist only in memory. The current "Context" manages that knowledge. Those
changes are finally pushed out to the underlying data sources with that last line.
Read
Retrieving object instances from the database is done with the get() method. A get() with no arguments
will return a list of all the objects in the table.
@all_cds = Music::Cd->get();
If you know the "id" (primary key) value of the objects you're interested in, you can pass that "id"
value as a single argument to get:
$cd = Music::Cd->get(3);
An arrayref of identity values can be passed-in as well. Note that if you query is going to return more
than one item, and it is called in scalar context, it will generate an exception.
@some_cds = Music::Cd->get([1, 2, 4]);
To filter the return list by a property other than the ID property, give a list of key-value pairs:
@some_cds = Music::Cd->get(artist_id => 3);
This will return all the CDs with the artist ID 5, 6 or 10.
@some_cds = Music::Cd->get(artist_id => [5, 6, 10]);
get() filters support operators other than strict equality. This will return a list of CDs with artist ID
2 and have the word 'Ticket' somewhere in the title.
@some_cds = Music::Cd->get(artist_id=> 2, title => { operator => 'like', value => '%Ticket%'} );
To search for NULL fields, use undef as the value:
@cds_with_no_year = Music::Cd->get(year => undef);
get_or_create
get_or_create() is used to retrieve an instance from the database if it exists, or create a new one if it
does not.
$possibly_new = Music::Artist->get_or_create(name => 'The Band');
Update
All the properties of an object are also mutators. To change the object's property, just call the method
for that property with the new value.
$cd->year(1990);
Remember that any changes made while the program runs are not saved in the database until you commit the
changes with "UR::Context->commit".
Delete
The delete() method does just what it says.
@all_tracks = Music::Track->get();
foreach my $track ( @all_tracks ) {
$track->delete();
}
Again, the corresponding database rows will not be removed until you commit.
Relationships
After running ur update classes, it will automatically create indirect properties for all the foreign
keys defined in the schema, but not for the reverse relationships. You can add other relationships in
yourself and they will persist even after you run ur update classes again. For example, there is a
foreign key that forces a track to be related to one CD. If you edit the file Cd.pm, you can define a
relationship so that CDs can have many tracks:
class Music::Cd {
table_name => 'CD',
id_by => [
cd_id => { is => 'INTEGER' },
],
has => [
artist => { is => 'Music::Artist', id_by => 'artist_id', constraint_name => 'CD_ARTIST_FK' },
artist_id => { is => 'INTEGER' },
title => { is => 'TEXT' },
year => { is => 'INTEGER' },
tracks => { is => 'Music::Track', reverse_as => 'cd', is_many => 1 }, # This is the new line
],
schema_name => 'Example',
data_source => 'Music::DataSource::Example',
};
This tells the system that there is a new property called 'tracks' which returns items of the class
Music::Track. It links them to the acting CD object through the Track's cd property.
After that is in place, you can ask for a list of all the tracks belonging to a CD with the line
@tracks = $cd->tracks()
You can also define indirect relationships through other indirect relationships. For example, if you edit
Artist.pm to add a couple of lines:
class Music::Artist {
table_name => 'ARTIST',
id_by => [
artist_id => { is => 'INTEGER' },
],
has => [
name => { is => 'TEXT' },
cds => { is => 'Music::Cd', reverse_as => 'artist', is_many => 1 },
tracks => { is => 'Music::Track', via => 'cds', to => 'tracks', is_many => 1},
],
schema_name => 'Example',
data_source => 'Music::DataSource::Example',
};
This defines a relationship 'cds' to return all the CDs from the acting artist. It also defines a
relationship called 'tracks' that will, behind the scenes, first look up all the CDs from the acting
artist, and then find and return all the tracks from those CDs.
Additional arguments can be passed to these indirect accessors to get a subset of the data
@cds_in_1990s = $artist->cds(year => { operator => 'between',
value => [1990,1999] } );
would get all the CDs from that artist where the year is between 1990 and 1999, inclusive.
Note that is_many relationships should always be named with plural words. The system will auto-create
other accessors based on the singular name for adding and removing items in the relationship. For
example:
$artist->add_cd(year => 1998, title => 'Cool Jams' );
would create a new Music::Cd object with the given year and title. The cd_id will be autogenerated by the
system, and the artist_id will be automatically set to the artist_id of $artist.
Custom SQL
It's possible to use get() with custom SQL to retrieve objects, as long as the select clause includes all
the ID properties of the class. To find Artist objects that have no CDs, you might do this:
my @artists_with_no_cds =
Music::Artist->get(sql => 'select artist.artist_id,
count(cd.artist_id)
from artist
left join cd on cd.artist_id = artist.artist_id
group by artist.artist_id
having count(cd.artist_id) = 0'
);
perl v5.38.2 2024-06-15 UR::Manual::Tutorial(3pm)