Models

As you've seen from the getting started page, a Model class type defines the objects that drive an ORB API. Most of the functionality of the Table class that you have been working with is actually derived from the base Model class. In addition to tables, there is also a View model type in ORB.

Note: All examples will build off the Getting Started models that we have already defined.

Tables vs. Views

The Table class is a model that allows for read and write capability, and will be what you use to store records and retrieve them later.

The View class is a read-only model that allows you to define multiple columns to be pre-joined together in the database or store for easy querying and access later on.

Basics

The basics for any data-driven system is CRUD -- Create, Read, Update and Delete. We have already touched upon this in the step-by-step introduction to ORB in the gettting started page, but will go into it in more detail now.

Creating records

There are a few different ways that you can do this. The most straightforward way is to instantiate a new model object, set its column values, and then save it.

1: Model constructor

>>> from intro import *

>>> # create a blank record, then populate properties and save
>>> u = User()
>>> u.set('username', 'bob')
>>> u.set('password', 'my_password_3')
>>> u.save()

>>> # create a record with properties, then save
>>> u = User({'username': 'sally', 'password': 'p4'})
>>> u.save()

When to use one way or another is up to you, based on what makes sense for the application. To ORB, either way works just as well.

2: Model.create [classmethod]

If you do know what the properties you are going to create your record with before hand, you can also use the create class method, which is a convenience method to the second approach:

>>> from intro import *

>>> # create record with properties
>>> u = User.create({'username': 'sam', 'password': 'p5'})

The create method will create a new instance of the model classin memory, populate the column fields from the given dictionary, and then save the record to the database and return it.

3: Model.ensureExists [classmethod]

The above two examples will create new models, without care for what already exists. For instance, if you have a Unique constraint on a column and try to create a second record that conflicts, the system will raise an orb.errors.DuplicateEntryFound error. If what you want is to make sure that a record exists in the database with a certain set of properties, you can use the ensureExists classmethod instead. This will first look to see if a record of a given model already is defined in your database and return it to you, otherwise, it will create the new record first and then return it.

>>> from intro import *
>>> import orb.errors

>>> # long way: try to create record, if that fails, look it up
>>> try:
...     u = User.create({'username': 'john', 'password': 'p5'})
... except orb.errors.DuplicateEntryFound:
...     u = User.byUsername('john')
>>> print u.id()
1

>>> # short way: use ensureExists to do the same thing
>>> u = User.ensureExists({'username': 'john'}, defaults={'password': 'p5'})
>>> print u.id()
1
>>> u = User.ensureExists({'username': 'susan'}, defaults={'password': 'p6'})
>>> print u.id()
6

As you can see from the above example, the first call to ensureExists returned the already created user for id 1. The second call created a new user with id 6.

One important difference to note here: when using ensureExists, you are providing 2 sets of column/value pairs. The first set is what will be used to verify uniqueness -- so in this example, we want to make sure a User of username 'john' exists. If that user does not exist, then create it with the additional default values. If we provided both username and password, then it would verify that combination is unique and in the database, but that is not what we care about since the user may have changed their password.

4: Collection.add/Collection.create

The final way to create new models is through a collection. This is a way to create models and relationships when working with reverse lookups and pipes, but we will get into that more in the collection page.

Reading Records

As with creation, there are a number of ways to retrieve records. Two of the more common ways are retrieval via id and retrieval via index, as we discussed in the getting started page. The most general way however is to use ORB's query syntax, but that is so powerful it deserves it's own page.

So, for now, the basics:

1: Fetch an ID

>>> from intro import *
>>> u = User(1)

That's it. Simply provide the ID of the record that you are looking for to the constructor and it will go fetch it for you. If the record does not exist in the database, the RecordNotFound error will be raised:

>>> from intro import *
>>> import orb.errors
>>> try:
...     u = User(1)
... except orb.errors.RecordNotFound:
...     print 'do something'

2: Model Indexes

Indexes provide a structure that allows common lookups and queries to be pre-defined for both database lookup efficiency, and code reduction. These objects are added to the Model class definition and are callable as classmethod's returning either a record or collection based on their uniqueness.

From the intro.py example, we had defined a few different indexes:

>>> from intro import *
>>> u = User.byUsername('john')
>>> u = User.byName('John', 'Doe')

We will go more in-depth on indexes in the index page, but this is the basic idea. These methods will not throw an error for a record not found. They will return None for nothing found instead:

>>> from intro import *
>>> u = User.byUsername('john')
>>> print u
<intro.User object at 0x10346f490>
>>> u = User.byUsername('johnny')
>>> print u
None

The difference here is that we're not directly fetching a known resource (as with lookup by ID) that we expect to exist. We are returning the results of a pre-defined query that we don't know if it exists or not.

