~cjwatson/storm/py39

« back to all changes in this revision

Viewing changes to storm/docs/infoheritance.rst

  • Committer: Colin Watson
  • Date: 2020-05-26 12:26:00 UTC
  • mfrom: (554.1.2 sphinx-doc)
  • Revision ID: cjwatson@canonical.com-20200526122600-ecp80bz8xf1dfby8
Add Sphinx documentation. [r=ilasc]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
This Storm document is included in Storm's source code at
2
 
`storm/tests/infoheritance.txt` so that it can be tested and be kept
3
 
up-to-date.
4
 
 
5
 
<<TableOfContents>>
6
 
 
7
 
 
8
 
==== Introduction ====
 
1
Infoheritance
 
2
=============
9
3
 
10
4
Storm doesn't support classes that have columns in multiple tables.  This
11
5
makes using inheritance rather difficult.  The infoheritance pattern described
13
7
the problems Storm has with multi-table classes.
14
8
 
15
9
 
16
 
==== Defining a sample model ====
 
10
Defining a sample model
 
11
-----------------------
17
12
 
18
13
Let's consider an inheritance hierarchy to migrate to Storm.
19
14
 
20
 
{{{#!python
21
 
class Person(object):
22
 
 
23
 
    def __init__(self, name):
24
 
        self.name = name
25
 
 
26
 
 
27
 
class SecretAgent(Person):
28
 
 
29
 
    def __init__(self, name, passcode):
30
 
        super(SecretAgent, self).__init__(name)
31
 
        self.passcode = passcode
32
 
 
33
 
 
34
 
class Teacher(Person):
35
 
 
36
 
    def __init__(self, name, school):
37
 
        super(Employee, self).__init__(name):
38
 
        self.school = school
39
 
}}}
40
 
 
41
 
We want to use three tables to store data for these objects: `person`,
42
 
`secret_agent` and `teacher`.  We can't simply convert instance attributes to
43
 
Storm properties and add `__storm_table__` definitions because a single object
44
 
may not have columns that come from more than one table.  We can't have
45
 
`Teacher` getting it's `name` column from the `person` table and it's `school`
46
 
column from the `teacher` table, for example.
47
 
 
48
 
 
49
 
==== The infoheritance pattern ====
 
15
.. code-block:: python
 
16
 
 
17
    class Person(object):
 
18
 
 
19
        def __init__(self, name):
 
20
            self.name = name
 
21
 
 
22
 
 
23
    class SecretAgent(Person):
 
24
 
 
25
        def __init__(self, name, passcode):
 
26
            super(SecretAgent, self).__init__(name)
 
27
            self.passcode = passcode
 
28
 
 
29
 
 
30
    class Teacher(Person):
 
31
 
 
32
        def __init__(self, name, school):
 
33
            super(Employee, self).__init__(name):
 
34
            self.school = school
 
35
 
 
36
We want to use three tables to store data for these objects: ``person``,
 
37
``secret_agent`` and ``teacher``.  We can't simply convert instance
 
38
attributes to Storm properties and add ``__storm_table__`` definitions
 
39
because a single object may not have columns that come from more than one
 
40
table.  We can't have ``Teacher`` getting its ``name`` column from the
 
41
``person`` table and its ``school`` column from the ``teacher`` table, for
 
42
example.
 
43
 
 
44
 
 
45
The infoheritance pattern
 
46
-------------------------
50
47
 
51
48
The infoheritance pattern uses composition instead of inheritance to work
52
49
around the multiple table limitation.  A base Storm class is used to represent
55
52
provides the additional data and behaviour you'd normally implement in a
56
53
subclass.  Following is the design from above converted to use the pattern.
57
54
 
58
 
