From: Mike Bayer Date: Fri, 3 Jul 2009 16:05:55 +0000 (+0000) Subject: merge -r6064:6082 of 0.5 trunk X-Git-Tag: rel_0_6_6~157 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6c4762472a5915766fd3391a51d2b8c5435d2172;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git merge -r6064:6082 of 0.5 trunk --- diff --git a/CHANGES b/CHANGES index 1d986020a2..faee0d3dd2 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,13 @@ CHANGES the tests. [ticket:970] - orm + - Session.mapper is now *deprecated*. + Call session.add() if you'd like a free-standing object to be + part of your session. Otherwise, a DIY version of + Session.mapper is now documented at + http://www.sqlalchemy.org/trac/wiki/UsageRecipes/SessionAwareMapper + The method will remain deprecated throughout 0.6. + - Fixed bug introduced in 0.5.4 whereby Composite types fail when default-holding columns are flushed. @@ -45,6 +52,8 @@ CHANGES columns, has been enhanced such that the fk->itself aspect of the relation won't be used to determine relation direction. + - repaired non-working attributes.set_committed_value function. + - Trimmed the pickle format for InstanceState which should further reduce the memory footprint of pickled instances. The format should be backwards compatible with that of 0.5.4 and previous. diff --git a/doc/build/mappers.rst b/doc/build/mappers.rst index ff9b1f95bf..2dcd52fb3b 100644 --- a/doc/build/mappers.rst +++ b/doc/build/mappers.rst @@ -1205,8 +1205,24 @@ Eager loading of relations occurs using joins or outerjoins from parent to child Specifying Alternate Join Conditions to relation() --------------------------------------------------- - -The ``relation()`` function uses the foreign key relationship between the parent and child tables to formulate the **primary join condition** between parent and child; in the case of a many-to-many relationship it also formulates the **secondary join condition**. If you are working with a ``Table`` which has no ``ForeignKey`` objects on it (which can be the case when using reflected tables with MySQL), or if the join condition cannot be expressed by a simple foreign key relationship, use the ``primaryjoin`` and possibly ``secondaryjoin`` conditions to create the appropriate relationship. +The ``relation()`` function uses the foreign key relationship between the parent and child tables to formulate the **primary join condition** between parent and child; in the case of a many-to-many relationship it also formulates the **secondary join condition**:: + + one to many/many to one: + ------------------------ + + parent_table --> parent_table.c.id == child_table.c.parent_id --> child_table + primaryjoin + + many to many: + ------------- + + parent_table --> parent_table.c.id == secondary_table.c.parent_id --> + primaryjoin + + secondary_table.c.child_id == child_table.c.id --> child_table + secondaryjoin + +If you are working with a ``Table`` which has no ``ForeignKey`` objects on it (which can be the case when using reflected tables with MySQL), or if the join condition cannot be expressed by a simple foreign key relationship, use the ``primaryjoin`` and possibly ``secondaryjoin`` conditions to create the appropriate relationship. In this example we create a relation ``boston_addresses`` which will only load the user addresses with a city of "Boston": @@ -1287,10 +1303,45 @@ Theres no restriction on how many times you can relate from parent to child. SQ .. _alternate_collection_implementations: +Rows that point to themselves / Mutually Dependent Rows +------------------------------------------------------- + +This is a very specific case where relation() must perform an INSERT and a second UPDATE in order to properly populate a row (and vice versa an UPDATE and DELETE in order to delete without violating foreign key constraints). The two use cases are: + + * A table contains a foreign key to itself, and a single row will have a foreign key value pointing to its own primary key. + * Two tables each contain a foreign key referencing the other table, with a row in each table referencing the other. + +For example:: + + user + --------------------------------- + user_id name related_user_id + 1 'ed' 1 + +Or:: + + widget entry + ------------------------------------------- --------------------------------- + widget_id name favorite_entry_id entry_id name widget_id + 1 'somewidget' 5 5 'someentry' 1 + +In the first case, a row points to itself. Technically, a database that uses sequences such as Postgres or Oracle can INSERT the row at once using a previously generated value, but databases which rely upon autoincrement-style primary key identifiers cannot. The ``relation()`` always assumes a "parent/child" model of row population during flush, so unless you are populating the primary key/foreign key columns directly, ``relation()`` needs to use two statements. + +In the second case, the "widget" row must be inserted before any referring "entry" rows, but then the "favorite_entry_id" column of that "widget" row cannot be set until the "entry" rows have been generated. In this case, it's typically impossible to insert the "widget" and "entry" rows using just two INSERT statements; an UPDATE must be performed in order to keep foreign key constraints fulfilled. The exception is if the foreign keys are configured as "deferred until commit" (a feature some databases support) and if the identifiers were populated manually (again essentially bypassing ``relation()``). + +To enable the UPDATE after INSERT / UPDATE before DELETE behavior on ``relation()``, use the ``post_update`` flag on *one* of the relations, preferably the many-to-one side:: + + mapper(Widget, widget, properties={ + 'entries':relation(Entry, primaryjoin=widget.c.widget_id==entry.c.widget_id), + 'favorite_entry':relation(Entry, primaryjoin=widget.c.favorite_entry_id==entry.c.entry_id, post_update=True) + }) + +When a structure using the above mapping is flushed, the "widget" row will be INSERTed minus the "favorite_entry_id" value, then all the "entry" rows will be INSERTed referencing the parent "widget" row, and then an UPDATE statement will populate the "favorite_entry_id" column of the "widget" table (it's one row at a time for the time being). + + Alternate Collection Implementations ------------------------------------- - Mapping a one-to-many or many-to-many relationship results in a collection of values accessible through an attribute on the parent instance. By default, this collection is a ``list``: .. sourcecode:: python+sql diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py index a77801fcfc..a616ee0f69 100644 --- a/lib/sqlalchemy/dialects/firebird/base.py +++ b/lib/sqlalchemy/dialects/firebird/base.py @@ -254,7 +254,7 @@ ischema_names = { 'DATE': lambda r: FBDate(), 'TIME': lambda r: FBTime(), 'TEXT': lambda r: FBString(r['flen']), - 'INT64': lambda r: FBNumeric(precision=r['fprec'], length=r['fscale'] * -1), # This generically handles NUMERIC() + 'INT64': lambda r: FBNumeric(precision=r['fprec'], scale=r['fscale'] * -1), # This generically handles NUMERIC() 'DOUBLE': lambda r: FBFloat(), 'TIMESTAMP': lambda r: FBDateTime(), 'VARYING': lambda r: FBString(r['flen']), diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index f187877c49..97d0e42e91 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -1415,7 +1415,7 @@ def set_committed_value(instance, key, value): """ state, dict_ = instance_state(instance), instance_dict(instance) - state.get_impl(key).set_committed_value(state, dict_, key, value) + state.get_impl(key).set_committed_value(state, dict_, value) def set_attribute(instance, key, value): """Set the value of an attribute, firing history events. diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index c8d90a5c1b..28eb63819e 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -5,7 +5,7 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php import sqlalchemy.exceptions as sa_exc -from sqlalchemy.util import ScopedRegistry, to_list, get_cls_kwargs +from sqlalchemy.util import ScopedRegistry, to_list, get_cls_kwargs, deprecated from sqlalchemy.orm import ( EXT_CONTINUE, MapperExtension, class_mapper, object_session ) @@ -23,12 +23,7 @@ class ScopedSession(object): Session = scoped_session(sessionmaker(autoflush=True)) - To map classes so that new instances are saved in the current - Session automatically, as well as to provide session-aware - class attributes such as "query": - - mapper = Session.mapper - mapper(Class, table, ...) + ... use session normally. """ @@ -57,8 +52,15 @@ class ScopedSession(object): self.registry().close() self.registry.clear() + @deprecated("Session.mapper is deprecated. " + "Please see http://www.sqlalchemy.org/trac/wiki/UsageRecipes/SessionAwareMapper " + "for information on how to replicate its behavior.") def mapper(self, *args, **kwargs): - """return a mapper() function which associates this ScopedSession with the Mapper.""" + """return a mapper() function which associates this ScopedSession with the Mapper. + + DEPRECATED. + + """ from sqlalchemy.orm import mapper diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py index c49c00cec0..1e2fc9b60d 100644 --- a/test/ext/test_declarative.py +++ b/test/ext/test_declarative.py @@ -398,6 +398,7 @@ class DeclarativeTest(DeclarativeTestBase): Address(email='two'), ])]) + @testing.uses_deprecated() def test_custom_mapper(self): class MyExt(sa.orm.MapperExtension): def create_instance(self): diff --git a/test/orm/test_attributes.py b/test/orm/test_attributes.py index a94fdbe414..0295e42b22 100644 --- a/test/orm/test_attributes.py +++ b/test/orm/test_attributes.py @@ -513,6 +513,32 @@ class AttributesTest(_base.ORMTest): except sa_exc.ArgumentError, e: assert False +class UtilTest(_base.ORMTest): + def test_helpers(self): + class Foo(object): + pass + + class Bar(object): + pass + + attributes.register_class(Foo) + attributes.register_class(Bar) + attributes.register_attribute(Foo, "coll", uselist=True, useobject=True) + + f1 = Foo() + b1 = Bar() + b2 = Bar() + coll = attributes.init_collection(f1, "coll") + assert coll.data is f1.coll + assert attributes.get_attribute(f1, "coll") is f1.coll + attributes.set_attribute(f1, "coll", [b1]) + assert f1.coll == [b1] + eq_(attributes.get_history(f1, "coll"), ([b1], [], [])) + attributes.set_committed_value(f1, "coll", [b2]) + eq_(attributes.get_history(f1, "coll"), ((), [b2], ())) + + attributes.del_attribute(f1, "coll") + assert "coll" not in f1.__dict__ class BackrefTest(_base.ORMTest): diff --git a/test/orm/test_scoping.py b/test/orm/test_scoping.py index 2117e8dccb..9f2f59e19b 100644 --- a/test/orm/test_scoping.py +++ b/test/orm/test_scoping.py @@ -96,6 +96,7 @@ class ScopedMapperTest(_ScopedTest): pass @classmethod + @testing.uses_deprecated() @testing.resolve_artifact_names def setup_mappers(cls): Session = scoped_session(sa.orm.create_session) @@ -122,6 +123,7 @@ class ScopedMapperTest(_ScopedTest): sso = SomeOtherObject.query().first() assert SomeObject.query.filter_by(id=1).one().options[0].id == sso.id + @testing.uses_deprecated() @testing.resolve_artifact_names def test_query_compiles(self): class Foo(object): @@ -141,6 +143,7 @@ class ScopedMapperTest(_ScopedTest): Session.mapper(Baz, table2, extension=ext) assert hasattr(Baz, 'query') + @testing.uses_deprecated() @testing.resolve_artifact_names def test_default_constructor_state_not_shared(self): scope = scoped_session(sa.orm.sessionmaker()) @@ -171,6 +174,7 @@ class ScopedMapperTest(_ScopedTest): assert_raises(TypeError, C, foo='bar') D(foo='bar') + @testing.uses_deprecated() @testing.resolve_artifact_names def test_validating_constructor(self): s2 = SomeObject(someid=12) @@ -183,6 +187,7 @@ class ScopedMapperTest(_ScopedTest): assert_raises(sa.exc.ArgumentError, ValidatedOtherObject, someid=12, bogus=345) + @testing.uses_deprecated() @testing.resolve_artifact_names def test_dont_clobber_methods(self): class MyClass(object): @@ -215,6 +220,7 @@ class ScopedMapperTest2(_ScopedTest): pass @classmethod + @testing.uses_deprecated() @testing.resolve_artifact_names def setup_mappers(cls): Session = scoped_session(sa.orm.sessionmaker())