1 | """A simple declarative layer for SQLAlchemy ORM. |
---|
2 | |
---|
3 | Synopsis |
---|
4 | ======== |
---|
5 | |
---|
6 | SQLAlchemy object-relational configuration involves the usage of :class:`~sqlalchemy.schema.Table`, |
---|
7 | :func:`~sqlalchemy.orm.mapper`, and class objects to define the three areas of configuration. |
---|
8 | ``declarative`` moves these three types of configuration underneath the individual |
---|
9 | mapped class. Regular SQLAlchemy schema and ORM constructs are used in most |
---|
10 | cases:: |
---|
11 | |
---|
12 | from sqlalchemy.ext.declarative import declarative_base |
---|
13 | |
---|
14 | Base = declarative_base() |
---|
15 | |
---|
16 | class SomeClass(Base): |
---|
17 | __tablename__ = 'some_table' |
---|
18 | id = Column(Integer, primary_key=True) |
---|
19 | name = Column(String(50)) |
---|
20 | |
---|
21 | Above, the :func:`declarative_base` callable produces a new base class from which |
---|
22 | all mapped classes inherit from. When the class definition is completed, a |
---|
23 | new :class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.orm.mapper` have been generated, accessible via the |
---|
24 | ``__table__`` and ``__mapper__`` attributes on the ``SomeClass`` class. |
---|
25 | |
---|
26 | Defining Attributes |
---|
27 | =================== |
---|
28 | |
---|
29 | :class:`~sqlalchemy.schema.Column` objects may be explicitly named, |
---|
30 | including using a different name than the attribute in which they are associated. |
---|
31 | The column will be assigned to the :class:`~sqlalchemy.schema.Table` using the |
---|
32 | given name, and mapped to the class using the attribute name:: |
---|
33 | |
---|
34 | class SomeClass(Base): |
---|
35 | __tablename__ = 'some_table' |
---|
36 | id = Column("some_table_id", Integer, primary_key=True) |
---|
37 | name = Column("name", String(50)) |
---|
38 | |
---|
39 | Otherwise, you may omit the names from the Column definitions. |
---|
40 | Declarative will set the ``name`` attribute on the column when the class |
---|
41 | is initialized:: |
---|
42 | |
---|
43 | class SomeClass(Base): |
---|
44 | __tablename__ = 'some_table' |
---|
45 | id = Column(Integer, primary_key=True) |
---|
46 | name = Column(String(50)) |
---|
47 | |
---|
48 | Attributes may be added to the class after its construction, and they will be |
---|
49 | added to the underlying :class:`~sqlalchemy.schema.Table` and :func:`~sqlalchemy.orm.mapper()` definitions as |
---|
50 | appropriate:: |
---|
51 | |
---|
52 | SomeClass.data = Column('data', Unicode) |
---|
53 | SomeClass.related = relation(RelatedInfo) |
---|
54 | |
---|
55 | Classes which are mapped explicitly using :func:`~sqlalchemy.orm.mapper()` can interact freely |
---|
56 | with declarative classes. It is recommended, though not required, that all tables |
---|
57 | share the same underlying :class:`~sqlalchemy.schema.MetaData` object, so that |
---|
58 | string-configured :class:`~sqlalchemy.schema.ForeignKey` references can be resolved without issue. |
---|
59 | |
---|
60 | Association of Metadata and Engine |
---|
61 | ================================== |
---|
62 | |
---|
63 | The :func:`declarative_base` base class contains a :class:`~sqlalchemy.schema.MetaData` object where newly |
---|
64 | defined :class:`~sqlalchemy.schema.Table` objects are collected. This is accessed via the |
---|
65 | :class:`~sqlalchemy.schema.MetaData` class level accessor, so to create tables we can say:: |
---|
66 | |
---|
67 | engine = create_engine('sqlite://') |
---|
68 | Base.metadata.create_all(engine) |
---|
69 | |
---|
70 | The :class:`~sqlalchemy.engine.base.Engine` created above may also be directly associated with the |
---|
71 | declarative base class using the ``bind`` keyword argument, where it will be |
---|
72 | associated with the underlying :class:`~sqlalchemy.schema.MetaData` object and allow SQL operations |
---|
73 | involving that metadata and its tables to make use of that engine |
---|
74 | automatically:: |
---|
75 | |
---|
76 | Base = declarative_base(bind=create_engine('sqlite://')) |
---|
77 | |
---|
78 | Or, as :class:`~sqlalchemy.schema.MetaData` allows, at any time using the ``bind`` attribute:: |
---|
79 | |
---|
80 | Base.metadata.bind = create_engine('sqlite://') |
---|
81 | |
---|
82 | The :func:`declarative_base` can also receive a pre-created :class:`~sqlalchemy.schema.MetaData` object, |
---|
83 | which allows a declarative setup to be associated with an already existing |
---|
84 | traditional collection of :class:`~sqlalchemy.schema.Table` objects:: |
---|
85 | |
---|
86 | mymetadata = MetaData() |
---|
87 | Base = declarative_base(metadata=mymetadata) |
---|
88 | |
---|
89 | Configuring Relations |
---|
90 | ===================== |
---|
91 | |
---|
92 | Relations to other classes are done in the usual way, with the added feature |
---|
93 | that the class specified to :func:`~sqlalchemy.orm.relation()` may be a string name. The "class |
---|
94 | registry" associated with ``Base`` is used at mapper compilation time to |
---|
95 | resolve the name into the actual class object, which is expected to have been |
---|
96 | defined once the mapper configuration is used:: |
---|
97 | |
---|
98 | class User(Base): |
---|
99 | __tablename__ = 'users' |
---|
100 | |
---|
101 | id = Column(Integer, primary_key=True) |
---|
102 | name = Column(String(50)) |
---|
103 | addresses = relation("Address", backref="user") |
---|
104 | |
---|
105 | class Address(Base): |
---|
106 | __tablename__ = 'addresses' |
---|
107 | |
---|
108 | id = Column(Integer, primary_key=True) |
---|
109 | email = Column(String(50)) |
---|
110 | user_id = Column(Integer, ForeignKey('users.id')) |
---|
111 | |
---|
112 | Column constructs, since they are just that, are immediately usable, as below |
---|
113 | where we define a primary join condition on the ``Address`` class using them:: |
---|
114 | |
---|
115 | class Address(Base): |
---|
116 | __tablename__ = 'addresses' |
---|
117 | |
---|
118 | id = Column(Integer, primary_key=True) |
---|
119 | email = Column(String(50)) |
---|
120 | user_id = Column(Integer, ForeignKey('users.id')) |
---|
121 | user = relation(User, primaryjoin=user_id == User.id) |
---|
122 | |
---|
123 | In addition to the main argument for :func:`~sqlalchemy.orm.relation`, other arguments |
---|
124 | which depend upon the columns present on an as-yet undefined class |
---|
125 | may also be specified as strings. These strings are evaluated as |
---|
126 | Python expressions. The full namespace available within this |
---|
127 | evaluation includes all classes mapped for this declarative base, |
---|
128 | as well as the contents of the ``sqlalchemy`` package, including |
---|
129 | expression functions like :func:`~sqlalchemy.sql.expression.desc` and :attr:`~sqlalchemy.sql.expression.func`:: |
---|
130 | |
---|
131 | class User(Base): |
---|
132 | # .... |
---|
133 | addresses = relation("Address", order_by="desc(Address.email)", |
---|
134 | primaryjoin="Address.user_id==User.id") |
---|
135 | |
---|
136 | As an alternative to string-based attributes, attributes may also be |
---|
137 | defined after all classes have been created. Just add them to the target |
---|
138 | class after the fact:: |
---|
139 | |
---|
140 | User.addresses = relation(Address, primaryjoin=Address.user_id == User.id) |
---|
141 | |
---|
142 | Configuring Many-to-Many Relations |
---|
143 | ================================== |
---|
144 | |
---|
145 | There's nothing special about many-to-many with declarative. The ``secondary`` |
---|
146 | argument to :func:`~sqlalchemy.orm.relation` still requires a :class:`~sqlalchemy.schema.Table` object, not a declarative class. |
---|
147 | The :class:`~sqlalchemy.schema.Table` should share the same :class:`~sqlalchemy.schema.MetaData` object used by the declarative base:: |
---|
148 | |
---|
149 | keywords = Table('keywords', Base.metadata, |
---|
150 | Column('author_id', Integer, ForeignKey('authors.id')), |
---|
151 | Column('keyword_id', Integer, ForeignKey('keywords.id')) |
---|
152 | ) |
---|
153 | |
---|
154 | class Author(Base): |
---|
155 | __tablename__ = 'authors' |
---|
156 | id = Column(Integer, primary_key=True) |
---|
157 | keywords = relation("Keyword", secondary=keywords) |
---|
158 | |
---|
159 | You should generally **not** map a class and also specify its table in a many-to-many |
---|
160 | relation, since the ORM may issue duplicate INSERT and DELETE statements. |
---|
161 | |
---|
162 | |
---|
163 | Defining Synonyms |
---|
164 | ================= |
---|
165 | |
---|
166 | Synonyms are introduced in :ref:`synonyms`. To define a getter/setter which |
---|
167 | proxies to an underlying attribute, use :func:`~sqlalchemy.orm.synonym` with the |
---|
168 | ``descriptor`` argument:: |
---|
169 | |
---|
170 | class MyClass(Base): |
---|
171 | __tablename__ = 'sometable' |
---|
172 | |
---|
173 | _attr = Column('attr', String) |
---|
174 | |
---|
175 | def _get_attr(self): |
---|
176 | return self._some_attr |
---|
177 | def _set_attr(self, attr): |
---|
178 | self._some_attr = attr |
---|
179 | attr = synonym('_attr', descriptor=property(_get_attr, _set_attr)) |
---|
180 | |
---|
181 | The above synonym is then usable as an instance attribute as well as a |
---|
182 | class-level expression construct:: |
---|
183 | |
---|
184 | x = MyClass() |
---|
185 | x.attr = "some value" |
---|
186 | session.query(MyClass).filter(MyClass.attr == 'some other value').all() |
---|
187 | |
---|
188 | For simple getters, the :func:`synonym_for` decorator can be used in conjunction |
---|
189 | with ``@property``:: |
---|
190 | |
---|
191 | class MyClass(Base): |
---|
192 | __tablename__ = 'sometable' |
---|
193 | |
---|
194 | _attr = Column('attr', String) |
---|
195 | |
---|
196 | @synonym_for('_attr') |
---|
197 | @property |
---|
198 | def attr(self): |
---|
199 | return self._some_attr |
---|
200 | |
---|
201 | Similarly, :func:`comparable_using` is a front end for the :func:`~sqlalchemy.orm.comparable_property` |
---|
202 | ORM function:: |
---|
203 | |
---|
204 | class MyClass(Base): |
---|
205 | __tablename__ = 'sometable' |
---|
206 | |
---|
207 | name = Column('name', String) |
---|
208 | |
---|
209 | @comparable_using(MyUpperCaseComparator) |
---|
210 | @property |
---|
211 | def uc_name(self): |
---|
212 | return self.name.upper() |
---|
213 | |
---|
214 | Table Configuration |
---|
215 | =================== |
---|
216 | |
---|
217 | Table arguments other than the name, metadata, and mapped Column arguments |
---|
218 | are specified using the ``__table_args__`` class attribute. This attribute |
---|
219 | accommodates both positional as well as keyword arguments that are normally |
---|
220 | sent to the :class:`~sqlalchemy.schema.Table` constructor. The attribute can be specified |
---|
221 | in one of two forms. One is as a dictionary:: |
---|
222 | |
---|
223 | class MyClass(Base): |
---|
224 | __tablename__ = 'sometable' |
---|
225 | __table_args__ = {'mysql_engine':'InnoDB'} |
---|
226 | |
---|
227 | The other, a tuple of the form ``(arg1, arg2, ..., {kwarg1:value, ...})``, which |
---|
228 | allows positional arguments to be specified as well (usually constraints):: |
---|
229 | |
---|
230 | class MyClass(Base): |
---|
231 | __tablename__ = 'sometable' |
---|
232 | __table_args__ = ( |
---|
233 | ForeignKeyConstraint(['id'], ['remote_table.id']), |
---|
234 | UniqueConstraint('foo'), |
---|
235 | {'autoload':True} |
---|
236 | ) |
---|
237 | |
---|
238 | Note that the dictionary is required in the tuple form even if empty. |
---|
239 | |
---|
240 | As an alternative to ``__tablename__``, a direct :class:`~sqlalchemy.schema.Table` |
---|
241 | construct may be used. The :class:`~sqlalchemy.schema.Column` objects, which |
---|
242 | in this case require their names, will be |
---|
243 | added to the mapping just like a regular mapping to a table:: |
---|
244 | |
---|
245 | class MyClass(Base): |
---|
246 | __table__ = Table('my_table', Base.metadata, |
---|
247 | Column('id', Integer, primary_key=True), |
---|
248 | Column('name', String(50)) |
---|
249 | ) |
---|
250 | |
---|
251 | Mapper Configuration |
---|
252 | ==================== |
---|
253 | |
---|
254 | Mapper arguments are specified using the ``__mapper_args__`` class variable, |
---|
255 | which is a dictionary that accepts the same names as the :class:`~sqlalchemy.orm.mapper` |
---|
256 | function accepts as keywords:: |
---|
257 | |
---|
258 | class Widget(Base): |
---|
259 | __tablename__ = 'widgets' |
---|
260 | id = Column(Integer, primary_key=True) |
---|
261 | |
---|
262 | __mapper_args__ = {'extension': MyWidgetExtension()} |
---|
263 | |
---|
264 | Inheritance Configuration |
---|
265 | ========================= |
---|
266 | |
---|
267 | Declarative supports all three forms of inheritance as intuitively |
---|
268 | as possible. The ``inherits`` mapper keyword argument is not needed, |
---|
269 | as declarative will determine this from the class itself. The various |
---|
270 | "polymorphic" keyword arguments are specified using ``__mapper_args__``. |
---|
271 | |
---|
272 | Joined Table Inheritance |
---|
273 | ~~~~~~~~~~~~~~~~~~~~~~~~ |
---|
274 | |
---|
275 | Joined table inheritance is defined as a subclass that defines its own |
---|
276 | table:: |
---|
277 | |
---|
278 | class Person(Base): |
---|
279 | __tablename__ = 'people' |
---|
280 | id = Column(Integer, primary_key=True) |
---|
281 | discriminator = Column('type', String(50)) |
---|
282 | __mapper_args__ = {'polymorphic_on': discriminator} |
---|
283 | |
---|
284 | class Engineer(Person): |
---|
285 | __tablename__ = 'engineers' |
---|
286 | __mapper_args__ = {'polymorphic_identity': 'engineer'} |
---|
287 | id = Column(Integer, ForeignKey('people.id'), primary_key=True) |
---|
288 | primary_language = Column(String(50)) |
---|
289 | |
---|
290 | Note that above, the ``Engineer.id`` attribute, since it shares the same |
---|
291 | attribute name as the ``Person.id`` attribute, will in fact represent the ``people.id`` |
---|
292 | and ``engineers.id`` columns together, and will render inside a query as ``"people.id"``. |
---|
293 | To provide the ``Engineer`` class with an attribute that represents only the |
---|
294 | ``engineers.id`` column, give it a different attribute name:: |
---|
295 | |
---|
296 | class Engineer(Person): |
---|
297 | __tablename__ = 'engineers' |
---|
298 | __mapper_args__ = {'polymorphic_identity': 'engineer'} |
---|
299 | engineer_id = Column('id', Integer, ForeignKey('people.id'), primary_key=True) |
---|
300 | primary_language = Column(String(50)) |
---|
301 | |
---|
302 | Single Table Inheritance |
---|
303 | ~~~~~~~~~~~~~~~~~~~~~~~~ |
---|
304 | |
---|
305 | Single table inheritance is defined as a subclass that does not have its |
---|
306 | own table; you just leave out the ``__table__`` and ``__tablename__`` attributes:: |
---|
307 | |
---|
308 | class Person(Base): |
---|
309 | __tablename__ = 'people' |
---|
310 | id = Column(Integer, primary_key=True) |
---|
311 | discriminator = Column('type', String(50)) |
---|
312 | __mapper_args__ = {'polymorphic_on': discriminator} |
---|
313 | |
---|
314 | class Engineer(Person): |
---|
315 | __mapper_args__ = {'polymorphic_identity': 'engineer'} |
---|
316 | primary_language = Column(String(50)) |
---|
317 | |
---|
318 | When the above mappers are configured, the ``Person`` class is mapped to the ``people`` |
---|
319 | table *before* the ``primary_language`` column is defined, and this column will not be included |
---|
320 | in its own mapping. When ``Engineer`` then defines the ``primary_language`` |
---|
321 | column, the column is added to the ``people`` table so that it is included in the mapping |
---|
322 | for ``Engineer`` and is also part of the table's full set of columns. |
---|
323 | Columns which are not mapped to ``Person`` are also excluded from any other |
---|
324 | single or joined inheriting classes using the ``exclude_properties`` mapper argument. |
---|
325 | Below, ``Manager`` will have all the attributes of ``Person`` and ``Manager`` but *not* |
---|
326 | the ``primary_language`` attribute of ``Engineer``:: |
---|
327 | |
---|
328 | class Manager(Person): |
---|
329 | __mapper_args__ = {'polymorphic_identity': 'manager'} |
---|
330 | golf_swing = Column(String(50)) |
---|
331 | |
---|
332 | The attribute exclusion logic is provided by the ``exclude_properties`` mapper argument, |
---|
333 | and declarative's default behavior can be disabled by passing an explicit |
---|
334 | ``exclude_properties`` collection (empty or otherwise) to the ``__mapper_args__``. |
---|
335 | |
---|
336 | Concrete Table Inheritance |
---|
337 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
---|
338 | |
---|
339 | Concrete is defined as a subclass which has its own table and sets the |
---|
340 | ``concrete`` keyword argument to ``True``:: |
---|
341 | |
---|
342 | class Person(Base): |
---|
343 | __tablename__ = 'people' |
---|
344 | id = Column(Integer, primary_key=True) |
---|
345 | name = Column(String(50)) |
---|
346 | |
---|
347 | class Engineer(Person): |
---|
348 | __tablename__ = 'engineers' |
---|
349 | __mapper_args__ = {'concrete':True} |
---|
350 | id = Column(Integer, primary_key=True) |
---|
351 | primary_language = Column(String(50)) |
---|
352 | name = Column(String(50)) |
---|
353 | |
---|
354 | Usage of an abstract base class is a little less straightforward as it requires |
---|
355 | usage of :func:`~sqlalchemy.orm.util.polymorphic_union`:: |
---|
356 | |
---|
357 | engineers = Table('engineers', Base.metadata, |
---|
358 | Column('id', Integer, primary_key=True), |
---|
359 | Column('name', String(50)), |
---|
360 | Column('primary_language', String(50)) |
---|
361 | ) |
---|
362 | managers = Table('managers', Base.metadata, |
---|
363 | Column('id', Integer, primary_key=True), |
---|
364 | Column('name', String(50)), |
---|
365 | Column('golf_swing', String(50)) |
---|
366 | ) |
---|
367 | |
---|
368 | punion = polymorphic_union({ |
---|
369 | 'engineer':engineers, |
---|
370 | 'manager':managers |
---|
371 | }, 'type', 'punion') |
---|
372 | |
---|
373 | class Person(Base): |
---|
374 | __table__ = punion |
---|
375 | __mapper_args__ = {'polymorphic_on':punion.c.type} |
---|
376 | |
---|
377 | class Engineer(Person): |
---|
378 | __table__ = engineers |
---|
379 | __mapper_args__ = {'polymorphic_identity':'engineer', 'concrete':True} |
---|
380 | |
---|
381 | class Manager(Person): |
---|
382 | __table__ = managers |
---|
383 | __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True} |
---|
384 | |
---|
385 | Class Usage |
---|
386 | =========== |
---|
387 | |
---|
388 | As a convenience feature, the :func:`declarative_base` sets a default |
---|
389 | constructor on classes which takes keyword arguments, and assigns them to the |
---|
390 | named attributes:: |
---|
391 | |
---|
392 | e = Engineer(primary_language='python') |
---|
393 | |
---|
394 | Note that ``declarative`` has no integration built in with sessions, and is |
---|
395 | only intended as an optional syntax for the regular usage of mappers and Table |
---|
396 | objects. A typical application setup using :func:`~sqlalchemy.orm.scoped_session` might look |
---|
397 | like:: |
---|
398 | |
---|
399 | engine = create_engine('postgres://scott:tiger@localhost/test') |
---|
400 | Session = scoped_session(sessionmaker(autocommit=False, |
---|
401 | autoflush=False, |
---|
402 | bind=engine)) |
---|
403 | Base = declarative_base() |
---|
404 | |
---|
405 | Mapped instances then make usage of :class:`~sqlalchemy.orm.session.Session` in the usual way. |
---|
406 | |
---|
407 | """ |
---|
408 | |
---|
409 | from sqlalchemy.schema import Table, Column, MetaData |
---|
410 | from sqlalchemy.orm import synonym as _orm_synonym, mapper, comparable_property, class_mapper |
---|
411 | from sqlalchemy.orm.interfaces import MapperProperty |
---|
412 | from sqlalchemy.orm.properties import PropertyLoader, ColumnProperty |
---|
413 | from sqlalchemy.orm.util import _is_mapped_class |
---|
414 | from sqlalchemy import util, exceptions |
---|
415 | from sqlalchemy.sql import util as sql_util |
---|
416 | |
---|
417 | |
---|
418 | __all__ = 'declarative_base', 'synonym_for', 'comparable_using', 'instrument_declarative' |
---|
419 | |
---|
420 | def instrument_declarative(cls, registry, metadata): |
---|
421 | """Given a class, configure the class declaratively, |
---|
422 | using the given registry (any dictionary) and MetaData object. |
---|
423 | This operation does not assume any kind of class hierarchy. |
---|
424 | |
---|
425 | """ |
---|
426 | if '_decl_class_registry' in cls.__dict__: |
---|
427 | raise exceptions.InvalidRequestError("Class %r already has been instrumented declaratively" % cls) |
---|
428 | cls._decl_class_registry = registry |
---|
429 | cls.metadata = metadata |
---|
430 | _as_declarative(cls, cls.__name__, cls.__dict__) |
---|
431 | |
---|
432 | def _as_declarative(cls, classname, dict_): |
---|
433 | cls._decl_class_registry[classname] = cls |
---|
434 | our_stuff = util.OrderedDict() |
---|
435 | for k in dict_: |
---|
436 | value = dict_[k] |
---|
437 | if (isinstance(value, tuple) and len(value) == 1 and |
---|
438 | isinstance(value[0], (Column, MapperProperty))): |
---|
439 | util.warn("Ignoring declarative-like tuple value of attribute " |
---|
440 | "%s: possibly a copy-and-paste error with a comma " |
---|
441 | "left at the end of the line?" % k) |
---|
442 | continue |
---|
443 | if not isinstance(value, (Column, MapperProperty)): |
---|
444 | continue |
---|
445 | prop = _deferred_relation(cls, value) |
---|
446 | our_stuff[k] = prop |
---|
447 | |
---|
448 | # set up attributes in the order they were created |
---|
449 | our_stuff.sort(key=lambda key: our_stuff[key]._creation_order) |
---|
450 | |
---|
451 | # extract columns from the class dict |
---|
452 | cols = [] |
---|
453 | for key, c in our_stuff.iteritems(): |
---|
454 | if isinstance(c, ColumnProperty): |
---|
455 | for col in c.columns: |
---|
456 | if isinstance(col, Column) and col.table is None: |
---|
457 | _undefer_column_name(key, col) |
---|
458 | cols.append(col) |
---|
459 | elif isinstance(c, Column): |
---|
460 | _undefer_column_name(key, c) |
---|
461 | cols.append(c) |
---|
462 | # if the column is the same name as the key, |
---|
463 | # remove it from the explicit properties dict. |
---|
464 | # the normal rules for assigning column-based properties |
---|
465 | # will take over, including precedence of columns |
---|
466 | # in multi-column ColumnProperties. |
---|
467 | if key == c.key: |
---|
468 | del our_stuff[key] |
---|
469 | |
---|
470 | table = None |
---|
471 | if '__table__' not in cls.__dict__: |
---|
472 | if '__tablename__' in cls.__dict__: |
---|
473 | tablename = cls.__tablename__ |
---|
474 | |
---|
475 | table_args = cls.__dict__.get('__table_args__') |
---|
476 | if isinstance(table_args, dict): |
---|
477 | args, table_kw = (), table_args |
---|
478 | elif isinstance(table_args, tuple): |
---|
479 | args = table_args[0:-1] |
---|
480 | table_kw = table_args[-1] |
---|
481 | if len(table_args) < 2 or not isinstance(table_kw, dict): |
---|
482 | raise exceptions.ArgumentError( |
---|
483 | "Tuple form of __table_args__ is " |
---|
484 | "(arg1, arg2, arg3, ..., {'kw1':val1, 'kw2':val2, ...})" |
---|
485 | ) |
---|
486 | else: |
---|
487 | args, table_kw = (), {} |
---|
488 | |
---|
489 | autoload = cls.__dict__.get('__autoload__') |
---|
490 | if autoload: |
---|
491 | table_kw['autoload'] = True |
---|
492 | |
---|
493 | cls.__table__ = table = Table(tablename, cls.metadata, |
---|
494 | *(tuple(cols) + tuple(args)), **table_kw) |
---|
495 | else: |
---|
496 | table = cls.__table__ |
---|
497 | if cols: |
---|
498 | for c in cols: |
---|
499 | if not table.c.contains_column(c): |
---|
500 | raise exceptions.ArgumentError("Can't add additional column %r when specifying __table__" % key) |
---|
501 | |
---|
502 | mapper_args = getattr(cls, '__mapper_args__', {}) |
---|
503 | if 'inherits' not in mapper_args: |
---|
504 | for c in cls.__bases__: |
---|
505 | if _is_mapped_class(c): |
---|
506 | mapper_args['inherits'] = cls._decl_class_registry.get(c.__name__, None) |
---|
507 | break |
---|
508 | |
---|
509 | if hasattr(cls, '__mapper_cls__'): |
---|
510 | mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__) |
---|
511 | else: |
---|
512 | mapper_cls = mapper |
---|
513 | |
---|
514 | if not table and 'inherits' not in mapper_args: |
---|
515 | raise exceptions.InvalidRequestError("Class %r does not have a __table__ or __tablename__ " |
---|
516 | "specified and does not inherit from an existing table-mapped class." % cls) |
---|
517 | |
---|
518 | elif 'inherits' in mapper_args and not mapper_args.get('concrete', False): |
---|
519 | inherited_mapper = class_mapper(mapper_args['inherits'], compile=False) |
---|
520 | inherited_table = inherited_mapper.local_table |
---|
521 | if 'inherit_condition' not in mapper_args and table: |
---|
522 | # figure out the inherit condition with relaxed rules |
---|
523 | # about nonexistent tables, to allow for ForeignKeys to |
---|
524 | # not-yet-defined tables (since we know for sure that our |
---|
525 | # parent table is defined within the same MetaData) |
---|
526 | mapper_args['inherit_condition'] = sql_util.join_condition( |
---|
527 | mapper_args['inherits'].__table__, table, |
---|
528 | ignore_nonexistent_tables=True) |
---|
529 | |
---|
530 | if not table: |
---|
531 | # single table inheritance. |
---|
532 | # ensure no table args |
---|
533 | table_args = cls.__dict__.get('__table_args__') |
---|
534 | if table_args is not None: |
---|
535 | raise exceptions.ArgumentError("Can't place __table_args__ on an inherited class with no table.") |
---|
536 | |
---|
537 | # add any columns declared here to the inherited table. |
---|
538 | for c in cols: |
---|
539 | if c.primary_key: |
---|
540 | raise exceptions.ArgumentError("Can't place primary key columns on an inherited class with no table.") |
---|
541 | inherited_table.append_column(c) |
---|
542 | |
---|
543 | # single or joined inheritance |
---|
544 | # exclude any cols on the inherited table which are not mapped on the parent class, to avoid |
---|
545 | # mapping columns specific to sibling/nephew classes |
---|
546 | inherited_mapper = class_mapper(mapper_args['inherits'], compile=False) |
---|
547 | inherited_table = inherited_mapper.local_table |
---|
548 | |
---|
549 | if 'exclude_properties' not in mapper_args: |
---|
550 | mapper_args['exclude_properties'] = exclude_properties = \ |
---|
551 | set([c.key for c in inherited_table.c if c not in inherited_mapper._columntoproperty]) |
---|
552 | exclude_properties.difference_update([c.key for c in cols]) |
---|
553 | |
---|
554 | cls.__mapper__ = mapper_cls(cls, table, properties=our_stuff, **mapper_args) |
---|
555 | |
---|
556 | class DeclarativeMeta(type): |
---|
557 | def __init__(cls, classname, bases, dict_): |
---|
558 | if '_decl_class_registry' in cls.__dict__: |
---|
559 | return type.__init__(cls, classname, bases, dict_) |
---|
560 | |
---|
561 | _as_declarative(cls, classname, dict_) |
---|
562 | return type.__init__(cls, classname, bases, dict_) |
---|
563 | |
---|
564 | def __setattr__(cls, key, value): |
---|
565 | if '__mapper__' in cls.__dict__: |
---|
566 | if isinstance(value, Column): |
---|
567 | _undefer_column_name(key, value) |
---|
568 | cls.__table__.append_column(value) |
---|
569 | cls.__mapper__.add_property(key, value) |
---|
570 | elif isinstance(value, ColumnProperty): |
---|
571 | for col in value.columns: |
---|
572 | if isinstance(col, Column) and col.table is None: |
---|
573 | _undefer_column_name(key, col) |
---|
574 | cls.__table__.append_column(col) |
---|
575 | cls.__mapper__.add_property(key, value) |
---|
576 | elif isinstance(value, MapperProperty): |
---|
577 | cls.__mapper__.add_property(key, _deferred_relation(cls, value)) |
---|
578 | else: |
---|
579 | type.__setattr__(cls, key, value) |
---|
580 | else: |
---|
581 | type.__setattr__(cls, key, value) |
---|
582 | |
---|
583 | |
---|
584 | class _GetColumns(object): |
---|
585 | def __init__(self, cls): |
---|
586 | self.cls = cls |
---|
587 | def __getattr__(self, key): |
---|
588 | mapper = class_mapper(self.cls, compile=False) |
---|
589 | if not mapper: |
---|
590 | return getattr(self.cls, key) |
---|
591 | else: |
---|
592 | prop = mapper.get_property(key) |
---|
593 | if not isinstance(prop, ColumnProperty): |
---|
594 | raise exceptions.InvalidRequestError("Property %r is not an instance of ColumnProperty (i.e. does not correspnd directly to a Column)." % key) |
---|
595 | return prop.columns[0] |
---|
596 | |
---|
597 | |
---|
598 | def _deferred_relation(cls, prop): |
---|
599 | def resolve_arg(arg): |
---|
600 | import sqlalchemy |
---|
601 | |
---|
602 | def access_cls(key): |
---|
603 | if key in cls._decl_class_registry: |
---|
604 | return _GetColumns(cls._decl_class_registry[key]) |
---|
605 | elif key in cls.metadata.tables: |
---|
606 | return cls.metadata.tables[key] |
---|
607 | else: |
---|
608 | return sqlalchemy.__dict__[key] |
---|
609 | |
---|
610 | d = util.PopulateDict(access_cls) |
---|
611 | def return_cls(): |
---|
612 | try: |
---|
613 | x = eval(arg, globals(), d) |
---|
614 | |
---|
615 | if isinstance(x, _GetColumns): |
---|
616 | return x.cls |
---|
617 | else: |
---|
618 | return x |
---|
619 | except NameError, n: |
---|
620 | raise exceptions.InvalidRequestError( |
---|
621 | "When compiling mapper %s, expression %r failed to locate a name (%r). " |
---|
622 | "If this is a class name, consider adding this relation() to the %r " |
---|
623 | "class after both dependent classes have been defined." % ( |
---|
624 | prop.parent, arg, n.args[0], cls)) |
---|
625 | return return_cls |
---|
626 | |
---|
627 | if isinstance(prop, PropertyLoader): |
---|
628 | for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_foreign_keys', 'remote_side'): |
---|
629 | v = getattr(prop, attr) |
---|
630 | if isinstance(v, basestring): |
---|
631 | setattr(prop, attr, resolve_arg(v)) |
---|
632 | |
---|
633 | if prop.backref: |
---|
634 | for attr in ('primaryjoin', 'secondaryjoin', 'secondary', 'foreign_keys', 'remote_side', 'order_by'): |
---|
635 | if attr in prop.backref.kwargs and isinstance(prop.backref.kwargs[attr], basestring): |
---|
636 | prop.backref.kwargs[attr] = resolve_arg(prop.backref.kwargs[attr]) |
---|
637 | |
---|
638 | |
---|
639 | return prop |
---|
640 | |
---|
641 | def synonym_for(name, map_column=False): |
---|
642 | """Decorator, make a Python @property a query synonym for a column. |
---|
643 | |
---|
644 | A decorator version of :func:`~sqlalchemy.orm.synonym`. The function being |
---|
645 | decorated is the 'descriptor', otherwise passes its arguments through |
---|
646 | to synonym():: |
---|
647 | |
---|
648 | @synonym_for('col') |
---|
649 | @property |
---|
650 | def prop(self): |
---|
651 | return 'special sauce' |
---|
652 | |
---|
653 | The regular ``synonym()`` is also usable directly in a declarative setting |
---|
654 | and may be convenient for read/write properties:: |
---|
655 | |
---|
656 | prop = synonym('col', descriptor=property(_read_prop, _write_prop)) |
---|
657 | |
---|
658 | """ |
---|
659 | def decorate(fn): |
---|
660 | return _orm_synonym(name, map_column=map_column, descriptor=fn) |
---|
661 | return decorate |
---|
662 | |
---|
663 | def comparable_using(comparator_factory): |
---|
664 | """Decorator, allow a Python @property to be used in query criteria. |
---|
665 | |
---|
666 | A decorator front end to :func:`~sqlalchemy.orm.comparable_property`, passes |
---|
667 | through the comparator_factory and the function being decorated:: |
---|
668 | |
---|
669 | @comparable_using(MyComparatorType) |
---|
670 | @property |
---|
671 | def prop(self): |
---|
672 | return 'special sauce' |
---|
673 | |
---|
674 | The regular ``comparable_property()`` is also usable directly in a |
---|
675 | declarative setting and may be convenient for read/write properties:: |
---|
676 | |
---|
677 | prop = comparable_property(MyComparatorType) |
---|
678 | |
---|
679 | """ |
---|
680 | def decorate(fn): |
---|
681 | return comparable_property(comparator_factory, fn) |
---|
682 | return decorate |
---|
683 | |
---|
684 | def _declarative_constructor(self, **kwargs): |
---|
685 | """A simple constructor that allows initialization from kwargs. |
---|
686 | |
---|
687 | Sets kwargs on the constructed instance. Only keys that are present as |
---|
688 | attributes of type(self) are allowed (for example, any mapped column or |
---|
689 | relation). |
---|
690 | |
---|
691 | """ |
---|
692 | for k in kwargs: |
---|
693 | if not hasattr(type(self), k): |
---|
694 | raise TypeError( |
---|
695 | "%r is an invalid keyword argument for %s" % |
---|
696 | (k, type(self).__name__)) |
---|
697 | setattr(self, k, kwargs[k]) |
---|
698 | _declarative_constructor.__name__ = '__init__' |
---|
699 | |
---|
700 | def declarative_base(bind=None, metadata=None, mapper=None, cls=object, |
---|
701 | name='Base', constructor=_declarative_constructor, |
---|
702 | metaclass=DeclarativeMeta, engine=None): |
---|
703 | """Construct a base class for declarative class definitions. |
---|
704 | |
---|
705 | The new base class will be given a metaclass that invokes |
---|
706 | :func:`instrument_declarative()` upon each subclass definition, and routes |
---|
707 | later Column- and Mapper-related attribute assignments made on the class |
---|
708 | into Table and Mapper assignments. |
---|
709 | |
---|
710 | :param bind: An optional :class:`~sqlalchemy.engine.base.Connectable`, will be assigned |
---|
711 | the ``bind`` attribute on the :class:`~sqlalchemy.MetaData` instance. |
---|
712 | The `engine` keyword argument is a deprecated synonym for `bind`. |
---|
713 | |
---|
714 | :param metadata: |
---|
715 | An optional :class:`~sqlalchemy.MetaData` instance. All :class:`~sqlalchemy.schema.Table` |
---|
716 | objects implicitly declared by |
---|
717 | subclasses of the base will share this MetaData. A MetaData instance |
---|
718 | will be create if none is provided. The MetaData instance will be |
---|
719 | available via the `metadata` attribute of the generated declarative |
---|
720 | base class. |
---|
721 | |
---|
722 | :param mapper: |
---|
723 | An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will be |
---|
724 | used to map subclasses to their Tables. |
---|
725 | |
---|
726 | :param cls: |
---|
727 | Defaults to :class:`object`. A type to use as the base for the generated |
---|
728 | declarative base class. May be a type or tuple of types. |
---|
729 | |
---|
730 | :param name: |
---|
731 | Defaults to ``Base``. The display name for the generated |
---|
732 | class. Customizing this is not required, but can improve clarity in |
---|
733 | tracebacks and debugging. |
---|
734 | |
---|
735 | :param constructor: |
---|
736 | Defaults to declarative._declarative_constructor, an __init__ |
---|
737 | implementation that assigns \**kwargs for declared fields and relations |
---|
738 | to an instance. If ``None`` is supplied, no __init__ will be installed |
---|
739 | and construction will fall back to cls.__init__ with normal Python |
---|
740 | semantics. |
---|
741 | |
---|
742 | :param metaclass: |
---|
743 | Defaults to :class:`DeclarativeMeta`. A metaclass or __metaclass__ |
---|
744 | compatible callable to use as the meta type of the generated |
---|
745 | declarative base class. |
---|
746 | |
---|
747 | """ |
---|
748 | lcl_metadata = metadata or MetaData() |
---|
749 | if bind or engine: |
---|
750 | lcl_metadata.bind = bind or engine |
---|
751 | |
---|
752 | bases = not isinstance(cls, tuple) and (cls,) or cls |
---|
753 | class_dict = dict(_decl_class_registry=dict(), |
---|
754 | metadata=lcl_metadata) |
---|
755 | |
---|
756 | if constructor: |
---|
757 | class_dict['__init__'] = constructor |
---|
758 | if mapper: |
---|
759 | class_dict['__mapper_cls__'] = mapper |
---|
760 | |
---|
761 | return metaclass(name, bases, class_dict) |
---|
762 | |
---|
763 | def _undefer_column_name(key, column): |
---|
764 | if column.key is None: |
---|
765 | column.key = key |
---|
766 | if column.name is None: |
---|
767 | column.name = key |
---|