AQuickTutorial » History » Version 10
Eric Worden, 12/22/2016 02:42 PM
rewrite for 2.0
1 | 10 | Eric Worden | h1. Simpycity Tutorial |
---|---|---|---|
2 | 8 | Aurynn Shaw | |
3 | 10 | Eric Worden | {{toc}} |
4 | 1 | Aurynn Shaw | |
5 | 10 | Eric Worden | You may wish to read the Simpycity [[WikiStart|README]] first. |
6 | 1 | Aurynn Shaw | |
7 | 10 | Eric Worden | This tutorial walks you through a simple console program that displays information about animals. First, we have one table in a database of animals: |
8 | 1 | Aurynn Shaw | |
9 | 10 | Eric Worden | <pre><code class="sql"> |
10 | create table animal |
||
11 | ( |
||
12 | id int primary key, |
||
13 | genus_id int, |
||
14 | species text, |
||
15 | common_name text, |
||
16 | extinct boolean |
||
17 | ); |
||
18 | </code></pre> |
||
19 | 1 | Aurynn Shaw | |
20 | 10 | Eric Worden | h2. Basic Models |
21 | 1 | Aurynn Shaw | |
22 | 10 | Eric Worden | Our Python model library will start like this: |
23 | 1 | Aurynn Shaw | |
24 | 10 | Eric Worden | <pre><code class="python"> |
25 | import simpycity.model |
||
26 | import simpycity.core |
||
27 | 1 | Aurynn Shaw | |
28 | 10 | Eric Worden | class Animal(simpycity.model.SimpleModel): |
29 | table = ['id', |
||
30 | 'genus_id', |
||
31 | 'species', |
||
32 | 'common_name', |
||
33 | 'extinct'] |
||
34 | 1 | Aurynn Shaw | |
35 | 10 | Eric Worden | __load__ = simpycity.core.QuerySingle('public.animal', ['id']) |
36 | </code></pre> |
||
37 | 1 | Aurynn Shaw | |
38 | 10 | Eric Worden | SimpleModel is the class that connects PostgreSQL types to Python classes. Programmers unfamiliar with PostgreSQL may be unfamiliar with the concept of "type" in the context of a database. In PostgreSQL, every table and view defines a row type. Additionally, composite data types can be defined and functions declared to return values or sets of such types. Simpycity supports mapping of these formally defined types -- and also adhoc query result sets -- to Python classes. |
39 | 9 | Aurynn Shaw | |
40 | 10 | Eric Worden | At the minimum, a SimpleModel needs to know what PostgreSQL type (or at least what result set column names) it maps to, and it needs to now how to query the database. Above, we explicitly list the columns from the animal table; later we'll see how that can be left blank and done automatically. The *__load__* attribute in this case defines the way to query the data as a simple query on a table by an id column. The analogous *__lazyload__* attribute does the same thing but only queries the database at the point where an attribute reference requires it. |
41 | 9 | Aurynn Shaw | |
42 | 10 | Eric Worden | Our main script file looks like this to start: |
43 | 1 | Aurynn Shaw | |
44 | 10 | Eric Worden | <pre><code class="python"> |
45 | import simpycity.config |
||
46 | import animals |
||
47 | 1 | Aurynn Shaw | |
48 | 10 | Eric Worden | simpycity.config.port = 5434 |
49 | simpycity.config.database = 'animals' |
||
50 | simpycity.config.user = 'ormboss' |
||
51 | 1 | Aurynn Shaw | |
52 | 10 | Eric Worden | animal = animals.Animal(id=3) |
53 | print(animal.common_name) |
||
54 | </code></pre> |
||
55 | 1 | Aurynn Shaw | |
56 | 10 | Eric Worden | And its output is *"racoon"*. When the *Animal* object is instantiated, the *__load__* attribute signals to immediately query the database and populate the class with attributes named for the members of *table*. So *animal.common_name* is already populated before the print function runs. |
57 | 1 | Aurynn Shaw | |
58 | |||
59 | 10 | Eric Worden | h2. Querying Models |
60 | 1 | Aurynn Shaw | |
61 | 10 | Eric Worden | Data accessor methods can easily be added to our SimpleModel. Here is a silly *mammals* function in sql: |
62 | 1 | Aurynn Shaw | |
63 | 10 | Eric Worden | <pre><code class="sql"> |
64 | CREATE FUNCTION mammals() RETURNS setof animal AS |
||
65 | $body$ |
||
66 | SELECT * FROM animal where id in (1,3,7); |
||
67 | $body$ language sql; |
||
68 | </code></pre> |
||
69 | 1 | Aurynn Shaw | |
70 | 10 | Eric Worden | We map this to our Animals class by adding: |
71 | 1 | Aurynn Shaw | |
72 | 10 | Eric Worden | <pre><code class="python"> |
73 | mammals = simpycity.core.Function('public.mammals', []) |
||
74 | </code></pre> |
||
75 | 1 | Aurynn Shaw | |
76 | 10 | Eric Worden | To our main script we add: |
77 | 1 | Aurynn Shaw | |
78 | 10 | Eric Worden | <pre><code class="python"> |
79 | for mammal in animal.mammals(): |
||
80 | print(repr(mammal)) |
||
81 | </code></pre> |
||
82 | 1 | Aurynn Shaw | |
83 | 10 | Eric Worden | which outputs psycopg2 DictRows: |
84 | 1 | Aurynn Shaw | |
85 | 10 | Eric Worden | <pre><code class="python"> |
86 | [1, 2, 'sapiens', 'human', False] |
||
87 | [3, 3, 'lotor', 'racoon', False] |
||
88 | [7, 7, 'hemionus', 'mule deer', False] |
||
89 | </code></pre> |
||
90 | 1 | Aurynn Shaw | |
91 | 10 | Eric Worden | Simpycity uses the psycopg2 library to execute queries, and utilizes its *DictRow* option, which causes result sets to behave like both lists and dicts, so *print(mammal['common_name'])* outputs *racoon*. |
92 | 1 | Aurynn Shaw | |
93 | 10 | Eric Worden | The *Function* class returns a psycopg2 cursor. The *QuerySingle* and *FunctionSingle* classes returns the single row from a sql query/function that returns a single row. |
94 | 1 | Aurynn Shaw | |
95 | 10 | Eric Worden | Soon we'll see how to return Python class instances in result sets instead of simple scalar values. First, we have to look at database connections. |
96 | 1 | Aurynn Shaw | |
97 | |||
98 | 10 | Eric Worden | h2. Database Connections and Handles |
99 | 1 | Aurynn Shaw | |
100 | 10 | Eric Worden | When your code is running a bunch of queries, you normally want them running in a single connection and often in a single transaction. The type-mapping feature described in the next section depends on the app utilizing a single connection. |
101 | 1 | Aurynn Shaw | |
102 | 10 | Eric Worden | Simpycity's *Handle* class wraps a psycopg2 connection and cursors. You need all your code to use the same *Handle* object. Add this under the *config* lines at the top of the script: |
103 | 1 | Aurynn Shaw | |
104 | 10 | Eric Worden | <pre><code class="python"> |
105 | handle = simpycity.handle.Handle() |
||
106 | 1 | Aurynn Shaw | |
107 | 10 | Eric Worden | def handle_factory(*args, **kwargs): |
108 | return handle |
||
109 | 1 | Aurynn Shaw | |
110 | 10 | Eric Worden | simpycity.config.handle_factory = handle_factory |
111 | </code></pre> |
||
112 | 1 | Aurynn Shaw | |
113 | 10 | Eric Worden | Now, any Simpycity object that needs a handle will use the one that has already been created from the *config* values. Before we added these lines, the script created a new connection handle every time a handle was needed. |
114 | 1 | Aurynn Shaw | |
115 | 10 | Eric Worden | h2. Mapping SQL Types to Python Classes |
116 | 1 | Aurynn Shaw | |
117 | 10 | Eric Worden | Returning lists from a query is a weak form of mapping. We can return Python classes from our queries instead. In our main script before the line instantiating *animal* we add: |
118 | 1 | Aurynn Shaw | |
119 | 10 | Eric Worden | animals.Animal.register_composite('public.animal') |
120 | 1 | Aurynn Shaw | |
121 | 10 | Eric Worden | This magical line tells psycopg2 to pass column values to the *Animal* constructor whenever a PostgreSQL type of *public.animal* is returned. The *Animal* constructor creates instance attributes from the column values. Now in our library code we can replace: |
122 | 1 | Aurynn Shaw | |
123 | 10 | Eric Worden | <pre><code class="python"> |
124 | mammals = simpycity.core.Function('public.mammals', []) |
||
125 | </code></pre> |
||
126 | 1 | Aurynn Shaw | |
127 | 10 | Eric Worden | with: |
128 | 1 | Aurynn Shaw | |
129 | 10 | Eric Worden | <pre><code class="python"> |
130 | mammals = simpycity.core.FunctionTyped('public.mammals', []) |
||
131 | </code></pre> |
||
132 | 9 | Aurynn Shaw | |
133 | 10 | Eric Worden | And in our main script we replace: |
134 | |||
135 | <pre><code class="python"> |
||
136 | print(mammal['common_name']) |
||
137 | </code></pre> |
||
138 | |||
139 | with: |
||
140 | |||
141 | <pre><code class="python"> |
||
142 | print(mammal.common_name) |
||
143 | </code></pre> |
||
144 | |||
145 | because each *mammal* is now an instance of *Animal*, not a psycopg2 *DictRow*. If your query only returns a single row, then you can use the *FunctionTypedSingle* class, or *QueryTypedSingle* class for table/view queries. For semantic tidiness you can use the *Property* class just like *FunctionTypedSingle*, except that a *Property* attribute cannot be called. For example: |
||
146 | |||
147 | <pre><code class="sql"> |
||
148 | CREATE FUNCTION human() RETURNS animal AS |
||
149 | $body$ |
||
150 | SELECT * FROM animal where id = 1; |
||
151 | $body$ language sql; |
||
152 | |||
153 | class Animal(simpycity.model.SimpleModel): |
||
154 | ... |
||
155 | master = simpycity.core.Property('public.human', []) |
||
156 | |||
157 | animal = animals.Animal(id=3) |
||
158 | print(animal.master.species) |
||
159 | >>>sapiens |
||
160 | </code></pre> |
||
161 | |||
162 | h2. Automatic Table Attribute |
||
163 | |||
164 | Because psycopg2 is deeply familiar with PostgreSQL, we can leverage type mapping so that figuring the *table* attribute, ie. list of column names, is completely automatic. In our *Animal* class we add a class attribute *pg_type*, and then omit *table*: |
||
165 | |||
166 | <pre><code class="python"> |
||
167 | pg_type = ('public', 'animal') |
||
168 | #table = [] |
||
169 | </code></pre> |
||
170 | |||
171 | The script output will remain identical. |
||
172 | |||
173 | |||
174 | h2. Type and Class Inheritance |
||
175 | |||
176 | Of course Python supports class inheritance and this feature is extremely powerful and useful. Many people don't realize that PostgreSQL also supports it. Simpycity maps the two together: |
||
177 | |||
178 | <pre><code class="sql"> |
||
179 | create table predator (tactic text, primary key (id)) inherits (animal); |
||
180 | insert into predator select * from animal where id in (1,6,10); |
||
181 | delete from only animal where id in (1,6,10); |
||
182 | update predator set tactic = 'shoot' where species = 'sapiens'; |
||
183 | update predator set tactic = 'gulp' where species = 'catesbeiana'; |
||
184 | update predator set tactic = 'chomp' where species = 'rex'; |
||
185 | |||
186 | create table predation |
||
187 | ( |
||
188 | predator_id int references predator (id), |
||
189 | prey_id int references animal (id) |
||
190 | ); |
||
191 | |||
192 | copy predatation from predator.csv with (format csv) |
||
193 | |||
194 | create type predator_type as |
||
195 | ( |
||
196 | base_ animal, |
||
197 | tactic text, |
||
198 | prey animal[] |
||
199 | ); |
||
200 | </code></pre> |
||
201 | |||
202 | *base_* is simply a special name given to an inherited type, in this case, *animal*. *animal[]* is an array of prey animal types. The following function returns a single composite *predator_type* representing a predator given an id: |
||
203 | |||
204 | <pre><code class="sql"> |
||
205 | create or replace function predator(id int) |
||
206 | returns predator_type |
||
207 | language sql as |
||
208 | $$ |
||
209 | select |
||
210 | row(p1.id, p1.genus_id, p1.species, p1.common_name, p1.extinct):animal, |
||
211 | p1.tactic, |
||
212 | array_agg(row(prey.*):animal):animal[] |
||
213 | from |
||
214 | predator p1 |
||
215 | join predation p2 on p1.id = p2.predator_id |
||
216 | join animal prey on prey.id = p2.prey_id |
||
217 | where p1.id = $1 |
||
218 | group by 1,2; |
||
219 | $$; |
||
220 | </code></pre> |
||
221 | |||
222 | Now inherit the base class. If you want *table* to be automatically figured, do not override it: |
||
223 | |||
224 | <pre><code class="python"> |
||
225 | class Predator(Animal): |
||
226 | pg_type = ('public', 'predator_type') |
||
227 | __load__ = simpycity.core.FunctionTypedSingle('public.predator', ['id']) |
||
228 | </code></pre> |
||
229 | |||
230 | In the main script be sure to register this class *after* the superclass: |
||
231 | |||
232 | <pre><code class="python"> |
||
233 | animals.Predator.register_composite('public.predator_type') |
||
234 | trex = animals.Predator(id=10) |
||
235 | print("{0}, {1}!".format(trex.common_name, trex.tactic)) |
||
236 | for victim in trex.prey: |
||
237 | print('Victim: ' + victim.common_name) |
||
238 | >>>tyranosaurus rex, chomp! |
||
239 | >>>Victim: mule deer |
||
240 | >>>Victim: elephant |
||
241 | </code></pre> |