Project

General

Profile

Model » History » Version 7

Elmer de Looff, 2011-09-21 15:44

1 1 Elmer de Looff
h1. Database abstraction model
2 1 Elmer de Looff
3 1 Elmer de Looff
h2. Goal of this component
4 1 Elmer de Looff
5 1 Elmer de Looff
The µWeb framework provides a @model@ module with the intention of simplifying database access. The design goal is to provide a rich abstraction that
6 1 Elmer de Looff
* takes away the tedious work of retrieving, creating and deleting records
7 1 Elmer de Looff
* can load its parent objects automatically if so required
8 1 Elmer de Looff
* _does *not* get in the way of the developer_
9 1 Elmer de Looff
10 1 Elmer de Looff
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.
11 1 Elmer de Looff
12 2 Elmer de Looff
h2. Using the Record
13 1 Elmer de Looff
14 2 Elmer de Looff
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.
15 1 Elmer de Looff
16 2 Elmer de Looff
h3. Basic Record usage
17 1 Elmer de Looff
18 2 Elmer de Looff
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.
19 1 Elmer de Looff
20 2 Elmer de Looff
h3. Creating your own @Record@
21 1 Elmer de Looff
22 2 Elmer de Looff
To create your own @Record@ subclass, nothing is required beyond the class name. The following example substitutes a complete working example:
23 2 Elmer de Looff
<pre><code class="python">
24 2 Elmer de Looff
from underdark.uweb import model
25 2 Elmer de Looff
class Message(model.Record):
26 2 Elmer de Looff
  """Abstraction class for messages stored in the database."""
27 2 Elmer de Looff
</code></pre>
28 1 Elmer de Looff
29 2 Elmer de Looff
h3. Primary field definition
30 1 Elmer de Looff
31 2 Elmer de Looff
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.
32 1 Elmer de Looff
33 2 Elmer de Looff
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:
34 1 Elmer de Looff
35 2 Elmer de Looff
<pre><code class="python">
36 2 Elmer de Looff
from underdark.uweb import model
37 2 Elmer de Looff
class Country(model.Record):
38 2 Elmer de Looff
  """Abstraction class for a country table.
39 1 Elmer de Looff
40 2 Elmer de Looff
  This class uses the ISO-3166-1 alpha2 country code as primary key.
41 2 Elmer de Looff
  """
42 2 Elmer de Looff
  _PRIMARY_KEY = 'alpha2'
43 2 Elmer de Looff
</code></pre>
44 1 Elmer de Looff
45 2 Elmer de Looff
h3. Class and table relation
46 2 Elmer de Looff
47 2 Elmer de Looff
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:
48 2 Elmer de Looff
49 2 Elmer de Looff
<pre><code class="python">
50 2 Elmer de Looff
from underdark.uweb import model
51 2 Elmer de Looff
class Message(model.Record):
52 2 Elmer de Looff
  """Abstraction class for messages stored in the database."""
53 2 Elmer de Looff
  _TABLE = 'MyMessage'
54 2 Elmer de Looff
</code></pre>
55 2 Elmer de Looff
56 6 Elmer de Looff
h3. Record initialization
57 1 Elmer de Looff
58 6 Elmer de Looff
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.
59 1 Elmer de Looff
60 6 Elmer de Looff
<pre><code class="python">
61 6 Elmer de Looff
from underdark.uweb import model
62 6 Elmer de Looff
class Message(model.Record):
63 6 Elmer de Looff
  """Abstraction class for messages stored in the database."""
64 1 Elmer de Looff
65 6 Elmer de Looff
# Caller side:
66 6 Elmer de Looff
>>> record = {'ID': 1, 'message': 'First message!', 'author': 'Elmer'}
67 7 Elmer de Looff
>>> message = Message(db_conn, record)
68 6 Elmer de Looff
>>> print message
69 6 Elmer de Looff
Message({'message': 'First message!', 'ID': 1, 'author': 'Elmer'})
70 6 Elmer de Looff
</code></pre>
71 1 Elmer de Looff
72 6 Elmer de Looff
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:
73 6 Elmer de Looff
74 6 Elmer de Looff
h3. Alternative initializer: create Record from primary key
75 6 Elmer de Looff
76 6 Elmer de Looff
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.
77 6 Elmer de Looff
78 6 Elmer de Looff
There is one such alternative initializer provided: @FromKey@, 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:
79 6 Elmer de Looff
80 6 Elmer de Looff
<pre><code class="python">
81 6 Elmer de Looff
from underdark.uweb import model
82 6 Elmer de Looff
class Message(model.Record):
83 6 Elmer de Looff
  """Abstraction class for messages stored in the database."""