3: Model.all

Another option for retrieval is to simply return all the records that exist for the model. This isn't recommended for large data sets, but can be very useful if you want to cache things like statuses that are relatively known and small enough.

>>> from intro import *
>>> users = User.all()
>>> print len(users)
6
>>> for user in users:
...     print user.get('username')
'john'
'jane'
'bob'
'sally'
'sam'
'susan'

4: Model.select

The most general way to retrieve records however, is to use the Model.select class method. This will allow you to build a query and filter out records that you want to retrieve, limit the results of your search, order the records, and define specific columns to fetch as well. All other systems build on top of this generic one -- Indexes simply pre-define the query information to provide to the model selection for instance.

We will go into more detail on the querying options in the query documentation, but if we want to show how the byUsername and byName indexes work as an example, we could fetch the same information by doing:

>>> from intro import *
>>> from orb import Query as Q

>>> # byUsername lookup
>>> john = User.select(where=Q('username') == 'john').first()
>>> print john.id()
1

>>> # byName lookup
>>> q = (Q('first_name') == 'John') & (Q('last_name') == 'Doe')
>>> users = User.select(where=q)
>>> print len(users)
1

Updating Records

As we explored in the getting started page, you can update records by calling the set method, however there are a couple of other ways to do it as well:

1: Model.set

>>> from intro import *

>>> # update one column at a time
>>> u = User(1)
>>> u.set('username', 'jack')
>>> u.save()

2: Model.update

The second way to update a model is in bulk, passing in a key/value pair of columns. This will still call the internal setter methods if you want to do custom work when changing a column.

>>> from intro import *

>>> # update multiple columns at once
>>> u = User(1)
>>> u.update({'username': 'john', 'first_name': 'John', 'last_name': 'Smith'})
>>> u.save()

Note that you still need to save after you have modified column values. Calling set or update will only change the values that are stored in memory, not in the database, until save is called.

Deleting Records

The final operation for all CRUD ORMs, deleting records. In ORB there are really 2 ways to remove records -- individually, or in bulk from a collection.

1: Model.delete

The easiest way to remove a record from the database is to simply call delete on it.

>>> from intro import *

>>> # remove a single user record
>>> u = User(1)
>>> u.delete()
1

2: Collection.delete

The second way to remove records is through a collection. This can be done via collectors, or from a direct selection:

>>> from intro import *

>>> # remove all group-user relations
>>> group_users = GroupUser.all()
>>> group_users.delete()
2

The response from deleting records is the number of records that were removed.

Models: Events

As operations are happening throughout the orb system, events will be triggered and can be connected to for performing custom operations.

All events can set the preventDefault property to True to override the default functionality.

These are the events associated with a Model class:

Model.onSync [classmethod]

The onSync method will be called during the Database sync method, directly after the model has been created or updated in the datastore. This method is commonly used to define default values for a database.

An example of this could be pre-defined status classes:

import orb

class Status(orb.Table):
  id = orb.IdColumn()
  code = orb.StringColumn(flags={'Unique'})
  name = orb.StringColumn()

  @classmethod
  def onSync(cls, event):
    for default in (('in_progress', 'In Progress',
                     'approved', 'Approved',
                     'rejected', 'Rejected')):
      code, name = default
      cls.ensureExists({'code': code}, defaults={'name': name})

This code, when synced to different databases, will ensure that the default statuses exist in each environment.

Model.onPreSave and Model.onPostSave

The onPreSave method is called right before the save method is called, and after a successful database save the onPostSave will be called.

import orb

class Comment(orb.Table):
  id = orb.IdColumn()
  text = orb.TextColumn()
  created_by = orb.ReferenceColumn(reference='User')
  parent = orb.ReferenceColumn(reference='Comment')

  def onPostSave(self, event):
    # if this is a new comment, notify the users in the thread
    if event.newRecord:
      # collect the users in this thread
      parent = self.parent()
      users_in_thread = set()
      while parent:
        users_in_thread.add(parent.get('created_by'))
        parent = parent.parent()
      users_in_thread.remove(self.get('created_by'))

      # notify the users in this thread about the new comment
      ...

Model.onChange

The onChange event is fired whenever a column value is changed. This will be done at runtime, so this will happen before the changes are saved to the database. You can use this to provide additional changes and verification on individual columns.

import orb
from orb.errors import ValidationError

class Ticket(orb.Table):
  id = orb.IdColumn()
  status = orb.ReferenceColumn(reference='Status')
  title = orb.StringColumn()
  description = orb.TextColumn()

  def onChange(self, event):
    if event.column.name() == 'status':
      if event.old.name() == 'Closed' and event.value().name() != 'Reopened':
        raise ValidationError('When a ticket is closed, it must be reopened first.')

