1 # Licensed to the Apache Software Foundation (ASF) under one or more
2 # contributor license agreements. See the NOTICE file distributed with
3 # this work for additional information regarding copyright ownership.
4 # The ASF licenses this file to You under the Apache License, Version 2.0
5 # (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 ARIA modeling relationship module
20 # pylint: disable=invalid-name, redefined-outer-name
22 from sqlalchemy.orm import relationship, backref
23 from sqlalchemy.orm.collections import attribute_mapped_collection
24 from sqlalchemy.ext.associationproxy import association_proxy as original_association_proxy
25 from sqlalchemy import (
32 from ..utils import formatting
34 NO_BACK_POP = 'NO_BACK_POP'
37 def foreign_key(other_table, nullable=False):
39 Declare a foreign key property, which will also create a foreign key column in the table with
40 the name of the property. By convention the property name should end in "_fk".
42 You are required to explicitly create foreign keys in order to allow for one-to-one,
43 one-to-many, and many-to-one relationships (but not for many-to-many relationships). If you do
44 not do so, SQLAlchemy will fail to create the relationship property and raise an exception with
45 a clear error message.
47 You should normally not have to access this property directly, but instead use the associated
48 relationship properties.
50 *This utility method should only be used during class creation.*
52 :param other_table: other table name
53 :type other_table: basestring
54 :param nullable: ``True`` to allow null values (meaning that there is no relationship)
58 return Column(Integer,
59 ForeignKey('{table}.id'.format(table=other_table), ondelete='CASCADE'),
63 def one_to_one_self(model_class, fk):
65 Declare a one-to-one relationship property. The property value would be an instance of the same
68 You will need an associated foreign key to our own table.
70 *This utility method should only be used during class creation.*
72 :param model_class: class in which this relationship will be declared
73 :type model_class: type
74 :param fk: foreign key name
78 remote_side = '{model_class}.{remote_column}'.format(
79 model_class=model_class.__name__,
80 remote_column=model_class.id_column_name()
83 primaryjoin = '{remote_side} == {model_class}.{column}'.format(
84 remote_side=remote_side,
85 model_class=model_class.__name__,
90 model_class.__tablename__,
92 'primaryjoin': primaryjoin,
93 'remote_side': remote_side,
99 def one_to_one(model_class,
103 back_populates=None):
105 Declare a one-to-one relationship property. The property value would be an instance of the other
108 You have two options for the foreign key. Either this table can have an associated key to the
109 other table (use the ``fk`` argument) or the other table can have an associated foreign key to
110 this our table (use the ``other_fk`` argument).
112 *This utility method should only be used during class creation.*
114 :param model_class: class in which this relationship will be declared
115 :type model_class: type
116 :param other_table: other table name
117 :type other_table: basestring
118 :param fk: foreign key name at our table (no need specify if there's no ambiguity)
120 :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity)
121 :type other_fk: basestring
122 :param back_populates: override name of matching many-to-many property at other table; set to
124 :type back_populates: basestring or bool
126 backref_kwargs = None
127 if back_populates is not NO_BACK_POP:
128 if back_populates is None:
129 back_populates = model_class.__tablename__
130 backref_kwargs = {'name': back_populates, 'uselist': False}
131 back_populates = None
133 return _relationship(model_class,
136 back_populates=back_populates,
137 backref_kwargs=backref_kwargs,
141 def one_to_many(model_class,
149 Declare a one-to-many relationship property. The property value would be a list or dict of
150 instances of the child table's model.
152 The child table will need an associated foreign key to our table.
154 The declaration will automatically create a matching many-to-one property at the child model,
155 named after our table name. Use the ``child_property`` argument to override this name.
157 *This utility method should only be used during class creation.*
159 :param model_class: class in which this relationship will be declared
160 :type model_class: type
161 :param other_table: other table name
162 :type other_table: basestring
163 :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity)
164 :type other_fk: basestring
165 :param dict_key: if set the value will be a dict with this key as the dict key; otherwise will
167 :type dict_key: basestring
168 :param back_populates: override name of matching many-to-one property at other table; set to
170 :type back_populates: basestring or bool
171 :param rel_kwargs: additional relationship kwargs to be used by SQLAlchemy
172 :type rel_kwargs: dict
173 :param self: used for relationships between a table and itself. if set, other_table will
174 become the same as the source table.
177 relationship_kwargs = rel_kwargs or {}
180 other_table_name = model_class.__tablename__
181 back_populates = False
182 relationship_kwargs['remote_side'] = '{model}.{column}'.format(model=model_class.__name__,
187 other_table_name = other_table
188 if back_populates is None:
189 back_populates = model_class.__tablename__
190 relationship_kwargs.setdefault('cascade', 'all')
192 return _relationship(
195 back_populates=back_populates,
198 relationship_kwargs=relationship_kwargs)
201 def many_to_one(model_class,
205 back_populates=None):
207 Declare a many-to-one relationship property. The property value would be an instance of the
208 parent table's model.
210 You will need an associated foreign key to the parent table.
212 The declaration will automatically create a matching one-to-many property at the child model,
213 named after the plural form of our table name. Use the ``parent_property`` argument to override
214 this name. Note: the automatic property will always be a SQLAlchemy query object; if you need a
215 Python collection then use :func:`one_to_many` at that model.
217 *This utility method should only be used during class creation.*
219 :param model_class: class in which this relationship will be declared
220 :type model_class: type
221 :param parent_table: parent table name
222 :type parent_table: basestring
223 :param fk: foreign key name at our table (no need specify if there's no ambiguity)
225 :param back_populates: override name of matching one-to-many property at parent table; set to
227 :type back_populates: basestring or bool
229 if back_populates is None:
230 back_populates = formatting.pluralize(model_class.__tablename__)
232 return _relationship(model_class,
234 back_populates=back_populates,
239 def many_to_many(model_class,
246 Declare a many-to-many relationship property. The property value would be a list or dict of
247 instances of the other table's model.
249 You do not need associated foreign keys for this relationship. Instead, an extra table will be
252 The declaration will automatically create a matching many-to-many property at the other model,
253 named after the plural form of our table name. Use the ``other_property`` argument to override
254 this name. Note: the automatic property will always be a SQLAlchemy query object; if you need a
255 Python collection then use :func:`many_to_many` again at that model.
257 *This utility method should only be used during class creation.*
259 :param model_class: class in which this relationship will be declared
260 :type model_class: type
261 :param other_table: parent table name
262 :type other_table: basestring
263 :param prefix: optional prefix for extra table name as well as for ``other_property``
264 :type prefix: basestring
265 :param dict_key: if set the value will be a dict with this key as the dict key; otherwise will
267 :type dict_key: basestring
268 :param other_property: override name of matching many-to-many property at other table; set to
270 :type other_property: basestring or bool
271 :param self: used for relationships between a table and itself. if set, other_table will
272 become the same as the source table.
276 this_table = model_class.__tablename__
277 this_column_name = '{0}_id'.format(this_table)
278 this_foreign_key = '{0}.id'.format(this_table)
281 other_table = this_table
283 other_column_name = '{0}_{1}'.format(other_table, 'self_ref_id' if self else 'id')
284 other_foreign_key = '{0}.{1}'.format(other_table, 'id')
286 secondary_table_name = '{0}_{1}'.format(this_table, other_table)
288 if prefix is not None:
289 secondary_table_name = '{0}_{1}'.format(prefix, secondary_table_name)
290 if other_property is None:
291 other_property = '{0}_{1}'.format(prefix, formatting.pluralize(this_table))
293 secondary_table = _get_secondary_table(
294 model_class.metadata,
295 secondary_table_name,
302 kwargs = {'relationship_kwargs': {'secondary': secondary_table}}
305 kwargs['back_populates'] = NO_BACK_POP
306 kwargs['relationship_kwargs']['primaryjoin'] = \
307 getattr(model_class, 'id') == getattr(secondary_table.c, this_column_name)
308 kwargs['relationship_kwargs']['secondaryjoin'] = \
309 getattr(model_class, 'id') == getattr(secondary_table.c, other_column_name)
311 kwargs['backref_kwargs'] = \
312 {'name': other_property, 'uselist': True} if other_property else None
313 kwargs['dict_key'] = dict_key
315 return _relationship(model_class, other_table, **kwargs)
318 def association_proxy(*args, **kwargs):
320 type_ = kwargs.get('type')
323 type_ = ':obj:`basestring`'
324 proxy = original_association_proxy(*args, **kwargs)
326 Internal. For use in SQLAlchemy queries.
333 def _relationship(model_class,
337 relationship_kwargs=None,
341 relationship_kwargs = relationship_kwargs or {}
344 relationship_kwargs.setdefault(
346 lambda: getattr(_get_class_for_table(model_class, model_class.__tablename__), fk)
350 relationship_kwargs.setdefault(
352 lambda: getattr(_get_class_for_table(model_class, other_table_name), other_fk)
356 relationship_kwargs.setdefault('collection_class',
357 attribute_mapped_collection(dict_key))
360 assert back_populates is None
362 lambda: _get_class_for_table(model_class, other_table_name),
363 backref=backref(**backref_kwargs),
364 **relationship_kwargs
367 if back_populates is not NO_BACK_POP:
368 relationship_kwargs['back_populates'] = back_populates
369 return relationship(lambda: _get_class_for_table(model_class, other_table_name),
370 **relationship_kwargs)
373 def _get_class_for_table(model_class, tablename):
374 if tablename in (model_class.__name__, model_class.__tablename__):
377 for table_cls in model_class._decl_class_registry.itervalues():
378 if tablename == getattr(table_cls, '__tablename__', None):
381 raise ValueError('unknown table: {0}'.format(tablename))
384 def _get_secondary_table(metadata,
393 Column(first_column, Integer, ForeignKey(first_foreign_key)),
394 Column(second_column, Integer, ForeignKey(second_foreign_key))