84 6 Elmer de Looff
85 6 Elmer de Looff
# Caller side:
86 7 Elmer de Looff
>>> message = Message.FromKey(db_conn, 1)
87 6 Elmer de Looff
>>> print message
88 6 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L, 'author': 'Elmer'})
89 1 Elmer de Looff
# Unicode and long integer are side effects from the database read, not the Record class
90 1 Elmer de Looff
</code></pre>
91 7 Elmer de Looff
92 7 Elmer de Looff
h3. On-demand loading of referenced records.
93 7 Elmer de Looff
94 7 Elmer de Looff
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.
95 7 Elmer de Looff
96 7 Elmer de Looff
Consider the following tables in your database:
97 7 Elmer de Looff
<pre><code class="html">
98 7 Elmer de Looff
-- TABLE `message`
99 7 Elmer de Looff
+----+--------+--------------------------------------------------+
100 7 Elmer de Looff
| ID | author | message                                          |
101 7 Elmer de Looff
+----+--------+--------------------------------------------------+
102 7 Elmer de Looff
|  1 |      1 | First message!                                   |
103 7 Elmer de Looff
|  2 |      2 | Robert'); DROP TABLE Students;--                 |
104 7 Elmer de Looff
|  3 |      1 | You didn't think it would be this easy, did you? |
105 7 Elmer de Looff
+----+--------+--------------------------------------------------+
106 7 Elmer de Looff
107 7 Elmer de Looff
-- TABLE `author`
108 7 Elmer de Looff
+----+-------+--------------------+
109 7 Elmer de Looff
| ID | name  | emailAddress       |
110 7 Elmer de Looff
+----+-------+--------------------+
111 7 Elmer de Looff
|  1 | Elmer | elmer@underdark.nl |
112 7 Elmer de Looff
|  2 | Bobby | bobby@tables.com   |
113 7 Elmer de Looff
+----+-------+--------------------+
114 7 Elmer de Looff
</code></pre>
115 7 Elmer de Looff
116 7 Elmer de Looff
And the following class definitions in Python:
117 7 Elmer de Looff
118 7 Elmer de Looff
<pre><code class="python">
119 7 Elmer de Looff
from underdark.uweb import model
120 7 Elmer de Looff
class Author(model.Record):
121 7 Elmer de Looff
  """Abstraction class for author records."""
122 7 Elmer de Looff
123 7 Elmer de Looff
class Message(model.Record):
124 7 Elmer de Looff
  """Abstraction class for messages records."""
125 7 Elmer de Looff
</code></pre>
126 7 Elmer de Looff
127 7 Elmer de Looff
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.
128 7 Elmer de Looff
129 7 Elmer de Looff
<pre><code class="python">
130 7 Elmer de Looff
>>> message = Message.FromKey(db_connection, 1)
131 7 Elmer de Looff
>>> message
132 7 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L, 'author': 1})
133 7 Elmer de Looff
# This is the same message we saw before, without author information.
134 7 Elmer de Looff
# However, retrieving the author field specifically, provides its record:
135 7 Elmer de Looff
>>> message['author']
136 7 Elmer de Looff
Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})
137 7 Elmer de Looff
>>> message
138 7 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L,
139 7 Elmer de Looff
         'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
140 7 Elmer de Looff
</code></pre>
141 7 Elmer de Looff
142 7 Elmer de Looff
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 FromKey alternative initializer, with _1_ as the primary key value.
143 7 Elmer de Looff
144 7 Elmer de Looff
# @message['author']@ uses the _author_ field
145 7 Elmer de Looff
# _author_ table is abstracted by Author class
146 7 Elmer de Looff
# @message['author']@ is replaced by @Author.FromKey(db_connection, message['author']@
147 7 Elmer de Looff
148 7 Elmer de Looff
149 7 Elmer de Looff
Where available, the model will automatically 
150 7 Elmer de Looff
151 7 Elmer de Looff
h3. Loading child objects (1-to-n relations)
152 2 Elmer de Looff
153 2 Elmer de Looff
*N.B.* In the default implementation, fields that refer to a record in another table (@n to 1@ or @1 to 1@ relationships) *MUST have the name of that table.*
154 2 Elmer de Looff
For example: Given two tables `child` and `parent`. Entries in `child` that refer to their parent, must do so using a field called `parent` (not parentID or some such). If the table names are plural, the fields that refer to the relation should also have a pluralized name.