Model.onDelete

The onDelete event is fired whenever a model is removed from the database. This will be called before the delete actually occurs, and you can set the preventDefault in the event to block the normal process from occurring. (You can also always raise an error)

import orb

class User(orb.Table):
  id = orb.IdColumn()
  username = orb.StringColumn()
  active = orb.BooleanColumn()

  def onDelete(event):
    self.set('active', False)
    self.save()
    event.preventDefault = True

In this example, we override the default functionality to disable deleting a User record, and instead just deactivate them.

Model.onInit and Model.onLoad

The final events for the Model class are the onInit and onLoad events. These events are fired when a model is initialized. The onInit event is triggered when a new model is created, while the onLoad event is triggered when a model has been loaded from the database.

import orb

class Comment(orb.Table):
  id = orb.IdColumn()
  created_by = orb.ReferenceColumn(reference='User')
  text = orb.TextColumn()

  def onInit(self, event):
    self.set('created_by', User.currentRecord())

In this example above, we can use the onInit callback to assign the default created_by user to a current user. (Note, this assumes there is a User.currentRecord method).

Inheritance

As mentioned in the introduction, ORB is an object-oriented framework. One of the more powerful aspects of the system is it's inheritance structure -- which will allow you to inherit data between models easily.

import orb

class Asset(orb.Table):
  id = orb.IdColumn()
  code = orb.StringColumn(flags={'Unique'})
  display_name = orb.StringColumn()
  parent = orb.ReferenceColumn(reference='Asset')
  project = orb.ReferenceColumn(reference='Project')

  type = orb.StringColumn(flags={'Polymorphic'})

  children = orb.ReverseLookup(from_column='Asset.parent')
  dependsOn = orb.Pipe(through_path='Dependency.target.source')
  dependencies = orb.Pipe(through_path='Dependency.source.target')

class Dependency(orb.Table):
  id = orb.IdColumn()
  source = orb.ReferenceColumn(reference='Asset')
  target = orb.ReferenceColumn(reference='Asset')

class Project(Asset):
  budget = orb.LongColumn()
  supervisors = orb.Pipe(through_path='ProjectSupervisor.project.user')

  def onInit(self, event):
    self.set('project', self)

class ProjectSupervisor(orb.Table):
  id = orb.IdColumn()
  project = orb.ReferenceColumn(reference='Project')
  user = orb.ReferenceColumn(reference='User')

class Character(Asset):
  polycount = orb.LongColumn()
  is_hero = orb.BooleanColumn()

In the above example, we have defined a number of tables. The key points to the inheritance structure are 2 points:

Python class inheritance

To define an inheritance structure in the ORM, you simply need to inherit one model from another, as the Character and Project models inherit from the Asset model.

>>> Character.schema().dbname()
'characters'
>>> Project.schema().dbname()
'projects'
>>> Project.schema().inherits()
'Asset'

Polymorphic column

To have the system be able to automatically track the source class, you should define a polymorphic column in the base table. This will store the model name when it saves, and then use that type when inflating.

>>> char = Character({'code': 'bob', 'display_name': 'Bob'})
>>> char.get('type')
'Character'

Advanced Usage

Defining custom getter's and setter's

So far, we have only used the get and set methods for a Model to modify it. There are also getter and setter methods that get automatically generated for you that you can use, and modify, to provide custom functionality.

>>> from intro import *

>>> # get a user record
>>> u = User(2)
>>> print u.username()  # same as doing u.get('username')
'jane'
>>> u.setUsername('jill')  # same as doing u.set('username', 'jill')

The getter and setter methods can be overridden to provide additional, custom functionality. If we modify the intro.py file to read:

import orb

class User(orb.Table):
  id = orb.IdColumn()
  username = orb.StringColumn(flags={'Required'})
  password = orb.StringColumn(flags={'Required'})
  first_name = orb.StringColumn()
  last_name = orb.StringColumn()

  addresses = orb.ReverseLookup(from_column='Address.user')
  preferences = orb.ReverseLookup(from_column='Preference.user', flags={'Unique'})
  groups = orb.Pipe(through_path='GroupUser.user.group')

  byUsername = orb.Index(columns=['username'], flags={'Unique'})
  byName = orb.Index(columns=['first_name', 'last_name'])

  @username.getter()
  def _get_username(self):
      print 'getting username'
      return self.get('username', useMethod=False)

  @username.setter()
  def _set_username(self, username):
      print 'setting username'
      return self.set('username', username, useMethod=False)

Providing the @username.getter() and @username.setter() decorators will indicate to the system to route all get's and set's through those methods.