{{{#!python
59
 
>>> from storm.locals import Storm, Store, Int, Unicode, Reference
60
 
 
61
 
>>> person_info_types = {}
62
 
 
63
 
>>> def register_person_info_type(info_type, info_class):
64
 
...     existing_info_class = person_info_types.get(info_type)
65
 
...     if existing_info_class is not None:
66
 
...         raise RuntimeError("%r has the same info_type of %r" %
67
 
...                            (info_class, existing_info_class))
68
 
...     person_info_types[info_type] = info_class
69
 
...     info_class.info_type = info_type
70
 
 
71
 
 
72
 
>>> class Person(Storm):
73
 
...
74
 
...     __storm_table__ = "person"
75
 
...
76
 
...     id = Int(allow_none=False, primary=True)
77
 
...     name = Unicode(allow_none=False)
78
 
...     info_type = Int(allow_none=False)
79
 
...     _info = None
80
 
...
81
 
...     def __init__(self, store, name, info_class, **kwargs):
82
 
...         self.name = name
83
 
...         self.info_type = info_class.info_type
84
 
...         store.add(self)
85
 
...         self._info = info_class(self, **kwargs)
86
 
...
87
 
...     @property
88
 
...     def info(self):
89
 
...         if self._info is not None:
90
 
...             return self._info
91
 
...         assert self.id is not None
92
 
...         info_class = person_info_types[self.info_type]
93
 
...         if not hasattr(info_class, "__storm_table__"):
94
 
...             info = info_class.__new__(info_class)
95
 
...             info.person = self
96
 
...         else:
97
 
...             info = Store.of(self).get(info_class, self.id)
98
 
...         self._info = info
99
 
...         return info
100
 
 
101
 
 
102
 
>>> class PersonInfo(object):
103
 
...
104
 
...     def __init__(self, person):
105
 
...         self.person = person
106
 
 
107
 
 
108
 
>>> class StoredPersonInfo(PersonInfo):
109
 
...
110
 
...     person_id = Int(allow_none=False, primary=True)
111
 
...     person = Reference(person_id, Person.id)
112
 
 
113
 
 
114
 
>>> class SecretAgent(StoredPersonInfo):
115
 
...
116
 
...     __storm_table__ = "secret_agent"
117
 
...
118
 
...     passcode = Unicode(allow_none=False)
119
 
...
120
 
...     def __init__(self, person, passcode=None):
121
 
...         super(SecretAgent, self).__init__(person)
122
 
...         self.passcode = passcode
123
 
 
124
 
 
125
 
>>> class Teacher(StoredPersonInfo):
126
 
...
127
 
...     __storm_table__ = "teacher"
128
 
...
129
 
...     school = Unicode(allow_none=False)
130
 
...
131
 
...     def __init__(self, person, school=None):
132
 
...         super(Teacher, self).__init__(person)
133
 
...         self.school = school
134
 
>>>
135
 
}}}
136
 
 
137
 
The pattern works by having a base class, `Person`, keep a reference to an
138
 
info class, `PersonInfo`.  Info classes need to be registered so that `Person`
139
 
can discover them and load them when necessary.  Note that info types have the
140
 
same ID as their parent object.  This isn't strictly necessary, but it makes
141
 
certain things easy, such as being able to look up info objects directly by ID
142
 
when given a person object.  `Person` objects are required to be in a store to
143
 
ensure that an ID is available and can used by the info class.
144
 
 
145
 
 
146
 
==== Registering info classes ====
 
55
.. doctest::
 
56
 
 
57
    >>> from storm.locals import Storm, Store, Int, Unicode, Reference
 
58
 
 
59
    >>> person_info_types = {}
 
60
 
 
61
    >>> def register_person_info_type(info_type, info_class):
 
62
    ...     existing_info_class = person_info_types.get(info_type)
 
63
    ...     if existing_info_class is not None:
 
64
    ...         raise RuntimeError("%r has the same info_type of %r" %
 
65
    ...                            (info_class, existing_info_class))
 
66
    ...     person_info_types[info_type] = info_class
 
67
    ...     info_class.info_type = info_type
 
68
 
 
69
 
 
70
    >>> class Person(Storm):
 
71
    ...
 
72
    ...     __storm_table__ = "person"
 
73
    ...
 
74
    ...     id = Int(allow_none=False, primary=True)
 
