Project

General

Profile

Model » History » Version 21

Elmer de Looff, 2012-05-16 14:37

1 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
2 1 Elmer de Looff
* takes away the tedious work of retrieving, creating and deleting records
3 1 Elmer de Looff
* can load its parent objects automatically if so required
4 1 Elmer de Looff
* _does *not* get in the way of the developer_
5 1 Elmer de Looff
6 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.
7 1 Elmer de Looff
8 15 Elmer de Looff
{{toc}}
9 15 Elmer de Looff
10 15 Elmer de Looff
h1. Record
11 1 Elmer de Looff
12 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.
13 1 Elmer de Looff
14 12 Elmer de Looff
h2. Basic Record usage
15 1 Elmer de Looff
16 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.
17 1 Elmer de Looff
18 21 Elmer de Looff
h2. Creating your own @Record@ class
19 1 Elmer de Looff
20 21 Elmer de Looff
To create your own @Record@ subclass, nothing is required beyond the class' name. The following example substitutes a complete working example:
21 1 Elmer de Looff
<pre><code class="python">
22 13 Jan Klopper
from uweb import model
23 2 Elmer de Looff
class Message(model.Record):
24 2 Elmer de Looff
  """Abstraction class for messages stored in the database."""
25 2 Elmer de Looff
</code></pre>
26 1 Elmer de Looff
27 12 Elmer de Looff
h2. Primary field definition
28 1 Elmer de Looff
29 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.
30 1 Elmer de Looff
31 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:
32 1 Elmer de Looff
33 2 Elmer de Looff
<pre><code class="python">
34 13 Jan Klopper
from uweb import model
35 2 Elmer de Looff
class Country(model.Record):
36 2 Elmer de Looff
  """Abstraction class for a country table.
37 1 Elmer de Looff
38 2 Elmer de Looff
  This class uses the ISO-3166-1 alpha2 country code as primary key.
39 2 Elmer de Looff
  """
40 2 Elmer de Looff
  _PRIMARY_KEY = 'alpha2'
41 2 Elmer de Looff
</code></pre>
42 1 Elmer de Looff
43 12 Elmer de Looff
h2. Class and table relation
44 2 Elmer de Looff
45 1 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:
46 2 Elmer de Looff
47 2 Elmer de Looff
<pre><code class="python">
48 13 Jan Klopper
from uweb import model
49 2 Elmer de Looff
class Message(model.Record):
50 2 Elmer de Looff
  """Abstraction class for messages stored in the database."""
51 2 Elmer de Looff
  _TABLE = 'MyMessage'
52 2 Elmer de Looff
</code></pre>
53 2 Elmer de Looff
54 12 Elmer de Looff
h2. Record initialization
55 1 Elmer de Looff
56 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.
57 1 Elmer de Looff
58 6 Elmer de Looff
<pre><code class="python">
59 13 Jan Klopper
from uweb import model
60 6 Elmer de Looff
class Message(model.Record):
61 6 Elmer de Looff
  """Abstraction class for messages stored in the database."""
62 1 Elmer de Looff
63 6 Elmer de Looff
# Caller side:
64 6 Elmer de Looff
>>> record = {'ID': 1, 'message': 'First message!', 'author': 'Elmer'}
65 7 Elmer de Looff
>>> message = Message(db_conn, record)
66 6 Elmer de Looff
>>> print message
67 6 Elmer de Looff
Message({'message': 'First message!', 'ID': 1, 'author': 'Elmer'})
68 6 Elmer de Looff
</code></pre>
69 1 Elmer de Looff
70 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:
71 6 Elmer de Looff
72 12 Elmer de Looff
h2. Alternative initializer: create Record from primary key
73 6 Elmer de Looff
74 1 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.
75 6 Elmer de Looff
76 16 Elmer de Looff
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:
77 6 Elmer de Looff
78 6 Elmer de Looff
<pre><code class="python">
79 13 Jan Klopper
from uweb import model
80 6 Elmer de Looff
class Message(model.Record):
81 6 Elmer de Looff
  """Abstraction class for messages stored in the database."""
