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.