Project

General

Profile

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>