82 6 Elmer de Looff
83 6 Elmer de Looff
# Caller side:
84 16 Elmer de Looff
>>> message = Message.FromPrimary(db_conn, 1)
85 6 Elmer de Looff
>>> print message
86 6 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L, 'author': 'Elmer'})
87 1 Elmer de Looff
# Unicode and long integer are side effects from the database read, not the Record class
88 1 Elmer de Looff
</code></pre>
89 7 Elmer de Looff
90 12 Elmer de Looff
h2. On-demand loading of referenced records.
91 7 Elmer de Looff
92 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.
93 7 Elmer de Looff
94 7 Elmer de Looff
Consider the following tables in your database:
95 7 Elmer de Looff
<pre><code class="html">
96 7 Elmer de Looff
-- TABLE `message`
97 7 Elmer de Looff
+----+--------+--------------------------------------------------+
98 7 Elmer de Looff
| ID | author | message                                          |
99 7 Elmer de Looff
+----+--------+--------------------------------------------------+
100 7 Elmer de Looff
|  1 |      1 | First message!                                   |
101 7 Elmer de Looff
|  2 |      2 | Robert'); DROP TABLE Students;--                 |
102 7 Elmer de Looff
|  3 |      1 | You didn't think it would be this easy, did you? |
103 7 Elmer de Looff
+----+--------+--------------------------------------------------+
104 7 Elmer de Looff
105 7 Elmer de Looff
-- TABLE `author`
106 7 Elmer de Looff
+----+-------+--------------------+
107 7 Elmer de Looff
| ID | name  | emailAddress       |
108 7 Elmer de Looff
+----+-------+--------------------+
109 7 Elmer de Looff
|  1 | Elmer | elmer@underdark.nl |
110 7 Elmer de Looff
|  2 | Bobby | bobby@tables.com   |
111 1 Elmer de Looff
+----+-------+--------------------+
112 7 Elmer de Looff
</code></pre>
113 7 Elmer de Looff
114 7 Elmer de Looff
And the following class definitions in Python:
115 7 Elmer de Looff
116 7 Elmer de Looff
<pre><code class="python">
117 13 Jan Klopper
from uweb import model
118 7 Elmer de Looff
class Author(model.Record):
119 7 Elmer de Looff
  """Abstraction class for author records."""
120 7 Elmer de Looff
121 7 Elmer de Looff
class Message(model.Record):
122 7 Elmer de Looff
  """Abstraction class for messages records."""
123 7 Elmer de Looff
</code></pre>
124 7 Elmer de Looff
125 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.
126 7 Elmer de Looff
127 7 Elmer de Looff
<pre><code class="python">
128 16 Elmer de Looff
>>> message = Message.FromPrimary(db_connection, 1)
129 7 Elmer de Looff
>>> message
130 7 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L, 'author': 1})
131 7 Elmer de Looff
# This is the same message we saw before, without author information.
132 7 Elmer de Looff
# However, retrieving the author field specifically, provides its record:
133 7 Elmer de Looff
>>> message['author']
134 7 Elmer de Looff
Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})
135 7 Elmer de Looff
>>> message
136 7 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L,
137 7 Elmer de Looff
         'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
138 7 Elmer de Looff
</code></pre>
139 7 Elmer de Looff
140 16 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 FromPrimary alternative initializer, with _1_ as the primary key value.
141 7 Elmer de Looff
142 7 Elmer de Looff
# @message['author']@ uses the _author_ field
143 7 Elmer de Looff
# _author_ table is abstracted by Author class
144 16 Elmer de Looff
# @message['author']@ is replaced by @Author.FromPrimary(db_connection, message['author']@
145 7 Elmer de Looff
146 8 Elmer de Looff
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.
147 1 Elmer de Looff
148 8 Elmer de Looff
An example case for a situation where the table names are plural, but the field names are singular:
149 8 Elmer de Looff
150 8 Elmer de Looff
<pre><code class="python">
151 13 Jan Klopper
from uweb import model
152 8 Elmer de Looff
class Author(model.Record):
153 8 Elmer de Looff
  """Abstraction class for author records."""
154 8 Elmer de Looff
  _TABLE = 'authors'
155 8 Elmer de Looff
156 1 Elmer de Looff
class Message(model.Record):
157 1 Elmer de Looff
  """Abstraction class for messages records."""
158 1 Elmer de Looff
  _TABLE = 'messages'
159 1 Elmer de Looff
  _FOREIGN_RELATIONS = {'author': Author}
160 1 Elmer de Looff
</code></pre>
161 1 Elmer de Looff
162 12 Elmer de Looff
h2. Loading child objects (1-to-n relations)
163 1 Elmer de Looff
164 17 Elmer de Looff
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. 
165 9 Elmer de Looff
166 9 Elmer de Looff
Given its name and usage, the suggested usage of this is to wrap a more descriptive method around this:
167 9 Elmer de Looff
168 9 Elmer de Looff
<pre><code class="python">
169 13 Jan Klopper
from uweb import model
170 9 Elmer de Looff
class Author(model.Record):
171 9 Elmer de Looff
  """Abstraction class for author records."""