75
    ...     name = Unicode(allow_none=False)
 
76
    ...     info_type = Int(allow_none=False)
 
77
    ...     _info = None
 
78
    ...
 
79
    ...     def __init__(self, store, name, info_class, **kwargs):
 
80
    ...         self.name = name
 
81
    ...         self.info_type = info_class.info_type
 
82
    ...         store.add(self)
 
83
    ...         self._info = info_class(self, **kwargs)
 
84
    ...
 
85
    ...     @property
 
86
    ...     def info(self):
 
87
    ...         if self._info is not None:
 
88
    ...             return self._info
 
89
    ...         assert self.id is not None
 
90
    ...         info_class = person_info_types[self.info_type]
 
91
    ...         if not hasattr(info_class, "__storm_table__"):
 
92
    ...             info = info_class.__new__(info_class)
 
93
    ...             info.person = self
 
94
    ...         else:
 
95
    ...             info = Store.of(self).get(info_class, self.id)
 
96
    ...         self._info = info
 
97
    ...         return info
 
98
 
 
99
 
 
100
    >>> class PersonInfo(object):
 
101
    ...
 
102
    ...     def __init__(self, person):
 
103
    ...         self.person = person
 
104
 
 
105
 
 
106
    >>> class StoredPersonInfo(PersonInfo):
 
107
    ...
 
108
    ...     person_id = Int(allow_none=False, primary=True)
 
109
    ...     person = Reference(person_id, Person.id)
 
110
 
 
111
 
 
112
    >>> class SecretAgent(StoredPersonInfo):
 
113
    ...
 
114
    ...     __storm_table__ = "secret_agent"
 
115
    ...
 
116
    ...     passcode = Unicode(allow_none=False)
 
117
    ...
 
118
    ...     def __init__(self, person, passcode=None):
 
119
    ...         super(SecretAgent, self).__init__(person)
 
120
    ...         self.passcode = passcode
 
121
 
 
122
 
 
123
    >>> class Teacher(StoredPersonInfo):
 
124
    ...
 
125
    ...     __storm_table__ = "teacher"
 
126
    ...
 
127
    ...     school = Unicode(allow_none=False)
 
128
    ...
 
129
    ...     def __init__(self, person, school=None):
 
130
    ...         super(Teacher, self).__init__(person)
 
131
    ...         self.school = school
 
132
 
 
133
The pattern works by having a base class, ``Person``, keep a reference to an
 
134
info class, ``PersonInfo``.  Info classes need to be registered so that
 
135
``Person`` can discover them and load them when necessary.  Note that info
 
136
types have the same ID as their parent object.  This isn't strictly
 
137
necessary, but it makes certain things easy, such as being able to look up
 
138
info objects directly by ID when given a person object.  ``Person`` objects
 
139
are required to be in a store to ensure that an ID is available and can used
 
140
by the info class.
 
141
 
 
142
 
 
143
Registering info classes
 
144
------------------------
147
145
 
148
146
Let's register our info classes.  Each class must be registered with a unique
149
147
info type key.  This key is stored in the database, so be sure to use a stable
150
148
value.
151
149
 
152
 
{{{#!python
153
 
>>> register_person_info_type(1, SecretAgent)
154
 
>>> register_person_info_type(2, Teacher)
155
 
>>>
156
 
}}}
 
150
.. doctest::
 
151
 
 
152
    >>> register_person_info_type(1, SecretAgent)
 
153
    >>> register_person_info_type(2, Teacher)
157
154
 
158
155
Let's create a database to store person objects before we continue.
159
156
 
160
 
