Getting Started

Installation

The main library for ORB is the orb-api package. This library can be installed using the standard Python setuptools and pip.

$ pip install orb-api
$ python
>>> import orb

Note: When you see an example start with >>> in the documentation, this just denotes that the example is for the Python console. When you see an example start with $, it denotes that the example is for a command line or shell.

View the source

This will download and install the core Python code, and you can start building out your APIs with that.

Create your first model

The first thing you will want to do with the API is to create a new model to represent how your data is structured, and what to store. To introduce this to you, let's start off with a file called intro.py and put the following content into it:

import orb

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

This is the way a model looks in ORB. There are two kinds of models, a Table and a View. Most commonly, you will be working with Table's, as these kinds of models support reading and writing to a database or data-store.

For this example, we have just defined a User class. This will create a standard Python class which will register itself to the global orb.system, defining it's schema information for syncing to various backend databases.

We've defined 3 columns for our user -- the IdColumn (every model is required to have an id column associated with it) called id, a StringColumn called username, and a PasswordColumn called password. We'll go into more detail about the differences between these column types later on.

Create your first database

Once you have created a model, you will want to be able to store and retrieve instances of that model. These are called records. A record in the database is just a single instance of a model class. Before we can do that tho, we will need somewhere to store it to.

To do that, we need to define a new database. For a list of supported databases, take a look at the connection types page.

Let's modify our code above to have a new database:

import orb

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

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

That's it! We've just created a simple database, using the standard SQLite Python adapter. This will create a file called example.db in the directory where you just executed this code.

Note: SQLite is a lite implementation of SQL. As such, it will not have full feature functionality. We will document what adapters more advanced features are supported in as we get to them and continue to improve the framework.

ORB supports model access to multiple databases and references between them. To do this, there are a number of ways that you can associate a database to a model. The most common of which, when you have a single database/datastore to access, is to just activate the database. Then it will be globally available for all of your models.

Syncing your Schema to your Database

You have models, you have a database. But right now, the database does not know what models have been defined. Whenever you make a change to your schema (at least for the SQL based backends) you will need to do a database sync to ensure the new columns and tables exist and can be used.

ORB takes a light-weight approach to this:

>>> from intro import *
>>> db.sync()

Done! We will go into more detail about this later. But you can now work with your database.

Create your first record

What is a record? We've already created a User class, which is a model, a record is an individual instance of a model. When you instantiate the class, you will be able to set the properties you defined for your model's schema, save it to the database, and then restore it later.

>>> from intro import *
>>> user_a = User({'username': 'jack', 'password': 'my_password'})
>>> user_b = User({'username': 'jill', 'password': 'my_password2'})
>>> print user_a.get('username')
'jack'
>>> print user_b.get('username')
'jill'

In this example, we have instantiated 2 User records. You can provide a model's constructor a dictionary for the column name and its corresponding value, and this will setup the values for your new record.

>>> user_a.set('username', 'john')
>>> user_b.set('username', 'jane')
>>> print user_a.get('username')
'john'
>>> print user_b.get('username')
'jane'

Saving to the database

So, we have now created 2 records, but at this point, they only exist in memory -- we have not actually stored them to our backend data store. The way ORB works, is you can create or retreive records, modify them, and when you are ready, save them back to the database. Only changes will be stored, and if no changes were made, then no database calls are made!

>>> print user_a.id()
None
>>> user_a.save()
>>> print user_a.id()
1
>>> user_b.save()

The save function will commit your changes to the backend. You can see in the example above, before the save, the user_a instance had no ID, and afterwards, it now has one. When the record saves for the first time, it will create a new instance and store it, while subsequent saves will update the record instead.

Retrieving from the database

The most straight forward way to retrieve a record from the database is to provide ORB the ID to the record. This will perform a direct fetch from the database and initialize your instance with the stored data:

>>> user_c = User(1)
>>> print user_c.get('username')
'john'

In this example, we have fetched the user whose id is 1 from the database, which returns the data that we stored from user_a.

Note: There are 3 ways to initialize a model, and at this point we've seen 2 -- you can initialize with a dictionary of column/values, you can initialize with an ID, and you can initialize with nothing. If you initialize with nothing, like User(), it will create a new in-memory instance with no pre-defined values for columns.

Retrieval failure

If you attempt to retrieve a record from the database and it does not exist, a RecordNotFound error will be raised:

