root / elixir / trunk / elixir / entity.py @ 16

Revision 14, 7.7 kB (checked in by ged, 6 years ago)

- updated/added comments.
- removed some unused code
- when an inverse of a relation is found, also check that it has not several

inverses. Since the inverse of a relation is not processed, it was possible
to have a relation with several inverse not raise an exception.

- first step in making autoload tables work: do not create a pk for

autoloaded tables

- save the genereated SQLAlchemy relation in the "property" variable for

many-to-many relationships (as for the other types of relations).
By the way: I wonder if this is useful.

- made m-to-m table names consistent (independant of

whether the relation or its inverse is setup first)

- m-to-m relations do not match as inverse of each other if the name of their

table is different

Line 
1"""
2    Entity baseclass, metaclass and descriptor
3"""
4
5from sqlalchemy                     import Table, Integer, desc
6from sqlalchemy.ext.assignmapper    import assign_mapper
7from supermodel.statements          import Statement
8from supermodel.fields              import Field
9
10import sys
11import supermodel
12
13
14__all__ = ['Entity']
15
16DEFAULT_AUTO_PRIMARYKEY_NAME = "id"
17DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer
18
19class Entity(object):
20   
21    """
22        The base class for all entities
23    """
24   
25    class __metaclass__(type):
26        def __init__(cls, name, bases, dict_):
27            try:
28                desc = cls._descriptor = EntityDescriptor(cls)
29                EntityDescriptor.current = desc
30            except NameError:
31                # happens only for the base class itself
32                #CHECKME: checking explicitely for the name 'Entity' seem
33                # cleaner to me because it's more explicit and we wouldn't
34                # rely on code position which is always subject to change
35                return 
36           
37            Statement.process(cls)
38
39            # setup misc options here (like tablename etc.)
40            desc.setup_options()
41           
42            # create table & assign (empty) mapper
43            desc.setup()
44           
45            # try to setup all uninitialized relationships
46            EntityDescriptor.setup_relationships()
47
48
49class EntityDescriptor(object):
50   
51    """
52        EntityDescriptor describes fields and options
53        that are needed for table creation
54    """
55   
56    uninitialized_rels = set()
57    current = None
58   
59    def __init__(self, entity):
60        self.entity = entity
61        self.primary_keys = list()
62        self.order_by = None
63        self.fields = dict()
64        self.relationships = dict()
65        self.constraints = list()
66        self.module = sys.modules[entity.__module__]
67
68        #CHECKME: this is a workaround for the "current" descriptor/target
69        # property ugliness. The problem is that this workaround is ugly too.
70        # I'm not sure if this is a safe practice. It works but...?
71#        setattr(self.module, entity.__name__, entity)
72        self.metadata = getattr(self.module, 'metadata', supermodel.metadata)
73        self.autoload = None
74        self.auto_primarykey = True
75        self.shortnames = False
76        self.tablename = None
77        self.extension = None
78       
79        entity.table = None
80        entity.mapper = None
81   
82    def setup_options(self):
83        """
84            Setup any values that might depend on using_options,
85            for now only the tablename
86        """
87        entity = self.entity
88       
89        if not self.tablename:
90            if self.shortnames:
91                self.tablename = entity.__name__.lower()
92            else:
93                modulename = entity.__module__.replace('.', '_')
94                tablename = "%s_%s" % (modulename, entity.__name__)
95                self.tablename = tablename.lower()
96   
97    def setup(self):
98        """
99            Create tables, keys, columns that have been specified so far
100            and assign a mapper. Will be called when an instance of the
101            entity is created or a mapper is needed to access one or many
102            instances of the entity. This *doesn't* initialize relations.
103        """
104       
105        self.setup_mapper()
106       
107        # This marks all relations of the entity (or, at least those which
108        # have been added so far by statements) as being uninitialized
109        EntityDescriptor.uninitialized_rels.update(
110            self.relationships.values())
111   
112    def setup_mapper(self):
113        """
114            Initializes and assign an (empty!) mapper to the given entity,
115            which needs a table defined, so it calls setup_table.
116        """
117        if self.entity.mapper:
118            return
119       
120        session = getattr(self.module, 'session', supermodel.objectstore)
121        table = self.setup_table()
122       
123        kwargs = dict()
124        if self.order_by:
125            if isinstance(self.order_by, basestring):
126                self.order_by = [self.order_by]
127           
128            order = list()
129            for field in self.order_by:
130                col = self.fields[field.strip('-')].column
131                if field.startswith('-'):
132                    col = desc(col)
133                order.append(col)
134           
135            kwargs['order_by'] = order
136       
137        if self.extension:
138            kwargs['extension'] = self.extension
139       
140        assign_mapper(session.context, self.entity, table, **kwargs)
141        supermodel.metadatas.add(self.metadata)
142   
143    def setup_table(self):
144        """
145            Create a SQLAlchemy table-object with all columns that have
146            been defined up to this point, which excludes
147        """
148        if self.entity.table:
149            return
150       
151        if not self.autoload:
152            if not self.primary_keys and self.auto_primarykey:
153                self.create_auto_primary_key()
154
155        # create list of columns and constraints
156        args = [field.column for field in self.fields.values()] \
157                    + self.constraints
158       
159        # specify options
160        kwargs = dict()
161       
162        if self.autoload:
163            kwargs['autoload'] = True
164       
165        table = Table(self.tablename, self.metadata, *args, **kwargs)
166        self.entity.table = table
167        return table
168   
169    def create_auto_primary_key(self):
170        """
171            Creates a primary key
172        """
173        assert not self.primary_keys and self.auto_primarykey
174       
175        if isinstance(self.auto_primarykey, basestring):
176            colname = self.auto_primarykey
177        else:
178            colname = DEFAULT_AUTO_PRIMARYKEY_NAME
179       
180        self.add_field(Field(DEFAULT_AUTO_PRIMARYKEY_TYPE,
181                             colname=colname, primary_key=True))
182       
183    def add_field(self, field):
184        self.fields[field.colname] = field
185       
186        if field.primary_key:
187            self.primary_keys.append(field)
188       
189        table = self.entity.table
190        if table:
191            table.append_column(field.column)
192
193    #FIXME: to remove. it's better to just use SA directly
194    def add_constraint(self, constraint):
195        self.constraints.append(constraint)
196       
197        table = self.entity.table
198        if table:
199            table.append_constraint(constraint)
200       
201    def get_inverse_relation(self, rel, reverse=False):
202        """Return the inverse relation of rel, if any, None otherwise."""
203
204        matching_rel = None
205        for other_rel in self.relationships.itervalues():
206            if other_rel.is_inverse(rel):
207                if matching_rel is None:
208                    matching_rel = other_rel
209                else:
210                    raise Exception(
211                            "Several relations match as inverse of the '%s' "
212                            "relation in class '%s'. You should specify "
213                            "inverse relations manually by using the inverse "
214                            "keyword."
215                            % (rel.name, rel.entity.__name__) 
216                          )
217        # When a matching inverse is found, we check that it has only
218        # one relation matching as its own inverse. We don't need the result
219        # of the method though. But we do need to be careful not to start an
220        # infinite recursive loop.
221        if matching_rel and not reverse:
222            rel.entity._descriptor.get_inverse_relation(matching_rel, True)
223           
224        return matching_rel
225
226
227    @classmethod
228    def setup_relationships(cls):
229        for relationship in list(EntityDescriptor.uninitialized_rels):
230            if relationship.setup():
231                EntityDescriptor.uninitialized_rels.remove(relationship)
Note: See TracBrowser for help on using the browser.