UR::Manual::Tutorial(3) 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'
                                );