172 9 Elmer de Looff
  def Messages(self):
173 9 Elmer de Looff
    """Returns an iterator for all messages written by this author."""
174 17 Elmer de Looff
    return self._Children(Message)
175 9 Elmer de Looff
176 9 Elmer de Looff
class Message(model.Record):
177 9 Elmer de Looff
  """Abstraction class for messages records."""
178 9 Elmer de Looff
179 9 Elmer de Looff
# Caller code
180 16 Elmer de Looff
>>> elmer = Author.FromPrimary(db_connection, 1)
181 9 Elmer de Looff
>>> for message in elmer.Messages():
182 9 Elmer de Looff
...   print message
183 9 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L,
184 9 Elmer de Looff
         'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
185 9 Elmer de Looff
Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L,
186 9 Elmer de Looff
         'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
187 9 Elmer de Looff
# Reflowing to keep things legible
188 9 Elmer de Looff
</code></pre>
189 9 Elmer de Looff
190 17 Elmer de Looff
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.
191 11 Elmer de Looff
192 9 Elmer de Looff
*N.B. @print@ and the methods @(iter)items@, @(iter)values@ all cause the object's foreign relations to be retrieved.*
193 9 Elmer de Looff
194 9 Elmer de Looff
An example with pluralized table names:
195 9 Elmer de Looff
196 9 Elmer de Looff
<pre><code class="python">
197 9 Elmer de Looff
class Author(model.Record):
198 9 Elmer de Looff
  """Abstraction class for author records."""
199 9 Elmer de Looff
  _TABLE = 'authors'
200 9 Elmer de Looff
201 9 Elmer de Looff
  def Messages(self):
202 9 Elmer de Looff
    """Returns an iterator for all messages written by this author."""
203 17 Elmer de Looff
    return self._Children(Message, relation_field='author')
204 9 Elmer de Looff
205 9 Elmer de Looff
class Message(model.Record):
206 9 Elmer de Looff
  """Abstraction class for messages records."""
207 9 Elmer de Looff
  _TABLE = 'messages'
208 9 Elmer de Looff
  _FOREIGN_RELATIONS = {'author': Author}
209 1 Elmer de Looff
</code></pre>
210 10 Elmer de Looff
211 12 Elmer de Looff
h2. Retrieving all records
212 11 Elmer de Looff
213 10 Elmer de Looff
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:
214 10 Elmer de Looff
215 10 Elmer de Looff
<pre><code class="python">
216 10 Elmer de Looff
class Message(model.Record):
217 10 Elmer de Looff
  """Abstraction class for messages records."""
218 10 Elmer de Looff
219 10 Elmer de Looff
# List all messages:
220 10 Elmer de Looff
>>> for message in Message.List(db_connection):
221 10 Elmer de Looff
...   print message
222 10 Elmer de Looff
... 
223 10 Elmer de Looff
Message({'message': u'First message!', 'ID': 1L, 'author': 1})
224 10 Elmer de Looff
Message({'message': u"Robert'); DROP TABLE Students;--", 'ID': 2L, 'author': 2})
225 10 Elmer de Looff
Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L, 'author': 1})
226 10 Elmer de Looff
</code></pre>
227 10 Elmer de Looff
228 10 Elmer de Looff
*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.
229 10 Elmer de Looff
230 12 Elmer de Looff
h2. Updating a record
231 10 Elmer de Looff
232 10 Elmer de Looff
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.
233 10 Elmer de Looff
234 10 Elmer de Looff
<pre><code class="python">
235 10 Elmer de Looff
class Author(model.Record):
236 10 Elmer de Looff
  """Abstraction class for author records."""
237 10 Elmer de Looff
238 10 Elmer de Looff
class Message(model.Record):
239 10 Elmer de Looff
  """Abstraction class for messages records."""
240 10 Elmer de Looff
241 16 Elmer de Looff
>>> retort = Message.FromPrimary(db_connection, 3)
242 10 Elmer de Looff
>>> retort['message'] = "Please go away Bobby."
243 10 Elmer de Looff
>>> # Our changes are not yet reflected in the database:
244 16 Elmer de Looff
>>> print Message.FromPrimary(db_connection, 3)
245 10 Elmer de Looff
Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L,
246 10 Elmer de Looff
         'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