>>> user_d = User(20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/ehulser/workspace/orb-framework/orb/orb/core/model.py", line 226, in __init__
    raise errors.RecordNotFound(self, record_id)
orb.errors.RecordNotFound: Could not find record User(20).

This is something to be aware of, so if you are unsure if a record exists or not, then you should wrap this retrieval in a try/catch block.

Creating an Index

Often times, being able to retrieve models by data other than the ID is useful. If I wanted to lookup a user based on their username or email for instance, there is an easy construct for doing this in ORB.

Indexes provide an easy way to define common ways to lookup data from a database. They also can provide information to the backend to optimize how that data is stored because we now know that it is important to use for retrieval.

If we modify our intro.py file to now read:

import orb

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

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

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

We have added the byUsername index to the User model. What this will do is generate a class-level method that you can use to lookup a model by simply passing in the username.

Using an index is very simple:

>>> from intro import *
>>> user_a = User.byUsername('john')
>>> print user_a.id()
1

Multiple Column Indexes

Having more than 1 column in an index is also possible. If, for instance, we want to track a user's first and last names and look them up by that -- we can do that too!

If we modify our intro.py file to now 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()

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

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

As you can see, we've now added two new columns, first_name and last_name, and a new index byName. For this index, we have specified that it is not unique (since you can have many John Doe's). Instead of getting a record back from this new index, we will now get back a collection.

>>> from intro import *
>>> # don't forget to sync now that we have new columns!
>>> db.sync()

>>> # first, let's update our 'john' user to have a first and last name
>>> u = User.byUsername('john')
>>> u.set('first_name', 'John')
>>> u.set('last_name', 'Doe')

>>> # saving the record now will update the existing user record
>>> u.save()

>>> # we can now retrieve the record by their name
>>> users = User.byName('John', 'Doe')
>>> print users
<orb.core.collection.Collection object at 0x103470b50>
>>> u2 = users.first()
>>> print u2.get('first_name'), u2.get('last_name')
'John Doe'

Creating Relationships

One-to-Many

One of the most important things that you need to be able to do with an ORM is create relationships between your models. The way this is done in ORB is by creating a reference column on one of your models.

Let's open up the intro.py file again. This time, we're going to add a new model called Address, and relate it back to the user.

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()

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

