From 63dfc50a54d360e409674d46db494ea2a7a3d11d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 18 Sep 2005 21:23:35 +0000 Subject: [PATCH] --- doc/build/components/formatting.myt | 2 +- doc/build/content/metadata.myt | 57 +++++++++++- doc/build/content/roadmap.myt | 8 +- lib/sqlalchemy/mapper.py | 78 ++++++++--------- lib/sqlalchemy/objectstore.py | 129 ++++++++++++++-------------- lib/sqlalchemy/schema.py | 1 + test/mapper.py | 2 +- test/objectstore.py | 6 +- 8 files changed, 169 insertions(+), 114 deletions(-) diff --git a/doc/build/components/formatting.myt b/doc/build/components/formatting.myt index c10f259e34..3ff0a2c91e 100644 --- a/doc/build/components/formatting.myt +++ b/doc/build/components/formatting.myt @@ -297,7 +297,7 @@ <%method code autoflush=False> <%args> title = None - syntaxtype = 'myghty' + syntaxtype = 'python' <%init> diff --git a/doc/build/content/metadata.myt b/doc/build/content/metadata.myt index 4468c776fc..159f4187ee 100644 --- a/doc/build/content/metadata.myt +++ b/doc/build/content/metadata.myt @@ -1,6 +1,61 @@ <%flags>inherit='document_base.myt' <&|doclib.myt:item, name="metadata", description="Database Meta Data" &> - <&|doclib.myt:item, name="tables", description="Describing Tables with Database Meta Data" &> + <&|doclib.myt:item, name="tables", description="Describing Tables with MetaData" &> +

The core of SQLAlchemy's query and object mapping operations is table metadata, which are Python objects that describe tables. Metadata objects can be created by explicitly naming the table and all its properties, using the Table, Column, and ForeignKey objects:

+ <&|formatting.myt:code&> + from sqlalchemy.schema import * + import sqlalchemy.sqlite as sqlite + engine = sqllite.engine(':memory:', {}) + + users = Table('users', engine, + Column('user_id', INTEGER, primary_key = True), + Column('user_name', VARCHAR(16), nullable = False), + Column('email_address', VARCHAR(60), key='email'), + Column('password', VARCHAR(20), nullable = False) + ) + + user_prefs = Table('user_prefs', engine, + Column('pref_id', INTEGER, primary_key = True), + Column('user_id', INTEGER, nullable = False, foreign_key = ForeignKey(users.c.user_id)) + Column('pref_name', VARCHAR(40), nullable = False), + Column('pref_value', VARCHAR(100)) + ) + + +

Metadata objects can also be reflected from tables that already exist in the database. Reflection means based on a table name, the names, datatypes, and attributes of all columns, including foreign keys, will be loaded automatically. This feature is supported by all database engines:

+ <&|formatting.myt:code&> + >>> messages = Table('messages', engine, autoload = True) + >>> [c.name for c in messages.columns] + ['message_id', 'message_name', 'date'] + + +

+ Note that if a reflected table has a foreign key referencing another table, then the metadata for the related table will be loaded as well, even if it has not been defined by the application: +

+ <&|formatting.myt:code&> + >>> shopping_cart_items = Table('shopping_cart_items', engine, autoload = True) + >>> print shopping_cart_items.c.cart_id.table.name + shopping_carts + +

To get direct access to 'shopping_carts', simply instantiate it via the Table constructor. You'll get the same instance of the shopping cart Table as the one that is attached to shopping_cart_items: + <&|formatting.myt:code&> + >>> shopping_carts = Table('shopping_carts', engine) + >>> shopping_carts is shopping_cart_items.c.cart_id.table.name + True + +

This works because when the Table constructor is called for a particular name and database engine, if the table has already been created then the instance returned will be the same as the original. This is a singleton constructor:

+ <&|formatting.myt:code&> + >>> news_articles = Table('news', engine, + ... Column('article_id', INTEGER, primary_key = True), + ... Column('url', VARCHAR(250), nullable = False) + ... ) + >>> othertable = Table('news', engine) + >>> othertable is news_articles + True + + + + <&|doclib.myt:item, name="building", description="Building and Dropping Database Tables" &> diff --git a/doc/build/content/roadmap.myt b/doc/build/content/roadmap.myt index deed372463..398e14fc19 100644 --- a/doc/build/content/roadmap.myt +++ b/doc/build/content/roadmap.myt @@ -13,13 +13,13 @@ Start | |------ Connection Pooling Configuration | | | | - |--- <&formatting.myt:link, path="dbengine_establishing" &> | + +--- <&formatting.myt:link, path="dbengine_establishing" &> | | | | | |--------- <&formatting.myt:link, path="dbengine_options" &> | | - |---- <&formatting.myt:link, path="metadata_tables" &> + +---- <&formatting.myt:link, path="metadata_tables" &> | | |---- <&formatting.myt:link, path="metadata_building" &> @@ -31,9 +31,9 @@ Start |---- Basic Data Mapping | | | | | | | - | |----------- Advanced Data Mapping + | +----------- Advanced Data Mapping | | - |----- Basic Active Record + +----- Basic Active Record \ No newline at end of file diff --git a/lib/sqlalchemy/mapper.py b/lib/sqlalchemy/mapper.py index d03b999887..53f4596d99 100644 --- a/lib/sqlalchemy/mapper.py +++ b/lib/sqlalchemy/mapper.py @@ -181,7 +181,7 @@ class Mapper(object): list of primary keys in the order of the table def's primary keys.""" key = objectstore.get_id_key(ident, self.class_, self.table) try: - return objectstore.get(key) + return objectstore.uow()._get(key) except KeyError: clause = sql.and_() i = 0 @@ -195,11 +195,6 @@ class Mapper(object): except IndexError: return None - def put(self, instance): - key = self.identity_key(instance) - objectstore.put(key, instance, self.scope) - return key - def identity_key(self, instance): return objectstore.get_id_key(tuple([self._getattrbycolumn(instance, column) for column in self.primary_keys[self.selectable]]), self.class_, self.table) @@ -290,9 +285,12 @@ class Mapper(object): self._setattrbycolumn(obj, col, primary_key) found = True - def register_dependencies(self, obj, uow): + def delete_obj(self, objects, uow): + pass + + def register_dependencies(self, *args, **kwargs): for prop in self.props.values(): - prop.register_dependencies(obj, uow) + prop.register_dependencies(*args, **kwargs) def transaction(self, f): return self.table.engine.multi_transaction(self.tables, f) @@ -330,7 +328,7 @@ class Mapper(object): # been exposed to being modified by the application. identitykey = self._identity_key(row) if objectstore.has_key(identitykey): - instance = objectstore.get(identitykey) + instance = objectstore.uow()._get(identitykey) if result is not None: result.append_nohistory(instance) @@ -371,8 +369,6 @@ class Mapper(object): return instance - def rollback(self, obj): - objectstore.uow().rollback_object(obj) class MapperOption: """describes a modification to a Mapper in the context of making a copy @@ -403,7 +399,7 @@ class MapperProperty: def delete(self, object): """called when the instance is being deleted""" pass - def register_dependencies(self, obj, uow): + def register_dependencies(self, *args, **kwargs): pass class ColumnProperty(MapperProperty): @@ -514,23 +510,25 @@ class PropertyLoader(MapperProperty): else: return sql.and_(crit) - def register_dependencies(self, objlist, uow): + def register_dependencies(self, uowcommit): if self.secondaryjoin is not None: # with many-to-many, set the parent as dependent on us, then the # list of associations as dependent on the parent # if only a list changes, the parent mapper is the only mapper that # gets added to the "todo" list - uow.register_dependency(self.mapper, self.parent, None, None) - uow.register_dependency(self.parent, None, self, objlist) + uowcommit.register_dependency(self.mapper, self.parent) + uowcommit.register_task(self.parent, self, uowcommit.get_objects(self.parent), False) elif self.foreignkey.table == self.target: - uow.register_dependency(self.parent, self.mapper, self, objlist) + uowcommit.register_dependency(self.parent, self.mapper) + uowcommit.register_task(self.parent, self, uowcommit.get_objects(self.parent), False) elif self.foreignkey.table == self.parent.table: - uow.register_dependency(self.mapper, self.parent, self, objlist) + uowcommit.register_dependency(self.mapper, self.parent) + uowcommit.register_task(self.mapper, self, uowcommit.get_objects(self.parent), False) else: raise " no foreign key ?" - + def process_dependencies(self, deplist, uowcommit, delete = False): - + print self.mapper.table.name + " process_dep" def getlist(obj): if self.uselist: return uowcommit.uow.attributes.get_list_history(obj, self.key) @@ -548,20 +546,28 @@ class PropertyLoader(MapperProperty): secondary_insert = [] for obj in deplist: childlist = getlist(obj) - for child in childlist.added_items(): - associationrow = {} - self.primaryjoin.accept_visitor(setter) - self.secondaryjoin.accept_visitor(setter) - secondary_insert.append(associationrow) - for child in childlist.deleted_items(): - associationrow = {} + if delete: clearkeys = True - self.primaryjoin.accept_visitor(setter) - self.secondaryjoin.accept_visitor(setter) - secondary_delete.append(associationrow) - if self.private: - uowcommit.add_item_to_delete(obj) - uowcommit.register_saved_list(childlist) + for child in childlist.deleted_items() + childlist.unchanged_items(): + associationrow = {} + self.primaryjoin.accept_visitor(setter) + self.secondaryjoin.accept_visitor(setter) + secondary_delete.append(associationrow) + uowcommit.register_removed_list(childlist) + else: + clearkeys = False + for child in childlist.added_items(): + associationrow = {} + self.primaryjoin.accept_visitor(setter) + self.secondaryjoin.accept_visitor(setter) + secondary_insert.append(associationrow) + clearkeys = True + for child in childlist.deleted_items(): + associationrow = {} + self.primaryjoin.accept_visitor(setter) + self.secondaryjoin.accept_visitor(setter) + secondary_delete.append(associationrow) + uowcommit.register_saved_list(childlist) if len(secondary_delete): statement = self.secondary.delete(sql.and_(*[c == sql.bindparam(c.key) for c in self.secondary.c])) statement.execute(*secondary_delete) @@ -577,8 +583,6 @@ class PropertyLoader(MapperProperty): for child in childlist.deleted_items() + childlist.current_items(): self.primaryjoin.accept_visitor(setter) uowcommit.register_saved_list(childlist) - if self.private: - uowcommit.add_item_to_delete(child) else: clearkeys = False for child in childlist.added_items(): @@ -588,8 +592,6 @@ class PropertyLoader(MapperProperty): for child in childlist.deleted_items(): self.primaryjoin.accept_visitor(setter) uowcommit.register_saved_list(childlist) - if self.private: - uowcommit.add_item_to_delete(child) elif self.foreignkey.table == self.parent.table: associationrow = {} for child in deplist: @@ -598,8 +600,6 @@ class PropertyLoader(MapperProperty): for obj in childlist.deleted_items() + childlist.current_items(): self.primaryjoin.accept_visitor(setter) uowcommit.register_saved_list(childlist) - if self.private: - uowcommit.add_item_to_delete(obj) else: clearkeys = False for obj in childlist.added_items(): @@ -607,8 +607,6 @@ class PropertyLoader(MapperProperty): uowcommit.register_saved_list(childlist) clearkeys = True for obj in childlist.deleted_items(): - if self.private: - uowcommit.add_item_to_delete(obj) self.primaryjoin.accept_visitor(setter) uowcommit.register_saved_list(childlist) else: diff --git a/lib/sqlalchemy/objectstore.py b/lib/sqlalchemy/objectstore.py index 7038c16576..2b00e7d66f 100644 --- a/lib/sqlalchemy/objectstore.py +++ b/lib/sqlalchemy/objectstore.py @@ -52,49 +52,11 @@ def get_row_key(row, class_, table, primary_keys): """ return (class_, table, tuple([row[column.label] for column in primary_keys])) -identity_map = {} - -def get(key): - val = identity_map[key] - if isinstance(val, dict): - return val[thread.get_ident()] - else: - return val - -def put(key, obj, scope='thread'): - if isinstance(obj, dict): - raise "cant put a dict in the object store" - - if scope == 'thread': - try: - d = identity_map[key] - except KeyError: - d = identity_map.setdefault(key, {}) - d[thread.get_ident()] = obj - else: - identity_map[key] = obj - -def clear(scope='thread'): - if scope == 'thread': - for k in identity_map.keys(): - if isinstance(identity_map[k], dict): - identity_map[k].clear() - uow.set(UnitOfWork()) - else: - for k in identity_map.keys(): - if not isinstance(identity_map[k], dict): - del identity_map[k] - uow.set(UnitOfWork(), scope="application") +def clear(): + uow.set(UnitOfWork()) def has_key(key): - if identity_map.has_key(key): - d = identity_map[key] - if isinstance(d, dict): - return d.has_key(thread.get_ident()) - else: - return True - else: - return False + return uow().identity_map.has_key(key) class UOWSmartProperty(attributes.SmartProperty): def attribute_registry(self): @@ -124,6 +86,10 @@ class UOWAttributeManager(attributes.AttributeManager): class UnitOfWork(object): def __init__(self, parent = None, is_begun = False): self.is_begun = is_begun + if parent is not None: + self.identity_map = parent.identity_map + else: + self.identity_map = {} self.attributes = UOWAttributeManager(self) self.new = util.HashSet() self.dirty = util.HashSet() @@ -131,16 +97,29 @@ class UnitOfWork(object): self.deleted = util.HashSet() self.parent = parent + def get(self, class_, *id): + return sqlalchemy.mapper.object_mapper(class_).get(*id) + + def _get(self, key): + return self.identity_map[key] + + def _put(self, key, obj): + self.identity_map[key] = obj + + def update(self, obj): + """called to add an object to this UnitOfWork as though it were loaded from the DB, but is + actually coming from somewhere else, like a web session or similar.""" + self._put(obj._instance_key, obj) + self.register_dirty(obj) + def register_attribute(self, class_, key, uselist): self.attributes.register_attribute(class_, key, uselist) def attribute_set_callable(self, obj, key, func): obj.__dict__[key] = func - def rollback_object(self, obj): - self.attributes.rollback(obj) - def register_clean(self, obj, scope="thread"): + def register_clean(self, obj): try: del self.dirty[obj] except KeyError: @@ -149,8 +128,7 @@ class UnitOfWork(object): del self.new[obj] except KeyError: pass - # TODO: figure scope out from what scope of this UOW is - put(obj._instance_key, obj, scope=scope) + self._put(obj._instance_key, obj) # TODO: get lists off the object and make sure theyre clean too ? def register_new(self, obj): @@ -217,6 +195,9 @@ class UnitOfWork(object): if self.parent: uow.set(self.parent) + def rollback_object(self, obj): + self.attributes.rollback(obj) + def rollback(self): if not self.is_begun: raise "UOW transaction is not begun" @@ -233,25 +214,38 @@ class UOWTransaction(object): self.saved_objects = util.HashSet() self.saved_lists = util.HashSet() self.deleted_objects = util.HashSet() - self.todelete = util.HashSet() def append_task(self, obj): mapper = self.object_mapper(obj) task = self.get_task_by_mapper(mapper) task.objects.append(obj) - def get_task_by_mapper(self, mapper): + def add_item_to_delete(self, obj): + mapper = self.object_mapper(obj) + task = self.get_task_by_mapper(mapper) + task.todelete.append(obj) + + def get_task_by_mapper(self, mapper, isdelete = False): try: - return self.tasks[mapper] + return self.tasks[(mapper, isdelete)] except KeyError: - return self.tasks.setdefault(mapper, UOWTask(mapper)) + return self.tasks.setdefault((mapper, isdelete), UOWTask(mapper, isdelete)) + def get_objects(self, mapper, isdelete = False): + try: + task = self.tasks[(mapper, isdelete)] + except KeyError: + return [] + + return task.objects + # TODO: better interface for tasks with no object save, or multiple dependencies - def register_dependency(self, mapper, dependency, processor, stuff_to_process): + def register_dependency(self, mapper, dependency): self.dependencies[(mapper, dependency)] = True - task = self.get_task_by_mapper(mapper) - if processor is not None: - task.dependencies.append((processor, stuff_to_process)) + + def register_task(self, mapper, processor, objects, isdelete): + task = self.get_task_by_mapper(mapper, isdelete) + task.dependencies.append((processor, objects)) def register_saved_object(self, obj): self.saved_objects.append(obj) @@ -262,8 +256,6 @@ class UOWTransaction(object): def register_deleted(self, obj): self.deleted_objects.append(obj) - def add_item_to_delete(self, obj): - self.todelete.append(obj) def object_mapper(self, obj): import sqlalchemy.mapper @@ -277,9 +269,9 @@ class UOWTransaction(object): def execute(self): for task in self.tasks.values(): - task.mapper.register_dependencies(task.objects, self) + task.mapper.register_dependencies(self) - mapperlist = self.tasks.values() + tasklist = self.tasks.values() def compare(a, b): if self.dependencies.has_key((a.mapper, b.mapper)): return -1 @@ -287,15 +279,21 @@ class UOWTransaction(object): return 1 else: return 0 - mapperlist.sort(compare) + tasklist.sort(compare) - for task in mapperlist: + import string + for task in tasklist: obj_list = task.objects - task.mapper.save_obj(obj_list, self) + if len(obj_list): + print "t:" + string.join([o.__class__.__name__ for o in obj_list]) + if not task.isdelete: + task.mapper.save_obj(obj_list, self) for dep in task.dependencies: - (processor, stuff_to_process) = dep - processor.process_dependencies(stuff_to_process, self) - + (processor, stuff) = dep + processor.process_dependencies(stuff, self, delete = task.isdelete) + if task.isdelete: + task.mapper.delete_obj(obj_list, self) + def post_exec(self): for obj in self.saved_objects: mapper = self.object_mapper(obj) @@ -310,8 +308,9 @@ class UOWTransaction(object): class UOWTask(object): - def __init__(self, mapper): + def __init__(self, mapper, isdelete = False): self.mapper = mapper + self.isdelete = isdelete self.objects = util.HashSet() self.dependencies = [] diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index c1982dfe4d..4df8d77a35 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -28,6 +28,7 @@ class INT: """integer datatype""" pass +INTEGER = INT class CHAR: """character datatype""" def __init__(self, length): diff --git a/test/mapper.py b/test/mapper.py index e2db30ca8c..ceea987af1 100644 --- a/test/mapper.py +++ b/test/mapper.py @@ -21,7 +21,7 @@ class MapperTest(AssertMixin): u = m.get(7) u2 = m.get(7) self.assert_(u is u2) - objectstore.clear("thread") + objectstore.clear() u2 = m.get(7) self.assert_(u is not u2) diff --git a/test/objectstore.py b/test/objectstore.py index 96839acccd..262fa0c465 100644 --- a/test/objectstore.py +++ b/test/objectstore.py @@ -7,7 +7,6 @@ import sqlalchemy.objectstore as objectstore ECHO = False DATA = False execfile("test/tables.py") -db.echo = True keywords.insert().execute( dict(keyword_id=1, name='blue'), @@ -21,6 +20,8 @@ keywords.insert().execute( db.connection().commit() +db.echo = True + class HistoryTest(AssertMixin): def testattr(self): m = mapper(User, users, properties = dict(addresses = relation(Address, addresses))) @@ -33,7 +34,7 @@ class HistoryTest(AssertMixin): u.addresses[1].email_address = 'there' print repr(u.__dict__) print repr(u.addresses) - m.rollback(u) + objectstore.uow().rollback_object(u) print repr(u.__dict__) class SaveTest(AssertMixin): @@ -130,6 +131,7 @@ class SaveTest(AssertMixin): # m = mapper(Address, addresses, properties = dict( # user = relation(User, users, foreignkey = addresses.c.user_id, primaryjoin = users.c.user_id == addresses.c.user_id, lazy = True, uselist = False) # )) + # TODO: put assertion in here !!! m = mapper(Address, addresses, properties = dict( user = relation(User, users, lazy = True, uselist = False) )) -- 2.47.2