{{{#!python
161
 
>>> from storm.locals import create_database
162
 
 
163
 
>>> database = create_database("sqlite:")
164
 
>>> store = Store(database)
165
 
>>> result = store.execute("""
166
 
...     CREATE TABLE person (
167
 
...         id INTEGER PRIMARY KEY,
168
 
...         info_type INTEGER NOT NULL,
169
 
...         name TEXT NOT NULL)
170
 
... """)
171
 
>>> result = store.execute("""
172
 
...     CREATE TABLE secret_agent (
173
 
...         person_id INTEGER PRIMARY KEY,
174
 
...         passcode TEXT NOT NULL)
175
 
... """)
176
 
>>> result = store.execute("""
177
 
...     CREATE TABLE teacher (
178
 
...         person_id INTEGER PRIMARY KEY,
179
 
...         school TEXT NOT NULL)
180
 
... """)
181
 
>>>
182
 
}}}
183
 
 
184
 
 
185
 
==== Creating info classes ====
 
157
.. doctest::
 
158
 
 
159
    >>> from storm.locals import create_database
 
160
 
 
161
    >>> database = create_database("sqlite:")
 
162
    >>> store = Store(database)
 
163
    >>> result = store.execute("""
 
164
    ...     CREATE TABLE person (
 
165
    ...         id INTEGER PRIMARY KEY,
 
166
    ...         info_type INTEGER NOT NULL,
 
167
    ...         name TEXT NOT NULL)
 
168
    ... """)
 
169
    >>> result = store.execute("""
 
170
    ...     CREATE TABLE secret_agent (
 
171
    ...         person_id INTEGER PRIMARY KEY,
 
172
    ...         passcode TEXT NOT NULL)
 
173
    ... """)
 
174
    >>> result = store.execute("""
 
175
    ...     CREATE TABLE teacher (
 
176
    ...         person_id INTEGER PRIMARY KEY,
 
177
    ...         school TEXT NOT NULL)
 
178
    ... """)
 
179
 
 
180
 
 
181
Creating info classes
 
182
---------------------
186
183
 
187
184
We can easily create person objects now.
188
185
 
189
 
{{{#!python
190
 
>>> secret_agent = Person(store, u"Dick Tracy",
191
 
...                       SecretAgent, passcode=u"secret!")
192
 
>>> teacher = Person(store, u"Mrs. Cohen",
193
 
...                  Teacher, school=u"Cameron Elementary School")
194
 
>>> store.commit()
195
 
>>>
196
 
}}}
 
186
.. doctest::
 
187
 
 
188
    >>> secret_agent = Person(store, u"Dick Tracy",
 
189
    ...                       SecretAgent, passcode=u"secret!")
 
190
    >>> teacher = Person(store, u"Mrs. Cohen",
 
191
    ...                  Teacher, school=u"Cameron Elementary School")
 
192
    >>> store.commit()
197
193
 
198
194
And we can easily find them again.
199
195
 
200
 
{{{#!python
201
 
>>> del secret_agent
202
 
>>> del teacher
203
 
>>> store.rollback()
204
 
 
205
 
>>> [type(person.info) for person in store.find(Person).order_by(Person.name)]
206
 
[<class '...SecretAgent'>, <class '...Teacher'>]
207
 
>>>
208
 
}}}
209
 
 
210
 
 
211
 
==== Retrieving info classes ====
212
 
 
213
 
Now that we have our basic hierarchy in place we're going to want to retrieve
214
 
objects by info type.  Let's implement a function to make finding `Person`s
215
 
easier.
216
 
 
217
 
{{{#!python
218
 
>>> def get_persons(store, info_classes=None):
219
 
...     where = []
220
 
...     if info_classes:
221
 
...         info_types = [info_class.info_type for info_class in info_classes]
222
 
...         where = [Person.info_type.is_in(info_types)]
223
 
...     result = store.find(Person, *where)
224
 
...     result.order_by(Person.name)
225
 
...     return result
226
 
 
227
 
>>> secret_agent = get_persons(store, info_classes=[SecretAgent]).one()
228
 
>>> print(secret_agent.name)
229
 
Dick Tracy
230
 
>>> print(secret_agent.info.passcode)
231
 
secret!
232
 
 
233
 
>>> teacher = get_persons(store, info_classes=[Teacher]).one()
234
 
>>> print(teacher.name)
235
 
Mrs. Cohen
236
 
>>> print(teacher.info.school)
237
 
Cameron Elementary School
238
 
 
239
 
>>>
240
 
}}}
241
 
 
242
 