class Address(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User')
  name = orb.StringColumn()
  street = orb.StringColumn()
  city = orb.StringColumn()
  state = orb.StringColumn()
  zipcode = orb.IntegerColumn()

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

The new Address model should be a little familiar to you by now. The new piece of the puzzle is the ReferenceColumn that we added to it. What this does is creates a relationship between the Address and User models, where an Address record will store the User reference information in the user column.

For instance:

>>> from intro import *
>>> # don't forget to sync, we added more models!
>>> db.sync()

>>> u = User.byUsername('john')
>>> address = Address({'name': 'Home'})
>>> address.set('user', u)
>>> address.set('street', '555 Main St.')
>>> address.set('city', 'Anywhere')
>>> address.set('state', 'CA')
>>> address.set('zipcode', 55555)
>>> address.save()
>>> print a.id()
1

We have now created a new Address record, and related it to the user 'john'. If I retrieve this address now, I can see what user it is related to:

>>> from intro import *
>>> a = Address(1)
>>> u = a.get('user')
>>> print u.get('username')
'john'

By default, this reference is a one-to-many relationship, in that one user can have multiple addresses.

One-to-One

One-to-one relationships are just as easy to create. All you need to do is to add a 'Unique' flag to the definition of the ReferenceColumn. This will tell the system that only one reference of each unique record is allowed. For instance:

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()

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

class Preference(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User', flags={'Unique'})
  notifications_enabled = orb.BooleanColumn()

class Address(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User')
  name = orb.StringColumn()
  street = orb.StringColumn()
  city = orb.StringColumn()
  state = orb.StringColumn()
  zipcode = orb.IntegerColumn()

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

This will now enforce that a user record has only one Preference that it can be related to.

>>> from intro import *
>>> db.sync()

>>> u = User.byUsername('john')
>>> p = Preference()
>>> p.set('user', u)
>>> p.set('notifications_enabled', True)
>>> p.save()

Collectors

So we have preferences for a User, and they can have multiple addresses. But right now, we still have to know the ids of those records to retrieve them. The easiest way to interrelate these models more fully is to assign some collectors to them. A collector is an object that will define how to select a collection of records from the database, given a set of criteria.

Reverse Lookups

Reverse lookups will collect records from the database by looking at a reference column in reverse. What I mean by this is instead of asking for an Address and then getting it's User, I can ask for a User and then have the collector find it's related Address records.

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'})

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

class Preference(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User', flags={'Unique'})
  notifications_enabled = orb.BooleanColumn()

class Address(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User')
  name = orb.StringColumn()
  street = orb.StringColumn()
  city = orb.StringColumn()
  state = orb.StringColumn()
  zipcode = orb.IntegerColumn()

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

The newly added line addresses = orb.ReverseLookup(from_column='Address.user') creates an addresses collector on the User model which will lookup addresses based on their user column. The syntax for the from_column is {model}.{column}.

Because the Address.user column is not unique, and the relationship is one-to-many, what is returned from calling addresses is a collection. Because the Preference.user is unique, however, an individual record is returned.

>>> from intro import *
>>> u = User.byUsername('john')
>>> print u.addresses()
<orb.core.collection.Collection object at 0x103450f90>
>>> print u.preferences()
<intro.Preference object at 0x103464950>

Pipes (Many-to-Many)

Another type of collector is the ORB solution for the many-to-many relationship called a Pipe. We have taken the explicit approach for many-to-many relationships, requiring you to define an intermediary model which you will "pipe" through for your relationships.

If we turn to the intro.py file again, we will introduce the concept of "user groups". For this, one user may be a part of many groups, and one group may have many users. This use case is what defines the many to many relationship.

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'])

class Preference(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User', flags={'Unique'})
  notifications_enabled = orb.BooleanColumn()

class Address(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User')
  name = orb.StringColumn()
  street = orb.StringColumn()
  city = orb.StringColumn()
  state = orb.StringColumn()
  zipcode = orb.IntegerColumn()

class Group(orb.Table):
  id = orb.IdColumn()
  name = orb.StringColumn()

  users = orb.Pipe(through_path='GroupUser.group.user')

  byName = orb.Index(columns=['name'], flags={'Unique'})

class GroupUser(orb.Table):
  id = orb.IdColumn()
  user = orb.ReferenceColumn(reference='User')
  group = orb.ReferenceColumn(reference='Group')

# create a new sqlite db
db = orb.Database('SQLite', 'intro.db')
db.activate()

You will see from this example that we have created 2 new models -- Group and GroupUser. The GroupUser table will act as the intermediary between Group.users and User.groups pipes.

The line users = orb.Pipe(through_path='GroupUser.group.user') defines the pipe for the Group table, with the syntax for the through_path keyword being {through_table}.{from_column}.{to_column}. Both the from and to columns should be ReferenceColumn types.

Here is an example of the usage:

>>> from intro import *
>>> db.sync()

>>> # retrieve a user
>>> john = User.byUsername('john')
>>> jane = User.byUsername('jane')

>>> # create some groups
>>> admins = Group({'name': 'admin'})
>>> admins.save()
>>> editors = Group({'name': 'editors'})
>>> editors.save()

>>> # associate users to groups
>>> admins.users().add(john)
>>> editors.users().add(john)
>>> # or, you can associate in reverse order as well
>>> jane.groups().add(editors)

>>> # validate the relationships
>>> print len(editors.users())
2
>>> print len(admins.users())
1
>>> print admins in john.groups()
True
>>> print admins in jane.groups()
False

You'll notice that for these examples, we first created a Group record, and then associated it to the pipe whereas for the Address example we created the address record setting the reference column to a User. The add method on a collection is a convenience method for creating the intermediary record. You could have also done it this way:

>>> gu = GroupUser({'user': john, 'group': editors})
>>> gu.save()

Removing Records

We've discussed most of the core functionality of ORB and general ORM's -- create, read, update, relationships. The last topic we'll discuss in this general overview is how to delete records from the database.

Using our last example, we now have 2 users (john and jane), two groups (admins and editors) and an address and a preference.

For discussing deleting records, we'll cover it in 2 sections -- removing an individual record and unlinking a record from a many-to-many relationship.

>>> from intro import *
>>> john = User.byUsername('john')
>>> editors = Group.byName('editors')

>>> # deleting a record directly
>>> address = Address(1)
>>> print len(john.addresses())
>>> print address.id()
1
>>> address.delete()
>>> print address.id()
None
>>> print len(john.addresses())
0

>>> # unlinking an intermediary table
>>> print editors.id()
2
>>> print len(GroupUser.all())
3
>>> john.groups().remove(editors)
>>> print len(GroupUser.all())
2
>>> print editors.id()
2

In the first example, we directly accessed the Address model for id 1 and deleted it. This removed the record from the data store, and consequently john no longer had that record in his collection. After the record was deleted, it's ID was cleared since it no longer exists in the data store -- although you do still have access to the in-memory record instance.

In the second example, we're actually deleting the intermediary record between our user and group models...not the user or group themselves. To do this, you could have selected the particular GroupUser you wanted to delete and delete it directly, but there is the convenience method remove for a collection that will do that for you. Calling john.groups().remove(editors) didn't delete john or editors, it instead deleted the GroupUser record whose user was john and whose group was editors.

What's up next

So that covers all of the basics of the ORB framework...but there is so much more to the system. To cover it all in a single page is too much, so we have broken down the in-depth instruction on the API Reference page, and have more real-world use case walkthroughs in the Cookbook.