Model » History » Version 27
Jan Klopper, 2017-01-05 13:23
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 | 26 | Arjen Pander | 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 organized, 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 | 22 | Elmer de Looff | h2. Your first @Record@ class |
15 | 1 | Elmer de Looff | |
16 | 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: |
17 | 1 | Elmer de Looff | <pre><code class="python"> |
18 | 1 | Elmer de Looff | from uweb import model |
19 | 13 | Jan Klopper | class Message(model.Record): |
20 | 1 | Elmer de Looff | """Abstraction class for messages stored in the database.""" |
21 | 2 | Elmer de Looff | </code></pre> |
22 | 1 | Elmer de Looff | |
23 | 22 | Elmer de Looff | h2. Loading fields from primary key |
24 | 1 | Elmer de Looff | |
25 | 22 | Elmer de Looff | The Record class comes loaded with a way to load records from your database using the @FromPrimary@ method. This is a classmethod available on the @Record@ class and all your own subclasses, and when given a connection and primary key value, will load that record from the database. Provided you have a database that looks like this: |
26 | 1 | Elmer de Looff | |
27 | 22 | Elmer de Looff | <pre><code class="html"> |
28 | 22 | Elmer de Looff | -- TABLE `message` |
29 | 22 | Elmer de Looff | +----+--------+--------------------------------------------------+ |
30 | 22 | Elmer de Looff | | ID | author | message | |
31 | 22 | Elmer de Looff | +----+--------+--------------------------------------------------+ |
32 | 22 | Elmer de Looff | | 1 | Elmer | First message! | |
33 | 22 | Elmer de Looff | | 2 | Bobby | Robert'); DROP TABLE Students;-- | |
34 | 22 | Elmer de Looff | | 3 | Elmer | You didn't think it would be this easy, did you? | |
35 | 22 | Elmer de Looff | +----+--------+--------------------------------------------------+ |
36 | 22 | Elmer de Looff | </code></pre> |
37 | 1 | Elmer de Looff | |
38 | 22 | Elmer de Looff | You can load data from this table with the following code: |
39 | 22 | Elmer de Looff | |
40 | 1 | Elmer de Looff | <pre><code class="python"> |
41 | 22 | Elmer de Looff | # The model: |
42 | 1 | Elmer de Looff | from uweb import model |
43 | 22 | Elmer de Looff | class Message(model.Record): |
44 | 22 | Elmer de Looff | """Abstraction class for messages stored in the database.""" |
45 | 22 | Elmer de Looff | |
46 | 22 | Elmer de Looff | # Using this: |
47 | 22 | Elmer de Looff | >>> message = Message.FromPrimary(db_conn, 1) |
48 | 22 | Elmer de Looff | >>> print message |
49 | 22 | Elmer de Looff | Message({'message': u'First message!', 'ID': 1L, 'author': u'Elmer'}) |
50 | 22 | Elmer de Looff | </code></pre> |
51 | 22 | Elmer de Looff | |
52 | 22 | Elmer de Looff | h3. Changing the primary key field |
53 | 22 | Elmer de Looff | |
54 | 22 | Elmer de Looff | By default, @Record@ uses a primary key called @'ID'@. You can change this to any value you like, and @FromPrimary@ will automatically work based on that value, and all other methods and functionality of the class will also use this new definition (deleting, creating and auto-loading from related tables, which are all explained later). |
55 | 22 | Elmer de Looff | |
56 | 22 | Elmer de Looff | To change the primary key field, create a class with a defined @_PRIMARY_KEY@ class variable: |
57 | 22 | Elmer de Looff | <pre><code class="python"> |
58 | 22 | Elmer de Looff | from uweb import model |
59 | 1 | Elmer de Looff | class Country(model.Record): |
60 | 6 | Elmer de Looff | """Abstraction class for a country table. |
61 | 1 | Elmer de Looff | |
62 | 1 | Elmer de Looff | This class uses the ISO-3166-1 alpha2 country code as primary key. |
63 | 1 | Elmer de Looff | """ |
64 | 1 | Elmer de Looff | _PRIMARY_KEY = 'alpha2' |
65 | 1 | Elmer de Looff | </code></pre> |
66 | 1 | Elmer de Looff | |
67 | 22 | Elmer de Looff | h3. Compound primary keys |
68 | 1 | Elmer de Looff | |
69 | 22 | Elmer de Looff | The µWeb model also supports compound primary keys, with one limitation: @AUTO_INCREMENT@ fields are not supported for creation of the Record, all values need to be provided for it. |
70 | 1 | Elmer de Looff | |
71 | 24 | Jan Klopper | Loading values from a compound primary key works by passing a tuple instead of a single value: |
72 | 22 | Elmer de Looff | |
73 | 1 | Elmer de Looff | <pre><code class="python"> |
74 | 22 | Elmer de Looff | # The model: |
75 | 1 | Elmer de Looff | from uweb import model |
76 | 22 | Elmer de Looff | class MonthReport(model.Record): |
77 | 22 | Elmer de Looff | """Abstraction class for the monthReport table. |
78 | 22 | Elmer de Looff | |
79 | 22 | Elmer de Looff | This is keyed on a composite of both year and month, foregoing the need for a separate AUTO_INCREMENT field. |
80 | 22 | Elmer de Looff | """ |
81 | 22 | Elmer de Looff | _PRIMARY_KEY = 'year', 'month' |
82 | 22 | Elmer de Looff | |
83 | 22 | Elmer de Looff | # Using this: |
84 | 22 | Elmer de Looff | >>> report = MonthReport.FromPrimary(db_conn, (2012, 5)) |
85 | 22 | Elmer de Looff | >>> print report |
86 | 22 | Elmer de Looff | Message({'report': 'Things went really well', 'month': 5, 'year': 2012}) |
87 | 1 | Elmer de Looff | </code></pre> |
88 | 1 | Elmer de Looff | |
89 | 22 | Elmer de Looff | h3. Class and table relation |
90 | 1 | Elmer de Looff | |
91 | 22 | 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 your own table name to the @_TABLE@ class constant. This new table name will then be used in all built-in Record methods: |
92 | 1 | Elmer de Looff | |
93 | 1 | Elmer de Looff | <pre><code class="python"> |
94 | 1 | Elmer de Looff | from uweb import model |
95 | 1 | Elmer de Looff | class Message(model.Record): |
96 | 1 | Elmer de Looff | """Abstraction class for messages stored in the database.""" |
97 | 22 | Elmer de Looff | _TABLE = 'MyMessage' |
98 | 22 | Elmer de Looff | </code></pre> |
99 | 1 | Elmer de Looff | |
100 | 22 | Elmer de Looff | Alternatively, you can override the @TableName@ class-method to alter the table-name transformation that is done. |
101 | 22 | Elmer de Looff | |
102 | 22 | Elmer de Looff | h2. Creating records |
103 | 22 | Elmer de Looff | |
104 | 22 | Elmer de Looff | To create a record in the database, you can use the classmethod @Create@. This takes the connection and a dictionary of the keys and values that should be inserted into the database. Using the @Message@ class we defined earlier, creating a new record is a relatively simple call: |
105 | 22 | Elmer de Looff | |
106 | 22 | Elmer de Looff | <pre><code class="python"> |
107 | 22 | Elmer de Looff | >>> message = Message.Create(db_conn, {'author': 'Bob', 'message': 'Another message'}) |
108 | 1 | Elmer de Looff | >>> print message |
109 | 22 | Elmer de Looff | Message({'message': 'Another message', 'ID': 4L, 'author': 'Bob'}) |
110 | 1 | Elmer de Looff | </code></pre> |
111 | 1 | Elmer de Looff | |
112 | 23 | Jan Klopper | *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. |
113 | 1 | Elmer de Looff | |
114 | 22 | Elmer de Looff | h2. Deleting records |
115 | 6 | Elmer de Looff | |
116 | 22 | Elmer de Looff | Records can be deleted from the database either from a loaded object, or using the @DeletePrimary@ classmethod. This latter removes the record from the database using the primary key to select it. |
117 | 6 | Elmer de Looff | |
118 | 22 | Elmer de Looff | <pre><code class="python"> |
119 | 22 | Elmer de Looff | class Message(model.Record): |
120 | 22 | Elmer de Looff | """Abstraction class for messages records.""" |
121 | 16 | Elmer de Looff | |
122 | 22 | Elmer de Looff | # Loading and deleting an active record. |
123 | 22 | Elmer de Looff | >>> bad_record = Message.FromPrimary(db_connection, 3) |
124 | 22 | Elmer de Looff | >>> bad_record.Delete() |
125 | 22 | Elmer de Looff | |
126 | 22 | Elmer de Looff | # Deleting a record based on its primary key. |
127 | 22 | Elmer de Looff | >>> Message.DeletePrimary(db_connection, 2) |
128 | 22 | Elmer de Looff | </code></pre> |
129 | 22 | Elmer de Looff | |
130 | 22 | Elmer de Looff | h2. Listing all records |
131 | 22 | Elmer de Looff | |
132 | 22 | Elmer de Looff | For situations where all records must be retrieved or processed, there is the @List@ classmethod. This takes the connection as argument and iterates over all records in the database: |
133 | 22 | Elmer de Looff | |
134 | 12 | Elmer de Looff | <pre><code class="python"> |
135 | 7 | Elmer de Looff | class Message(model.Record): |
136 | 22 | Elmer de Looff | """Abstraction class for messages records.""" |
137 | 7 | Elmer de Looff | |
138 | 22 | Elmer de Looff | # List all messages: |
139 | 22 | Elmer de Looff | >>> for message in Message.List(db_connection): |
140 | 22 | Elmer de Looff | ... print message |
141 | 22 | Elmer de Looff | ... |
142 | 22 | Elmer de Looff | Message({'message': u'First message!', 'ID': 1L, 'author': 1}) |
143 | 22 | Elmer de Looff | Message({'message': u"Robert'); DROP TABLE Students;--", 'ID': 2L, 'author': 2}) |
144 | 22 | Elmer de Looff | Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L, 'author': 1}) |
145 | 7 | Elmer de Looff | </code></pre> |
146 | 7 | Elmer de Looff | |
147 | 25 | Jan Klopper | h3. Filtering and sorting using the list method: |
148 | 25 | Jan Klopper | |
149 | 25 | Jan Klopper | Possible arguments and default values: |
150 | 25 | Jan Klopper | |
151 | 25 | Jan Klopper | <pre> |
152 | 25 | Jan Klopper | @ connection: object |
153 | 25 | Jan Klopper | Database connection to use. |
154 | 25 | Jan Klopper | % conditions: str / iterable ~~ None |
155 | 25 | Jan Klopper | Optional query portion that will be used to limit the list of results. |
156 | 25 | Jan Klopper | If multiple conditions are provided, they are joined on an 'AND' string. |
157 | 25 | Jan Klopper | % limit: int ~~ None |
158 | 25 | Jan Klopper | Specifies a maximum number of items to be yielded. The limit happens on |
159 | 25 | Jan Klopper | the database side, limiting the query results. |
160 | 25 | Jan Klopper | % offset: int ~~ None |
161 | 25 | Jan Klopper | Specifies the offset at which the yielded items should start. Combined |
162 | 25 | Jan Klopper | with limit this enables proper pagination. |
163 | 25 | Jan Klopper | % order: iterable of str/2-tuple |
164 | 25 | Jan Klopper | Defines the fields on which the output should be ordered. This should |
165 | 25 | Jan Klopper | be a list of strings or 2-tuples. The string or first item indicates the |
166 | 25 | Jan Klopper | field, the second argument defines descending order (desc. if True). |
167 | 25 | Jan Klopper | % yield_unlimited_total_first: bool ~~ False |
168 | 25 | Jan Klopper | Instead of yielding only Record objects, the first item returned is the |
169 | 25 | Jan Klopper | number of results from the query if it had been executed without limit. |
170 | 25 | Jan Klopper | </pre> |
171 | 25 | Jan Klopper | |
172 | 25 | Jan Klopper | h3. Examples: |
173 | 25 | Jan Klopper | |
174 | 25 | Jan Klopper | <pre><code class="python"> |
175 | 25 | Jan Klopper | # List all messages: |
176 | 25 | Jan Klopper | Message.List(db_connection, conditions={'author':1}, limit=15) |
177 | 25 | Jan Klopper | # list only the first 15 records where the conditions are met. |
178 | 25 | Jan Klopper | |
179 | 25 | Jan Klopper | # List all messages: |
180 | 25 | Jan Klopper | Message.List(db_connection, limit=20, offset=10) |
181 | 25 | Jan Klopper | # lists records 10 to 30 |
182 | 25 | Jan Klopper | </code></pre> |
183 | 25 | Jan Klopper | |
184 | 7 | Elmer de Looff | h2. On-demand loading of referenced records. |
185 | 7 | Elmer de Looff | |
186 | 22 | Elmer de Looff | In databases that are more complex than a single table (nearly ''all''), 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. |
187 | 7 | Elmer de Looff | |
188 | 7 | Elmer de Looff | Consider the following tables in your database: |
189 | 7 | Elmer de Looff | <pre><code class="html"> |
190 | 1 | Elmer de Looff | -- TABLE `message` |
191 | 7 | Elmer de Looff | +----+--------+--------------------------------------------------+ |
192 | 7 | Elmer de Looff | | ID | author | message | |
193 | 1 | Elmer de Looff | +----+--------+--------------------------------------------------+ |
194 | 7 | Elmer de Looff | | 1 | 1 | First message! | |
195 | 7 | Elmer de Looff | | 2 | 2 | Robert'); DROP TABLE Students;-- | |
196 | 7 | Elmer de Looff | | 3 | 1 | You didn't think it would be this easy, did you? | |
197 | 7 | Elmer de Looff | +----+--------+--------------------------------------------------+ |
198 | 13 | Jan Klopper | |
199 | 7 | Elmer de Looff | -- TABLE `author` |
200 | 7 | Elmer de Looff | +----+-------+--------------------+ |
201 | 1 | Elmer de Looff | | ID | name | emailAddress | |
202 | 7 | Elmer de Looff | +----+-------+--------------------+ |
203 | 1 | Elmer de Looff | | 1 | Elmer | elmer@underdark.nl | |
204 | 7 | Elmer de Looff | | 2 | Bobby | bobby@tables.com | |
205 | 1 | Elmer de Looff | +----+-------+--------------------+ |
206 | 1 | Elmer de Looff | </code></pre> |
207 | 7 | Elmer de Looff | |
208 | 1 | Elmer de Looff | And the following class definitions in Python: |
209 | 1 | Elmer de Looff | |
210 | 1 | Elmer de Looff | <pre><code class="python"> |
211 | 1 | Elmer de Looff | from uweb import model |
212 | 1 | Elmer de Looff | class Author(model.Record): |
213 | 7 | Elmer de Looff | """Abstraction class for author records.""" |
214 | 7 | Elmer de Looff | |
215 | 16 | Elmer de Looff | class Message(model.Record): |
216 | 7 | Elmer de Looff | """Abstraction class for messages records.""" |
217 | 8 | Elmer de Looff | </code></pre> |
218 | 1 | Elmer de Looff | |
219 | 8 | 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. |
220 | 8 | Elmer de Looff | |
221 | 8 | Elmer de Looff | <pre><code class="python"> |
222 | 22 | Elmer de Looff | >>> message = Message.FromPrimary(db_conn, 1) |
223 | 8 | Elmer de Looff | >>> message |
224 | 8 | Elmer de Looff | Message({'message': u'First message!', 'ID': 1L, 'author': 1}) |
225 | 8 | Elmer de Looff | # This is the same message we saw before, without author information. |
226 | 1 | Elmer de Looff | # However, retrieving the author field specifically, provides its record: |
227 | 8 | Elmer de Looff | >>> message['author'] |
228 | 1 | Elmer de Looff | Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'}) |
229 | 1 | Elmer de Looff | >>> message |
230 | 1 | Elmer de Looff | Message({'message': u'First message!', 'ID': 1L, |
231 | 1 | Elmer de Looff | 'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})}) |
232 | 1 | Elmer de Looff | </code></pre> |
233 | 1 | Elmer de Looff | |
234 | 22 | 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 table*. 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 classmethod, with @1@ as the primary key value. |
235 | 10 | Elmer de Looff | |
236 | 10 | Elmer de Looff | # @message['author']@ uses the _author_ field |
237 | 22 | Elmer de Looff | # _author_ table is represented by Author class |
238 | 10 | Elmer de Looff | # @message['author']@ is replaced by @Author.FromPrimary(db_connection, message['author']@ |
239 | 10 | Elmer de Looff | |
240 | 22 | Elmer de Looff | h3. Customize table-references |
241 | 10 | Elmer de Looff | |
242 | 22 | Elmer de Looff | The auto-loading 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 can be a class or @None@. |
243 | 10 | Elmer de Looff | |
244 | 22 | Elmer de Looff | * @None@ specifies that the field does *not* represent a reference, and should be used as-is. |
245 | 22 | Elmer de Looff | * 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@. This "class as string" exception only exists for classes that are defined in the same module, and exists so that the model does not force you to define your classes in a certain order. It also enables the case where two tables cross-reference eachother. |
246 | 22 | Elmer de Looff | |
247 | 22 | Elmer de Looff | The following is an example case where the table names are plural, but the field names are singular: |
248 | 22 | Elmer de Looff | |
249 | 10 | Elmer de Looff | <pre><code class="python"> |
250 | 10 | Elmer de Looff | from uweb import model |
251 | 10 | Elmer de Looff | class Author(model.Record): |
252 | 12 | Elmer de Looff | """Abstraction class for author records.""" |
253 | 10 | Elmer de Looff | _TABLE = 'authors' |
254 | 10 | Elmer de Looff | |
255 | 1 | Elmer de Looff | class Message(model.Record): |
256 | 10 | Elmer de Looff | """Abstraction class for messages records.""" |
257 | 10 | Elmer de Looff | _TABLE = 'messages' |
258 | 10 | Elmer de Looff | _FOREIGN_RELATIONS = {'author': Author} |
259 | 10 | Elmer de Looff | </code></pre> |
260 | 10 | Elmer de Looff | |
261 | 10 | Elmer de Looff | h2. Loading child objects (1-to-n relations) |
262 | 10 | Elmer de Looff | |
263 | 22 | 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. |
264 | 16 | Elmer de Looff | |
265 | 10 | Elmer de Looff | Given its name and usage, the suggested usage of this is to wrap a more descriptive method around this: |
266 | 10 | Elmer de Looff | |
267 | 16 | Elmer de Looff | <pre><code class="python"> |
268 | 10 | Elmer de Looff | from uweb import model |
269 | 10 | Elmer de Looff | class Author(model.Record): |
270 | 10 | Elmer de Looff | """Abstraction class for author records.""" |
271 | 10 | Elmer de Looff | def Messages(self): |
272 | 16 | Elmer de Looff | """Returns an iterator for all messages written by this author.""" |
273 | 10 | Elmer de Looff | return self._Children(Message) |
274 | 10 | Elmer de Looff | |
275 | 10 | Elmer de Looff | class Message(model.Record): |
276 | 10 | Elmer de Looff | """Abstraction class for messages records.""" |
277 | 10 | Elmer de Looff | |
278 | 10 | Elmer de Looff | # Caller code |
279 | 12 | Elmer de Looff | >>> elmer = Author.FromPrimary(db_connection, 1) |
280 | 10 | Elmer de Looff | >>> for message in elmer.Messages(): |
281 | 10 | Elmer de Looff | ... print message |
282 | 10 | Elmer de Looff | Message({'message': u'First message!', 'ID': 1L, |
283 | 10 | Elmer de Looff | 'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})}) |
284 | 10 | Elmer de Looff | Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L, |
285 | 10 | Elmer de Looff | 'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})}) |
286 | 10 | Elmer de Looff | # Reflowing to keep things legible |
287 | 10 | Elmer de Looff | </code></pre> |
288 | 10 | Elmer de Looff | |
289 | 10 | 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. |
290 | 10 | Elmer de Looff | |
291 | 10 | Elmer de Looff | *N.B. @print@ and the methods @(iter)items@, @(iter)values@ all cause the object's foreign relations to be retrieved.* |
292 | 10 | Elmer de Looff | |
293 | 22 | Elmer de Looff | The same example, this time with pluralized table names: |
294 | 16 | Elmer de Looff | |
295 | 10 | Elmer de Looff | <pre><code class="python"> |
296 | 10 | Elmer de Looff | class Author(model.Record): |
297 | 10 | Elmer de Looff | """Abstraction class for author records.""" |
298 | 10 | Elmer de Looff | _TABLE = 'authors' |
299 | 10 | Elmer de Looff | |
300 | 10 | Elmer de Looff | def Messages(self): |
301 | 10 | Elmer de Looff | """Returns an iterator for all messages written by this author.""" |
302 | 10 | Elmer de Looff | return self._Children(Message, relation_field='author') |
303 | 10 | Elmer de Looff | |
304 | 10 | Elmer de Looff | class Message(model.Record): |
305 | 10 | Elmer de Looff | """Abstraction class for messages records.""" |
306 | 10 | Elmer de Looff | _TABLE = 'messages' |
307 | 10 | Elmer de Looff | _FOREIGN_RELATIONS = {'author': Author} |
308 | 10 | Elmer de Looff | </code></pre> |
309 | 16 | Elmer de Looff | |
310 | 10 | Elmer de Looff | h2. Updating a record |
311 | 10 | Elmer de Looff | |
312 | 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. |
313 | 12 | Elmer de Looff | |
314 | 10 | Elmer de Looff | <pre><code class="python"> |
315 | 10 | Elmer de Looff | class Author(model.Record): |
316 | 10 | Elmer de Looff | """Abstraction class for author records.""" |
317 | 10 | Elmer de Looff | |
318 | 10 | Elmer de Looff | class Message(model.Record): |
319 | 10 | Elmer de Looff | """Abstraction class for messages records.""" |
320 | 10 | Elmer de Looff | |
321 | 10 | Elmer de Looff | >>> retort = Message.FromPrimary(db_connection, 3) |
322 | 16 | Elmer de Looff | >>> retort['message'] = "Please go away Bobby." |
323 | 10 | Elmer de Looff | >>> # Our changes are not yet reflected in the database: |
324 | 10 | Elmer de Looff | >>> print Message.FromPrimary(db_connection, 3) |
325 | 10 | Elmer de Looff | Message({'message': u"You didn't think it would be this easy, did you?", 'ID': 3L, |
326 | 10 | Elmer de Looff | 'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})}) |
327 | 10 | Elmer de Looff | >>> retort.Save() |
328 | 1 | Elmer de Looff | >>> # Now our changes are committed to the database: |
329 | 19 | Elmer de Looff | >>> print Message.FromPrimary(db_connection, 3) |
330 | 19 | Elmer de Looff | Message({'message': u'Please go away Bobby.', 'ID': 3L, |
331 | 20 | Elmer de Looff | 'author': Author({'emailAddress': u'elmer@underdark.nl', 'ID': 1, 'name': u'Elmer'})}) |
332 | 1 | Elmer de Looff | </code></pre> |
333 | 1 | Elmer de Looff | |
334 | 22 | Elmer de Looff | To save all changes in related fields, we can provide the named argument *save_foreign* and set it to _True_. This way we could alter both the author name and the message itself in one database transaction. |
335 | 12 | Elmer de Looff | |
336 | 10 | Elmer de Looff | h2. Comparisons |
337 | 10 | Elmer de Looff | |
338 | 10 | Elmer de Looff | h3. Equality |
339 | 20 | Elmer de Looff | |
340 | 10 | Elmer de Looff | Records must pass the following criteria to be considered equal to one another.: |
341 | 18 | Elmer de Looff | # *Type*: Two objects must be of the same type (class) |
342 | 16 | Elmer de Looff | # *Primary key*: The primary key values must compare equal |
343 | 16 | 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. |
344 | 16 | Elmer de Looff | # *Data*: All remaining data fields must be equal and symmetric (i.e. both objects describe the same fields) |
345 | 16 | Elmer de Looff | |
346 | 1 | Elmer de Looff | h3. Greater / smaller |
347 | 1 | Elmer de Looff | |
348 | 1 | 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. |
349 | 1 | Elmer de Looff | |
350 | 27 | Jan Klopper | h3. Hooks |
351 | 27 | Jan Klopper | |
352 | 27 | Jan Klopper | To facilitate various checks or custom code when records are created, initialized, inserted etc the following hooks are available: |
353 | 27 | Jan Klopper | |
354 | 27 | Jan Klopper | * _PreCreate |
355 | 27 | Jan Klopper | * _PreSave |
356 | 27 | Jan Klopper | * _PostInit |
357 | 27 | Jan Klopper | * _PostCreate |
358 | 27 | Jan Klopper | * _PostSave |
359 | 27 | Jan Klopper | |
360 | 27 | Jan Klopper | <pre><code class="python"> |
361 | 27 | Jan Klopper | from uweb import model |
362 | 27 | Jan Klopper | import hashlib |
363 | 27 | Jan Klopper | |
364 | 27 | Jan Klopper | class Author(model.Record): |
365 | 27 | Jan Klopper | """Demonstrate a PreCreate method.""" |
366 | 27 | Jan Klopper | |
367 | 27 | Jan Klopper | def _PreCreate(self, cursor): |
368 | 27 | Jan Klopper | """Sets the API key for the new record and insert the dateCreated value""" |
369 | 27 | Jan Klopper | now = datetime.datetime.utcnow() |
370 | 27 | Jan Klopper | self['dateCreated'] = now |
371 | 27 | Jan Klopper | self['apikey'] = hashlib.md5(str(random.randrange(9999999999))).hexdigest() |
372 | 27 | Jan Klopper | </code></pre> |
373 | 27 | Jan Klopper | |
374 | 1 | Elmer de Looff | h1. VersionedRecord |
375 | 27 | Jan Klopper | |
376 | 27 | Jan Klopper | The versionedRecord class provides a dual key table, in which there is one primary key (for example a record ID) which looks up the individual versioned records, and an overall key which is used to return the most recent version. |
377 | 27 | Jan Klopper | If for example you'd need a table that holds all of your customer details. And you also need to keep older versions of their details available for older invoices, you could used a VersionedRecord to hold this information. |
378 | 27 | Jan Klopper | You would use the overall key to load the current / newest customer details, and you would use the specific record ID to keep track of which version of the customer details you need for a specific invoice. |
379 | 27 | Jan Klopper | |
380 | 27 | Jan Klopper | Te VersionedRecord acts much like the Record class. |
381 | 27 | Jan Klopper | It introduces: |
382 | 27 | Jan Klopper | |
383 | 27 | Jan Klopper | The method: 'Versions' which lists all the versions for a specific identifier. |
384 | 27 | Jan Klopper | The property: 'identifier' which returns the version value for the current record. |
385 | 1 | Elmer de Looff | |
386 | 1 | Elmer de Looff | h1. MongoRecord |