Project

General

Profile

Model » History » Version 21

Version 20 (Elmer de Looff, 2012-05-16 14:33) → Version 21/28 (Elmer de Looff, 2012-05-16 14:37)

The µWeb framework provides a @model@ module with the intention of simplifying database access. The design goal is to provide a rich abstraction that
* takes away the tedious work of retrieving, creating and deleting records
* can load its parent objects automatically if so required
* _does *not* get in the way of the developer_

Making database interaction easier without restricting the abilities of the developer is our main goal. Some default mechanisms make assumptions on the way the database is organised, but these are well-documented, and it's entirely possible to change the behavior of these mechanisms.

{{toc}}

h1. Record

The basic idea of the @Record@ class is that it is a container for your database records, with related records automatically loaded as needed, and custom methods that provide more info, child objects, etc. Outlined below are the default features available, with minimal configuration requirements.

h2. Basic Record usage

There are a few ways to use the @Record@ class. The direct way to create a @Record@ is to initiate it with a connection, and a dictionary of @field -> value@ information. The @Record@ is a dictionary subclass that largely copies all the functionality of a dictionary. Retrieving values for keys works exactly as you'd expect.



h2. Creating your own @Record@ class

To create your own @Record@ subclass, nothing is required beyond the class' class name. The following example substitutes a complete working example:
<pre><code class="python">
from uweb import model
class Message(model.Record):
"""Abstraction class for messages stored in the database."""
</code></pre>



h2. Primary field definition

The @Record@ requires that a table has a single-field unique column. It's advisable for this to be a PRIMARY index in the database, though this is not required. This field is used to automatically look up a record if it is referenced and requested elsewhere.

By default, this primary key field is assumed to be @ID@. If this is not the case for your table, you can easily change this by defining the @_PRIMARY_KEY@ class constant:

<pre><code class="python">
from uweb import model
class Country(model.Record):
"""Abstraction class for a country table.

This class uses the ISO-3166-1 alpha2 country code as primary key.
"""
_PRIMARY_KEY = 'alpha2'
</code></pre>

h2. Class and table relation

By default, the assumption is made that the table name is the same as the class name, with the first letter lowercase. *The table related to the class @Message@ would be @message@.* To change this behavior, assign the correct table name to the @_TABLE@ class constant. This new table name will then be used in all built-in Record methods:

<pre><code class="python">
from uweb import model
class Message(model.Record):
"""Abstraction class for messages stored in the database."""
_TABLE = 'MyMessage'
</code></pre>

h2. Record initialization

Initializing a Record object requires a database connection as first argument, and a dictionary with the record's data as second argument. This second argument can, alternatively, be an iterator of key+value tuples.

<pre><code class="python">
from uweb import model
class Message(model.Record):
"""Abstraction class for messages stored in the database."""

# Caller side:
>>> record = {'ID': 1, 'message': 'First message!', 'author': 'Elmer'}
>>> message = Message(db_conn, record)
>>> print message
Message({'message': 'First message!', 'ID': 1, 'author': 'Elmer'})
</code></pre>

This basic construction is rarely needed in code using the Record objects, but is important for alternative initializers, of which one is provided by default:

h2. Alternative initializer: create Record from primary key

On the caller side, it's impractical to first query the database, and then instantiate a Record subclass from that. Alternative initializers provide a solution without requiring module-level functions that have poor cohesion to the relevant class. Alternative initializers are @classmethods@, working not on instance, but aiming to create and return one.

There is one such alternative initializer provided: @FromPrimary@, which loads a record from the database based on its primary key. Required for this to function are two arguments: A database connection, and the value for the primary key field:

<pre><code class="python">
from uweb import model
class Message(model.Record):
"""Abstraction class for messages stored in the database."""

# Caller side:
>>> message = Message.FromPrimary(db_conn, 1)
>>> print message
Message({'message': u'First message!', 'ID': 1L, 'author': 'Elmer'})
# Unicode and long integer are side effects from the database read, not the Record class
</code></pre>

h2. On-demand loading of referenced records.

In databases that are more complex than a single table, information is often normalized. That is, the author information in our previously demonstrated *message* table will be stored in a separate *author* table. The author field on message records will be a _reference_ to a record in the author table.