247 10 Elmer de Looff
>>> retort.Save()
248 10 Elmer de Looff
>>> # Now our changes are committed to the database:
249 16 Elmer de Looff
>>> print Message.FromPrimary(db_connection, 3)
250 10 Elmer de Looff
Message({'message': u'Please go away Bobby.', 'ID': 3L,
251 10 Elmer de Looff
         'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})})
252 10 Elmer de Looff
</code></pre>
253 10 Elmer de Looff
254 10 Elmer de Looff
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.
255 10 Elmer de Looff
256 12 Elmer de Looff
h2. Adding a record
257 10 Elmer de Looff
258 10 Elmer de Looff
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.
259 10 Elmer de Looff
260 10 Elmer de Looff
*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.
261 10 Elmer de Looff
262 10 Elmer de Looff
Creating a record using an auto-incrementing primary key:
263 10 Elmer de Looff
<pre><code class="python">
264 10 Elmer de Looff
class Message(model.Record):
265 10 Elmer de Looff
  """Abstraction class for messages records."""
266 10 Elmer de Looff
267 10 Elmer de Looff
>>> new_message = Message(db_connection, {'author': 1, 'message': 'A new message, should be #4'})
268 10 Elmer de Looff
>>> new_message.Save()
269 10 Elmer de Looff
>>> new_message.key
270 10 Elmer de Looff
4L
271 16 Elmer de Looff
>>> print Message.FromPrimary(db_connection, new_message.key)
272 10 Elmer de Looff
Message({'message': u'A new message, should be #4', 'ID': 4L, 'author': 1})
273 10 Elmer de Looff
</code></pre>
274 10 Elmer de Looff
275 10 Elmer de Looff
Creating a record where we specify the key:
276 10 Elmer de Looff
<pre><code class="python">
277 10 Elmer de Looff
class Message(model.Record):
278 10 Elmer de Looff
  """Abstraction class for messages records."""
279 10 Elmer de Looff
280 10 Elmer de Looff
>>> another_message = Message(db_connection, {})
281 10 Elmer de Looff
>>> another_message.key = 6  # we could assign to the 'ID' key as well
282 10 Elmer de Looff
>>> another_message['author'] = 2
283 10 Elmer de Looff
>>> another_message['message'] = 'Creating a message with a defined primary key value'
284 10 Elmer de Looff
>>> another_message.Save()
285 10 Elmer de Looff
4L
286 16 Elmer de Looff
>>> print Message.FromPrimary(db_connection, another_message.key)
287 10 Elmer de Looff
Message({'message': u'Creating a message with a defined primary key value', 'ID': 6L, 'author': 2})
288 10 Elmer de Looff
</code></pre>
289 10 Elmer de Looff
290 12 Elmer de Looff
h2. Deleting a record
291 10 Elmer de Looff
292 10 Elmer de Looff
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.
293 10 Elmer de Looff
294 10 Elmer de Looff
<pre><code class="python">
295 10 Elmer de Looff
class Message(model.Record):
296 10 Elmer de Looff
  """Abstraction class for messages records."""
297 10 Elmer de Looff
298 10 Elmer de Looff
# Loading and deleting an active record.
299 16 Elmer de Looff
>>> bad_record = Message.FromPrimary(db_connection, 3)
300 10 Elmer de Looff
>>> bad_record.Delete()
301 10 Elmer de Looff
302 10 Elmer de Looff
# Deleting a record based on its primary key.
303 10 Elmer de Looff
>>> Message.DeleteKey(db_connection, 2)
304 10 Elmer de Looff
</code></pre>
305 1 Elmer de Looff
306 19 Elmer de Looff
h2. Comparisons
307 19 Elmer de Looff
308 20 Elmer de Looff
h3. Equality
309 1 Elmer de Looff
310 1 Elmer de Looff
Records must pass the following criteria to be considered equal to one another.:
311 10 Elmer de Looff
# *Type*: Two objects must be of the same type (class)
312 12 Elmer de Looff
# *Primary key*: The primary key values must compare equal
313 10 Elmer de Looff
# *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.
314 10 Elmer de Looff
# *Data*: All remaining data fields must be equal and symmetric (i.e. both objects describe the same fields)
315 10 Elmer de Looff
316 20 Elmer de Looff
h3. Greater / smaller
317 10 Elmer de Looff
318 18 Elmer de Looff
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.
319 16 Elmer de Looff
320 16 Elmer de Looff
h1. VersionedRecord
321 16 Elmer de Looff
322 16 Elmer de Looff
h1. MongoRecord