>>> from intro import *
>>> u = User(1)
>>> print u.username()
'getting username'
'john'
>>> print u.get('username')
'getting username'
'john'

Virtual Columns and Collectors

Another powerful feature to the ORB framework is the ability to define virtual columns and collectors. These are properties that will be exposed to the schema, but are defined in code and not stored in the database.

This is done by using the orb.virtual decorator.

import orb

class User(orb.Table):
  id = orb.IdColumn()
  username = orb.StringColumn()
  first_name = orb.StringColumn()
  last_name = orb.StringColumn()
  groups = orb.Pipe(through_path='GroupUser.user.group')

  @orb.virtual(orb.StringColumn)
  def displayName(self):
    return '{0} {1}'.format(self.get('first_name'), self.get('last_name'))

  @orb.virtual(orb.Collector)
  def allGroups(self, **context):
     groups = self.groups(**context)
     groups.add(orb.Group.byName('Everyone'))
     return groups
>>> u = User({'first_name': 'John', 'last_name': 'Doe'})
>>> print u.get('display_name')
'John Doe'
>>> print u.displayName()
'John Doe'
>>> u.schema().column('display_name') is not None
True
>>> u.allGroups().values('name')
['Everyone']

In this example, we have added 2 virtual methods -- displayName and allGroups. These will now exist within the schema, however will not exist in the database. By default, virtual methods are ReadOnly, so if you try to set the display_name value, it will raise an error.

To support setting virtual columns, you can use the setter decorator like so:

import orb

class User(orb.Table):
  id = orb.IdColumn()
  username = orb.StringColumn()
  first_name = orb.StringColumn()
  last_name = orb.StringColumn()
  groups = orb.Pipe(through_path='GroupUser.user.group')

  @orb.virtual(orb.StringColumn)
  def displayName(self):
    return '{0} {1}'.format(self.get('first_name'), self.get('last_name'))

  @displayName.setter()
  def setDisplayName(self, name):
    first_name, last_name = name.split(' ', 1)
    self.set('first_name', first_name)
    self.set('last_name', last_name)

  @orb.virtual(orb.Collector)
  def allGroups(self, **context):
     groups = self.groups(**context)
     groups.add(orb.Group.byName('Everyone'))
     return groups
>>> u = User({'first_name': 'John', 'last_name': 'Doe'})
>>> u.set('display_name', 'Tom Jones)
>>> print u.get('first_name')
'Tom'
>>> print u.get('last_name')
'Jones'

Schema as Mixin

Another common paradigm (and one we will get into later with automatic schema generation) is to have one class be the base schema, and the other be the custom model. You can achieve this by using the ModelMixin class. We could redefine the User class definition as such:

import orb

class UserSchema(orb.ModelMixin):
  id = orb.IdColumn()
  username = orb.StringColumn(flags={'Required'})
  password = orb.StringColumn(flags={'Required'})
  first_name = orb.StringColumn()
  last_name = orb.StringColumn()

  addresses = orb.ReverseLookup(from_column='Address.user')
  preferences = orb.ReverseLookup(from_column='Preference.user', flags={'Unique'})
  groups = orb.Pipe(through_path='GroupUser.user.group')

  byUsername = orb.Index(columns=['username'], flags={'Unique'})
  byName = orb.Index(columns=['first_name', 'last_name'])

class User(UserSchema, orb.Table):
  def username(self):
      print 'getting username'
      return self.get('username', useMethod=False)

In this example, we can just override the username method since there will not be a conflict with the column definition for the schema. Instead, we just redefine what the username method does and it will automatically be connected.

Mixin vs. Inheritance

What are the differences between mixins and inheritance? They both can achieve similar results -- code reuse and base columns -- they go about it in different ways.

When you use a mixin, the logic and schema is applied to other models at generation time, but you cannot share base references to a mixin. This is because each column, while shared in definition, is copied and unique in implementation per model.

Inheritance, on the other hand, will share the base schema. Internally, it will also store the base table as its own data table, merging into inherited tables at query time. Doing this, allows you to share references to base objects.

Modifying the Base Query

By default, models select through every record in the database. However, you can override this behavior by defining the baseQuery for a model. For instance, if you want to only include active users by default, you could define that like:

import orb
from orb import Query as Q

class User(orb.Table):
  id = orb.IdColumn()
  username = orb.StringColumn()
  password = orb.StringColumn()
  active = orb.BooleanColumn(default=True)

  @classmethod
  def baseQuery(cls, **context):
    context = orb.Context(**context)
    if not (context.where or context.where.has('active')):
      return (Q('active') == True)
    else:
      return Q()

In this example, if there is no query already defined, or if the query does not already define options for the active column, then we will default the query to only use active users.