Great, we can easily find different kinds of `Person`s.
243
 
 
244
 
 
245
 
==== In-memory info objects ====
 
196
.. doctest::
 
197
 
 
198
    >>> del secret_agent
 
199
    >>> del teacher
 
200
    >>> store.rollback()
 
201
 
 
202
    >>> [type(person.info)
 
203
    ...  for person in store.find(Person).order_by(Person.name)]
 
204
    [<class '...SecretAgent'>, <class '...Teacher'>]
 
205
 
 
206
 
 
207
Retrieving info classes
 
208
-----------------------
 
209
 
 
210
Now that we have our basic hierarchy in place we're going to want to
 
211
retrieve objects by info type.  Let's implement a function to make finding
 
212
``Person``\ s easier.
 
213
 
 
214
.. doctest::
 
215
 
 
216
    >>> def get_persons(store, info_classes=None):
 
217
    ...     where = []
 
218
    ...     if info_classes:
 
219
    ...         info_types = [
 
220
    ...             info_class.info_type for info_class in info_classes]
 
221
    ...         where = [Person.info_type.is_in(info_types)]
 
222
    ...     result = store.find(Person, *where)
 
223
    ...     result.order_by(Person.name)
 
224
    ...     return result
 
225
 
 
226
    >>> secret_agent = get_persons(store, info_classes=[SecretAgent]).one()
 
227
    >>> print(secret_agent.name)
 
228
    Dick Tracy
 
229
    >>> print(secret_agent.info.passcode)
 
230
    secret!
 
231
 
 
232
    >>> teacher = get_persons(store, info_classes=[Teacher]).one()
 
233
    >>> print(teacher.name)
 
234
    Mrs. Cohen
 
235
    >>> print(teacher.info.school)
 
236
    Cameron Elementary School
 
237
 
 
238
Great, we can easily find different kinds of ``Person``\ s.
 
239
 
 
240
 
 
241
In-memory info objects
 
242
----------------------
246
243
 
247
244
This design also allows for in-memory info objects.  Let's add one to our
248
245
hierarchy.
249
246
 
250
 
{{{#!python
251
 
>>> class Ghost(PersonInfo):
252
 
...
253
 
...     friendly = True
254
 
 
255
 
>>> register_person_info_type(3, Ghost)
256
 
>>>
257
 
}}}
 
247
.. doctest::
 
248
 
 
249
    >>> class Ghost(PersonInfo):
 
250
    ...
 
251
    ...     friendly = True
 
252
 
 
253
    >>> register_person_info_type(3, Ghost)
258
254
 
259
255
We create and load in-memory objects the same way we do stored ones.
260
256
 
261
 
{{{#!python
262
 
>>> ghost = Person(store, u"Casper", Ghost)
263
 
>>> store.commit()
264
 
>>> del ghost
265
 
>>> store.rollback()
266
 
 
267
 
>>> ghost = get_persons(store, info_classes=[Ghost]).one()
268
 
>>> print(ghost.name)
269
 
Casper
270
 
>>> print(ghost.info.friendly)
271
 
True
272
 
 
273
 
>>>
274
 
}}}
 
257
.. doctest::
 
258
 
 
259
    >>> ghost = Person(store, u"Casper", Ghost)
 
260
    >>> store.commit()
 
261
    >>> del ghost
 
262
    >>> store.rollback()
 
263
 
 
264
    >>> ghost = get_persons(store, info_classes=[Ghost]).one()
 
265
    >>> print(ghost.name)
 
266
    Casper
 
267
    >>> print(ghost.info.friendly)
 
268
    True
275
269
 
276
270
This pattern is very handy when using Storm with code that would naturally be
277
271
implemented using inheritance.
278
272
 
279
 
 
280
 
==== Clean up ====
281
 
 
 
273
..
282
274
    >>> Person._storm_property_registry.clear()
283
 
 
284
 
## vim:ts=4:sw=4:et:ft=moin1_5