root / supermodel / trunk / supermodel / relationships.py @ 7

Revision 7, 11.5 kB (checked in by ged, 6 years ago)

- General cleanup. Added more comments
- removed systematic post_update. It should now be specified manually for the

relations which need it

- table names are now always lowercase, whether we use the shortname option or

not.

- slightly nicer names for columns of the intermediate table of many-to-many

relationships (they are now named as xxx1 and xxx2 instead of xxx and xxx2

- added test when there are multiple self references, to test the remote_side

argument.

Line 
1import sys
2from sqlalchemy import relation, ForeignKeyConstraint, Column, Table, \
3                       backref, class_mapper, and_
4from sqlalchemy.util import to_set
5from supermodel.statements import Statement
6from supermodel.fields import Field
7from supermodel.entity import EntityDescriptor
8
9
10class Relationship(object):
11    """
12        Base class for relationships
13    """
14   
15    def __init__(self, entity, name, *args, **kwargs):
16        self.name = name
17        self.of_kind = kwargs.pop('of_kind')
18        self.inverse_name = kwargs.pop('inverse', None)
19       
20        self.entity = entity
21        self._target = None
22       
23        self.initialized = False
24        self.secondary = None
25        self._inverse = None
26        self.foreign_key = None
27       
28        self.foreign_key = kwargs.pop('foreign_key', None)
29        if self.foreign_key and not isinstance(self.foreign_key, list):
30            self.foreign_key = [self.foreign_key]
31       
32        self.property = None # sqlalchemy property
33       
34        self.args = args
35        self.kwargs = kwargs
36       
37        #CHECKME: is this useful?
38        self.entity._descriptor.relationships[self.name] = self
39   
40    def create_keys(self):
41        """
42            Subclasses (ie. concrete relationships) may
43            override this method to create foreign keys
44        """
45        pass
46   
47    def create_tables(self):
48        """
49            Subclasses (ie. concrete relationships) may
50            override this method to create secondary tables
51        """
52        pass
53   
54    def create_properties(self):
55        """
56            Subclasses (ie. concrete relationships) may
57            override this method to add properties to the
58            involved entities
59        """
60        pass
61   
62    def setup(self):
63        """
64            Sets up the relationship, creates foreign keys
65            and secondary tables
66        """
67       
68        if not self.target:
69            return False
70       
71        self.create_keys()
72        self.create_tables()
73        self.create_properties()
74       
75        return True
76   
77    @property
78    def target(self):
79        if not self._target:
80            path = self.of_kind.rsplit('.', 1)
81            classname = path.pop()
82
83            # full qualified entity name?
84            if path:
85                module = sys.modules[path.pop()]
86            # if not, try the same module as the source
87            else: 
88                module = self.entity._descriptor.module
89           
90            try:
91                self._target = getattr(module, classname)
92            except AttributeError:
93                # This is ugly but we need it because the class which is
94                # currently being defined (we have to keep in mind we are in
95                # its metaclass code) is not yet available in the module
96                # namespace, so the getattr above fails. And unfortunately,
97                # this doesn't only happen for the owning entity of this
98                # relation since we might be setting up a deferred relation.
99                e = EntityDescriptor.current.entity
100                if classname == e.__name__ or \
101                        self.of_kind == e.__module__ +'.'+ e.__name__:
102                    self._target = e
103                else:
104                    return None
105       
106        return self._target
107   
108    @property
109    def inverse(self):
110        #TODO: we should use a different value for when an inverse was searched
111        # for but none was found than when it hasn't been searched for yet so
112        # that we don't do the whole search again
113        if not self._inverse:
114            if self.inverse_name:
115                desc = self.target._descriptor
116                inverse = desc.relationships[self.inverse_name]
117                assert self.match_type_of(inverse)
118            else:
119                inverse = self.target._descriptor.get_inverse_relation(self)
120       
121            if inverse:
122                self._inverse = inverse
123                inverse._inverse = self
124       
125        return self._inverse
126   
127    def match_type_of(self, other):
128        t1, t2 = type(self), type(other)
129   
130        if t1 is HasAndBelongsToMany:
131            return t1 is t2
132        elif t1 in (HasOne, HasMany):
133            return t2 is BelongsTo
134        elif t1 is BelongsTo:
135            return t2 in (HasMany, HasOne)
136        else:
137            return False
138
139    def is_inverse(self, other):
140        return other is not self and \
141               self.match_type_of(other) and \
142               self.entity == other.target and \
143               other.entity == self.target and \
144               (self.inverse_name == other.name or not self.inverse_name) and \
145               (other.inverse_name == self.name or not other.inverse_name)
146
147class BelongsTo(Relationship):
148    def create_keys(self):
149        """
150            Find all primary keys on the target and create
151            foreign keys on the source accordingly
152        """
153        source_desc = self.entity._descriptor
154        target_desc = self.target._descriptor
155       
156        if self.foreign_key:
157            self.foreign_key = [source_desc.fields[k]
158                                    for k in self.foreign_key 
159                                        if isinstance(k, basestring)]
160            return
161       
162        fk_refcols = list()
163        fk_colnames = list()
164
165        self.foreign_key = list()
166        self.primaryjoin_clauses = list()
167
168        for key in target_desc.primary_keys:
169            pk_col = key.column
170
171            colname = '%s_%s' % (self.name, pk_col.name)
172            # we use a Field here instead of using a Column directly
173            # because of add_field
174            field = Field(pk_col.type, colname=colname, index=True)
175            source_desc.add_field(field)
176
177            self.foreign_key.append(field)
178
179            # build the list of local columns which will be part of
180            # the foreign key
181            fk_colnames.append(colname)
182
183            # build the list of columns the foreign key will point to
184            fk_refcols.append(target_desc.tablename + '.' + pk_col.name)
185
186            # build up the primary join. This is needed when you have several
187            # belongs_to relations between two objects
188            self.primaryjoin_clauses.append(field.column == pk_col)
189       
190        # TODO: better constraint-naming?
191        #CHECKME: do we really need use_alter systematically?
192        source_desc.add_constraint(ForeignKeyConstraint(
193                                        fk_colnames, fk_refcols,
194                                        name=self.name +'_fk',
195                                        use_alter=True))
196   
197    def create_properties(self):
198        kwargs = self.kwargs
199       
200        if self.entity is self.target:
201            cols = [k.column for k in self.target._descriptor.primary_keys]
202            kwargs['remote_side'] = cols
203
204        kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
205        kwargs['uselist'] = False
206       
207        self.property = relation(self.target, **kwargs)
208        self.entity.mapper.add_property(self.name, self.property)
209
210
211class HasOne(Relationship):
212    uselist = False
213
214    def create_keys(self):
215        # make sure the inverse is set up because it creates the
216        # foreign key we'll need
217        self.inverse.setup()
218   
219    def create_properties(self):
220        kwargs = self.kwargs
221       
222        if self.entity is self.target:
223            kwargs['remote_side'] = [field.column
224                                        for field in self.inverse.foreign_key]
225       
226        kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
227        kwargs['uselist'] = self.uselist
228       
229        self.property = relation(self.target, **kwargs)
230        self.entity.mapper.add_property(self.name, self.property)
231
232
233class HasMany(HasOne):
234    uselist = True
235
236
237class HasAndBelongsToMany(Relationship):
238    def create_tables(self):
239        if self.inverse:
240            if self.inverse.secondary:
241                self.secondary = self.inverse.secondary
242                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
243                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
244
245        if not self.secondary:
246            e1_desc = self.entity._descriptor
247            e2_desc = self.target._descriptor
248           
249            columns = list()
250            constraints = list()
251
252            self.primaryjoin_clauses = list()
253            self.secondaryjoin_clauses = list()
254
255            for num, desc, join_name in (('1', e1_desc, 'primary'), 
256                                         ('2', e2_desc, 'secondary')):
257                fk_colnames = list()
258                fk_refcols = list()
259           
260                for key in desc.primary_keys:
261                    pk_col = key.column
262                   
263                    colname = '%s_%s' % (desc.tablename, pk_col.name)
264
265                    # In case we have a many-to-many self-reference, we need
266                    # to tweak the names of the columns so that we don't end
267                    # up with twice the same column name.
268                    if self.entity is self.target:
269                        colname += num
270
271                    col = Column(colname, pk_col.type)
272                    columns.append(col)
273
274                    # build the list of local columns which will be part of
275                    # the foreign key
276                    fk_colnames.append(colname)
277
278                    # build the list of columns the foreign key will point to
279                    fk_refcols.append(desc.tablename + '.' + pk_col.name)
280
281                    # build join clauses
282                    join_list = getattr(self, join_name+'join_clauses')
283                    join_list.append(col == pk_col)
284               
285                # TODO: better constraint-naming?
286                #CHECKME: do we really need use_alter systematically?
287                constraints.append(
288                    ForeignKeyConstraint(fk_colnames, fk_refcols,
289                                         name=desc.tablename + '_fk', 
290                                         use_alter=True))
291       
292            # In the table name code below, we use the name of the relation
293            # for the first entity (instead of the name of its primary key),
294            # so that we can have two many-to-many relations between the same
295            # objects without having a table name collision. On the other hand,
296            # we use the name of the primary key for the second entity
297            # (instead of the inverse relation's name) so that a many-to-many
298            # relation can be defined without inverse.
299            e2_pk_name = '_'.join([key.column.name for key in
300                                   e2_desc.primary_keys])
301            tablename = "%s_%s__%s_%s" % (e1_desc.tablename, self.name,
302                                          e2_desc.tablename, e2_pk_name)
303
304            args = columns + constraints
305            self.secondary = Table(tablename, e1_desc.metadata, *args)
306   
307    def create_properties(self):
308        kwargs = self.kwargs
309
310        if self.target is self.entity:
311            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
312            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
313
314        m = self.entity.mapper
315        #FIXME: using post_update systematically is *really* not good
316        m.add_property(self.name,
317                       relation(self.target, secondary=self.secondary,
318                                uselist=True, **kwargs))
319
320
321belongs_to = Statement(BelongsTo)
322has_one = Statement(HasOne)
323has_many = Statement(HasMany)
324has_and_belongs_to_many = Statement(HasAndBelongsToMany)
Note: See TracBrowser for help on using the browser.