Consider the following tables in your database:
<pre><code class="html">
-- TABLE `message`
+----+--------+--------------------------------------------------+
| ID | author | message |
+----+--------+--------------------------------------------------+
| 1 | 1 | First message! |
| 2 | 2 | Robert'); DROP TABLE Students;-- |
| 3 | 1 | You didn't think it would be this easy, did you? |
+----+--------+--------------------------------------------------+

-- TABLE `author`
+----+-------+--------------------+
| ID | name | emailAddress |
+----+-------+--------------------+
| 1 | Elmer | elmer@underdark.nl |
| 2 | Bobby | bobby@tables.com |
+----+-------+--------------------+
</code></pre>

And the following class definitions in Python:

<pre><code class="python">
from uweb import model
class Author(model.Record):
"""Abstraction class for author records."""

class Message(model.Record):
"""Abstraction class for messages records."""
</code></pre>

This makes it possible to retrieve a message, and from that Message object, retrieve the author information. This is done when the information is requested, and not pre-loaded beforehand. This means that retrieving a thousand Message objects will *not* trigger an additional 1000 queries to retrieve the author information, if that information might not be used at all.

<pre><code class="python">
>>> message = Message.FromPrimary(db_connection, 1)
>>> message
Message({'message': u'First message!', 'ID': 1L, 'author': 1})
# This is the same message we saw before, without author information.
# However, retrieving the author field specifically, provides its record:
>>> message['author']
Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})
>>> message
Message({'message': u'First message!', 'ID': 1L,
'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
</code></pre>

This works on the assumption that *any field name, that is also the table name of another Record class, is a reference to that record*. In the case of the example above: The message table contains a field _author_. There exists a Record subclass for that table (namely _Author_, table 'author'). The value of @message['author']@ (1), is now used to load an Author record using the FromPrimary alternative initializer, with _1_ as the primary key value.

# @message['author']@ uses the _author_ field
# _author_ table is abstracted by Author class
# @message['author']@ is replaced by @Author.FromPrimary(db_connection, message['author']@

This behavior can be modified using the _FOREIGN_RELATIONS class constant. This provides a mapping that specifies (and overrides) which Record classes should be used to resolve references from fields. The key for the mapping is a field name (string), and the corresponding value is a class or None. None specifies that the field does *not* represent a reference, and should be used as-is. Classes may be given as string because at the time of evaluation, not all classes exist, and attempting using a class directly might result in a NameError. Without this provision, the order of classes would be dictated by the model, and cross-references would not be possible at all.

An example case for a situation where the table names are plural, but the field names are singular:

<pre><code class="python">
from uweb import model
class Author(model.Record):
"""Abstraction class for author records."""
_TABLE = 'authors'

class Message(model.Record):
"""Abstraction class for messages records."""
_TABLE = 'messages'
_FOREIGN_RELATIONS = {'author': Author}
</code></pre>

h2. Loading child objects (1-to-n relations)

The model provides a generic method to retrieve child records (that is, _1 to n_ relations) of a Record. The desired relations _should_ have an associated Record class. The method to use is @_Children@, which is a private method of any Record class. As its argument, it needs the name of a child class. Returned is an iterator that yields instances of the given Record subclass.

Given its name and usage, the suggested usage of this is to wrap a more descriptive method around this:

<pre><code class="python">
from uweb import model
class Author(model.Record):
"""Abstraction class for author records."""
def Messages(self):
"""Returns an iterator for all messages written by this author."""
return self._Children(Message)

class Message(model.Record):
"""Abstraction class for messages records."""

# Caller code
>>> elmer = Author.FromPrimary(db_connection, 1)
>>> for message in elmer.Messages():
... print message
Message({'message': u'First message!', 'ID': 1L,
'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L,
'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
# Reflowing to keep things legible
</code></pre>

What you can see here is that all messages written by the given author are retrieved from the database, and presented. This is done with a single database query, where the _child_ Record's table is searched for rows where the @relation_field@ is equal to the parent Record's primary key value. This @relation_field@ is an optional argument to the @_Children@ method, and defaults to the class' table name.

*N.B. @print@ and the methods @(iter)items@, @(iter)values@ all cause the object's foreign relations to be retrieved.*

An example with pluralized table names:

<pre><code class="python">
class Author(model.Record):
"""Abstraction class for author records."""
_TABLE = 'authors'

def Messages(self):
"""Returns an iterator for all messages written by this author."""
return self._Children(Message, relation_field='author')

class Message(model.Record):
"""Abstraction class for messages records."""
_TABLE = 'messages'
_FOREIGN_RELATIONS = {'author': Author}
</code></pre>

h2. Retrieving all records

For situations where all records must be retrieved or processed, there is the @List@ classmethod, that takes a single connection argument. This returns an iterator for all Record objects of the type it's called for:

<pre><code class="python">
class Message(model.Record):
"""Abstraction class for messages records."""

# List all messages:
>>> for message in Message.List(db_connection):
... print message
...
Message({'message': u'First message!', 'ID': 1L, 'author': 1})
Message({'message': u"Robert'); DROP TABLE Students;--", 'ID': 2L, 'author': 2})
Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L, 'author': 1})
</code></pre>

*N.B.*: If the Author class were defined here, it would be automatically loaded where the primary key for the author is now listed. This has been omitted in this example for reasons of brevity and readability.

h2. Updating a record

After loading a record, it can be altered, and saved. These changes (and optionally changes to nested records), will be committed to the database, and reflected in the current loaded record.

<pre><code class="python">
class Author(model.Record):
"""Abstraction class for author records."""

class Message(model.Record):
"""Abstraction class for messages records."""

>>> retort = Message.FromPrimary(db_connection, 3)
>>> retort['message'] = "Please go away Bobby."
>>> # Our changes are not yet reflected in the database:
>>> print Message.FromPrimary(db_connection, 3)
Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L,
'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
>>> retort.Save()
>>> # Now our changes are committed to the database:
>>> print Message.FromPrimary(db_connection, 3)
Message({'message': u'Please go away Bobby.', 'ID': 3L,
'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
</code></pre>

If we specify *save_foreign* as _True_, we can also alter the information stored in foreign relations, and have that saved in the same operation. This way we could alter both the author name, or email address, as well as the message itself.

h2. Adding a record

Using the same @Save@ method, we can also add records to the database. This can be done either with the Primary Key given, or left undefined. If the key is left undefined (or defined as None), the Record will assume that the primary key field is an auto-increment field, and insert data in that manner.

*N.B.* Skipping fields that are optional in the database is allowed, but their default values assigned by the database will _not_ be reflected in the object. That is, the record will not be reloaded after storing.

Creating a record using an auto-incrementing primary key:
<pre><code class="python">
class Message(model.Record):
"""Abstraction class for messages records."""

>>> new_message = Message(db_connection, {'author': 1, 'message': 'A new message, should be #4'})
>>> new_message.Save()
>>> new_message.key
4L
>>> print Message.FromPrimary(db_connection, new_message.key)
Message({'message': u'A new message, should be #4', 'ID': 4L, 'author': 1})
</code></pre>

Creating a record where we specify the key:
<pre><code class="python">
class Message(model.Record):
"""Abstraction class for messages records."""

>>> another_message = Message(db_connection, {})
>>> another_message.key = 6 # we could assign to the 'ID' key as well
>>> another_message['author'] = 2
>>> another_message['message'] = 'Creating a message with a defined primary key value'
>>> another_message.Save()
4L
>>> print Message.FromPrimary(db_connection, another_message.key)
Message({'message': u'Creating a message with a defined primary key value', 'ID': 6L, 'author': 2})
</code></pre>

h2. Deleting a record

Records can be deleted from the database either from a loaded object, or using the DeleteKey classmethod. This latter removes the record from the database using the primary key to select it.

<pre><code class="python">
class Message(model.Record):
"""Abstraction class for messages records."""

# Loading and deleting an active record.
>>> bad_record = Message.FromPrimary(db_connection, 3)
>>> bad_record.Delete()

# Deleting a record based on its primary key.
>>> Message.DeleteKey(db_connection, 2)
</code></pre>

h2. Comparisons

h3. Equality

Records must pass the following criteria to be considered equal to one another.:
# *Type*: Two objects must be of the same type (class)
# *Primary key*: The primary key values must compare equal
# *Foreign relations*: Foreign relations must be the same. If these are not resolved in one object but are in the other, the primary key of the resolved object will be compared to the data of the other record.
# *Data*: All remaining data fields must be equal and symmetric (i.e. both objects describe the same fields)

h3. Greater / smaller

Comparing two objects with one another to tell their relative order can _only_ be done if they are of the same type. If they are, the comparison is done based on the primary key values of the records. In most cases this will result in an ordering similar to the database-insert order.

h1. VersionedRecord

h1. MongoRecord