From be4b854304e22caac69cd714b8694090818e8576 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 26 Oct 2012 20:59:21 -0400 Subject: [PATCH] - more docs - it appears we can get rid of all those "XYZ_toplevel" names and use :doc:. --- doc/build/changelog/migration_08.rst | 95 ++++-- doc/build/core/connections.rst | 2 +- doc/build/core/engines.rst | 2 - doc/build/core/inspection.rst | 2 +- doc/build/core/tutorial.rst | 2 +- doc/build/glossary.rst | 78 +++++ doc/build/orm/inheritance.rst | 2 +- doc/build/orm/loading.rst | 76 ++--- doc/build/orm/session.rst | 2 - doc/build/orm/tutorial.rst | 488 +++++++++++++-------------- lib/sqlalchemy/engine/__init__.py | 2 +- lib/sqlalchemy/engine/base.py | 2 +- lib/sqlalchemy/orm/__init__.py | 8 +- lib/sqlalchemy/orm/query.py | 4 +- lib/sqlalchemy/orm/session.py | 2 +- lib/sqlalchemy/orm/util.py | 2 +- lib/sqlalchemy/sql/operators.py | 16 +- test/orm/test_of_type.py | 13 + 18 files changed, 456 insertions(+), 342 deletions(-) diff --git a/doc/build/changelog/migration_08.rst b/doc/build/changelog/migration_08.rst index 33264860c4..08a727a3cb 100644 --- a/doc/build/changelog/migration_08.rst +++ b/doc/build/changelog/migration_08.rst @@ -199,15 +199,20 @@ introspectable, this has never been a fully stable and supported feature, and users tended to not have a clear idea how to get at this information. -0.8 has a plan to produce a consistent, stable and fully -documented API for this purpose, which would provide an -inspection system that works on classes, instances, and -possibly other things as well. While many elements of this -system are already available, the plan is to lock down the -API including various accessors available from such objects -as :class:`.Mapper`, :class:`.InstanceState`, and :class:`.MapperProperty`: - -:: +0.8 now provides a consistent, stable and fully +documented API for this purpose, including an inspection +system which works on mapped classes, instances, attributes, +and other Core and ORM constructs. The entrypoint to this +system is the core-level :func:`.inspect` function. +In most cases, the object being inspected +is one already part of SQLAlchemy's system, +such as :class:`.Mapper`, :class:`.InstanceState`, +:class:`.Inspector`. In some cases, new objects have been +added with the job of providing the inspection API in +certain contexts, such as :class:`.AliasedInsp` and +:class:`.AttributeState`. + +A walkthrough of some key capabilities follows:: >>> class User(Base): ... __tablename__ = 'user' @@ -353,15 +358,26 @@ usable anywhere: of_type() works with alias(), with_polymorphic(), any(), has(), joinedload(), subqueryload(), contains_eager() -------------------------------------------------------------------------------------------------------------- -You can use :meth:`.PropComparator.of_type` with aliases and polymorphic -constructs; also works with most relationship functions like -:func:`.joinedload`, :func:`.subqueryload`, :func:`.contains_eager`, -:meth:`.PropComparator.any`, and :meth:`.PropComparator.has`: +The :meth:`.PropComparator.of_type` method is used to specify +a specific subtype to use when constructing SQL expressions along +a :func:`.relationship` that has a :term:`polymorphic` mapping as its target. +This method can now be used to target *any number* of target subtypes, +by combining it with the new :func:`.with_polymorphic` function:: -:: + # use eager loading in conjunction with with_polymorphic targets + Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True) + q = s.query(DataContainer).\ + join(DataContainer.jobs.of_type(Job_P)).\ + options(contains_eager(DataContainer.jobs.of_type(Job_P))) + +The method now works equally well in most places a regular relationship +attribute is accepted, including with loader functions like +:func:`.joinedload`, :func:`.subqueryload`, :func:`.contains_eager`, +and comparison methods like :meth:`.PropComparator.any` +and :meth:`.PropComparator.has`:: # use eager loading in conjunction with with_polymorphic targets - Job_P = with_polymorphic(Job, SubJob, aliased=True) + Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True) q = s.query(DataContainer).\ join(DataContainer.jobs.of_type(Job_P)).\ options(contains_eager(DataContainer.jobs.of_type(Job_P))) @@ -433,7 +449,7 @@ in one step: ORM Classes Now Accepted by Core Constructs ------------------------------------------- -While the SQL expressions used with :class:`.Query.filter`, +While the SQL expressions used with :meth:`.Query.filter`, such as ``User.id == 5``, have always been compatible for use with core constructs such as :func:`.select`, the mapped class itself would not be recognized when passed to :func:`.select`, @@ -769,27 +785,32 @@ when features such as :meth:`.MetaData.create_all` and :func:`.cast` is used:: :ticket:`2276` -"Prefixes" now supported for :func:`.insert`, :func:`.update`, :func:`.delete` -------------------------------------------------------------------------------- +"Prefixes" now supported for :func:`.update`, :func:`.delete` +------------------------------------------------------------- -Geared towards MySQL, a "prefix" can be rendered within any of these -statements:: +Geared towards MySQL, a "prefix" can be rendered within any of +these constructs. E.g.:: - stmt = table.insert().prefix_with("LOW_PRIORITY", dialect="mysql") + stmt = table.delete().prefix_with("LOW_PRIORITY", dialect="mysql") stmt = table.update().prefix_with("LOW_PRIORITY", dialect="mysql") -.. seealso:: +The method is new in addition to those which already existed +on :func:`.insert`, :func:`.select` and :class:`.Query`. - :meth:`.Insert.prefix_with` +.. seealso:: :meth:`.Update.prefix_with` :meth:`.Delete.prefix_with` + :meth:`.Insert.prefix_with` + :meth:`.Select.prefix_with` + :meth:`.Query.prefix_with` + :ticket:`2431` @@ -828,7 +849,7 @@ use cases should use the new "before_attach" event: Query now auto-correlates like a select() does ---------------------------------------------- -Previously it was necessary to call ``Query.correlate`` in +Previously it was necessary to call :meth:`.Query.correlate` in order to have a column- or WHERE-subquery correlate to the parent: @@ -878,10 +899,10 @@ usual scope so the behavior is removed. :ticket:`2277` -Fixed the behavior of Session.is_modified() +Fixed the behavior of :meth:`.Session.is_modified` ------------------------------------------- -The ``Session.is_modified()`` method accepts an argument +The :meth:`.Session.is_modified` method accepts an argument ``passive`` which basically should not be necessary, the argument in all cases should be the value ``True`` - when left at its default of ``False`` it would have the effect of @@ -891,14 +912,18 @@ argument will have no effect, and unloaded attributes will never be checked for history since by definition there can be no pending state change on an unloaded attribute. +.. seealso:: + + :meth:`.Session.is_modified` + :ticket:`2320` -``column.key`` is honored in the ``.c.`` attribute of ``select()`` with ``apply_labels()`` ------------------------------------------------------------------------------------------- +:attr:`.Column.key` is honored in the :attr:`.Select.c` attribute of :func:`.select` with :meth:`.Select.apply_labels` +----------------------------------------------------------------------------------------------------------------------- -Users of the expression system know that ``apply_labels()`` +Users of the expression system know that :meth:`.Select.apply_labels` prepends the table name to each column name, affecting the -names that are available from ``.c.``: +names that are available from :attr:`.Select.c`: :: @@ -906,9 +931,9 @@ names that are available from ``.c.``: s.c.table1_col1 s.c.table1_col2 -Before 0.8, if the ``Column`` had a different ``key``, this +Before 0.8, if the :class:`.Column` had a different :attr:`.Column.key`, this key would be ignored, inconsistently versus when -``apply_labels()`` were not used: +:meth:`.Select.apply_labels` were not used: :: @@ -924,7 +949,7 @@ key would be ignored, inconsistently versus when s.c.table1_column_one # would raise AttributeError s.c.table1_col1 # would be accessible like this -In 0.8, ``key`` is honored in both cases: +In 0.8, :attr:`.Column.key` is honored in both cases: :: @@ -943,9 +968,9 @@ In 0.8, ``key`` is honored in both cases: All other behavior regarding "name" and "key" are the same, including that the rendered SQL will still use the form ``_`` - the emphasis here was on -preventing the ``key`` contents from being rendered into the +preventing the :attr:`.Column.key` contents from being rendered into the ``SELECT`` statement so that there are no issues with -special/ non-ascii characters used in the ``key``. +special/ non-ascii characters used in the :attr:`.Column.key`. :ticket:`2397` diff --git a/doc/build/core/connections.rst b/doc/build/core/connections.rst index 3c403c1cba..de12a4689d 100644 --- a/doc/build/core/connections.rst +++ b/doc/build/core/connections.rst @@ -18,7 +18,7 @@ higher level management services, the :class:`.Engine` and Basic Usage =========== -Recall from :ref:`engines_toplevel` that an :class:`.Engine` is created via +Recall from :doc:`/core/engines` that an :class:`.Engine` is created via the :func:`.create_engine` call:: engine = create_engine('mysql://scott:tiger@localhost/test') diff --git a/doc/build/core/engines.rst b/doc/build/core/engines.rst index 2a8cfdd5ee..d793595bac 100644 --- a/doc/build/core/engines.rst +++ b/doc/build/core/engines.rst @@ -1,5 +1,3 @@ -.. _engines_toplevel: - ==================== Engine Configuration ==================== diff --git a/doc/build/core/inspection.rst b/doc/build/core/inspection.rst index 6d36f1015c..6a069ebb83 100644 --- a/doc/build/core/inspection.rst +++ b/doc/build/core/inspection.rst @@ -10,7 +10,7 @@ Runtime Inpection API Available Inspection Targets ---------------------------- -Following is a listing of all inspection targets. +Below is a listing of many of the most common inspection targets. * :class:`.Connectable` (i.e. :class:`.Engine`, :class:`.Connection`) - returns an :class:`.Inspector` object. diff --git a/doc/build/core/tutorial.rst b/doc/build/core/tutorial.rst index e9ed5c27ac..a05e6ccdf4 100644 --- a/doc/build/core/tutorial.rst +++ b/doc/build/core/tutorial.rst @@ -1554,7 +1554,7 @@ Expression Language Reference: :ref:`expression_api_toplevel` Database Metadata Reference: :ref:`metadata_toplevel` -Engine Reference: :ref:`engines_toplevel` +Engine Reference: :doc:`/core/engines` Connection Reference: :ref:`connections_toplevel` diff --git a/doc/build/glossary.rst b/doc/build/glossary.rst index b47ae71311..62f1e785ff 100644 --- a/doc/build/glossary.rst +++ b/doc/build/glossary.rst @@ -12,6 +12,7 @@ Glossary .. glossary:: + descriptor In Python, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the `descriptor protocol `_. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined @@ -50,6 +51,16 @@ Glossary >>> inspect(m1).attrs.data.history.added "some data" + discriminator + A result-set column which is used during :term:`polymorphic` loading + to determine what kind of mapped class should be applied to a particular + incoming result row. In SQLAlchemy, the classes are always part + of a hierarchy mapping using inheritance mapping. + + .. seealso:: + + :ref:`inheritance_toplevel` + instrumentation instrumented Instrumentation refers to the process of augmenting the functionality @@ -62,6 +73,26 @@ Glossary class which each represent a particular database column or relationship to a related class. + lazy load + In object relational mapping, a "lazy load" refers to an + attribute that does not contain its database-side value + for some period of time, typically when the object is + first loaded. Instead, the attribute receives a + *memoization* that causes it to go out to the database + and load its data when it's first used. Using this pattern, + the complexity and time spent within object fetches can + sometimes be reduced, in that + attributes for related tables don't need to be addressed + immediately. + + .. seealso:: + + `Lazy Load (on Martin Fowler) `_ + + :term:`N plus one problem` + + :doc:`orm/loading` + mapping mapped We say a class is "mapped" when it has been passed through the @@ -71,6 +102,40 @@ Glossary using a :class:`.Session` as well as loaded using a :class:`.Query`. + N plus one problem + The N plus one problem is a common side effect of the + :term:`lazy load` pattern, whereby an application wishes + to iterate through a related attribute or collection on + each member of a result set of objects, where that + attribute or collection is set to be loaded via the lazy + load pattern. The net result is that a SELECT statement + is emitted to load the initial result set of parent objects; + then, as the application iterates through each member, + an additional SELECT statement is emitted for each member + in order to load the related attribute or collection for + that member. The end result is that for a result set of + N parent objects, there will be N + 1 SELECT statements emitted. + + The N plus one problem is alleviated using :term:`eager loading`. + + .. seealso:: + + :doc:`orm/loading` + + polymorphic + polymorphically + Refers to a function that handles several types at once. In SQLAlchemy, + the term is usually applied to the concept of an ORM mapped class + whereby a query operation will return different subclasses + based on information in the result set, typically by checking the + value of a particular column in the result known as the :term:`discriminator`. + + Polymorphic loading in SQLAlchemy implies that a one or a + combination of three different schemes are used to map a hierarchy + of classes; "joined", "single", and "concrete". The section + :ref:`inheritance_toplevel` describes inheritance mapping fully. + + release releases released @@ -95,3 +160,16 @@ Glossary :ref:`pooling_toplevel` + + unit of work + This pattern is where the system transparently keeps + track of changes to objects and periodically flushes all those + pending changes out to the database. SQLAlchemy's Session + implements this pattern fully in a manner similar to that of + Hibernate. + + .. seealso:: + + `Unit of Work by Martin Fowler `_ + + :doc:`orm/session` diff --git a/doc/build/orm/inheritance.rst b/doc/build/orm/inheritance.rst index cf199841b2..cf8c6dc287 100644 --- a/doc/build/orm/inheritance.rst +++ b/doc/build/orm/inheritance.rst @@ -15,7 +15,7 @@ The most common forms of inheritance are single and joined table, while concrete inheritance presents more configurational challenges. When mappers are configured in an inheritance relationship, SQLAlchemy has the -ability to load elements "polymorphically", meaning that a single query can +ability to load elements :term:`polymorphically`, meaning that a single query can return objects of multiple types. Joined Table Inheritance diff --git a/doc/build/orm/loading.rst b/doc/build/orm/loading.rst index 7c9001afdd..e841795580 100644 --- a/doc/build/orm/loading.rst +++ b/doc/build/orm/loading.rst @@ -1,5 +1,3 @@ -.. _loading_toplevel: - .. currentmodule:: sqlalchemy.orm Relationship Loading Techniques @@ -21,14 +19,14 @@ in order to load the related object or objects: .. sourcecode:: python+sql {sql}>>> jack.addresses - SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, + SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE ? = addresses.user_id [5] {stop}[, ] -The one case where SQL is not emitted is for a simple many-to-one relationship, when +The one case where SQL is not emitted is for a simple many-to-one relationship, when the related object can be identified by its primary key alone and that object is already present in the current :class:`.Session`. @@ -66,17 +64,17 @@ parent objects: {sql}>>> jack = session.query(User).\ ... options(subqueryload('addresses')).\ - ... filter_by(name='jack').all() - SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, - users.password AS users_password - FROM users + ... filter_by(name='jack').all() + SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, + users.password AS users_password + FROM users WHERE users.name = ? ('jack',) - SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, - addresses.user_id AS addresses_user_id, anon_1.users_id AS anon_1_users_id - FROM (SELECT users.id AS users_id - FROM users - WHERE users.name = ?) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id + SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, + addresses.user_id AS addresses_user_id, anon_1.users_id AS anon_1_users_id + FROM (SELECT users.id AS users_id + FROM users + WHERE users.name = ?) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id, addresses.id ('jack',) @@ -162,9 +160,9 @@ Default Loading Strategies .. versionadded:: 0.7.5 Default loader strategies as a new feature. -Each of :func:`.joinedload`, :func:`.subqueryload`, :func:`.lazyload`, +Each of :func:`.joinedload`, :func:`.subqueryload`, :func:`.lazyload`, and :func:`.noload` can be used to set the default style of -:func:`.relationship` loading +:func:`.relationship` loading for a particular query, affecting all :func:`.relationship` -mapped attributes not otherwise specified in the :class:`.Query`. This feature is available by passing @@ -174,19 +172,19 @@ the string ``'*'`` as the argument to any of these options:: Above, the ``lazyload('*')`` option will supercede the ``lazy`` setting of all :func:`.relationship` constructs in use for that query, -except for those which use the ``'dynamic'`` style of loading. +except for those which use the ``'dynamic'`` style of loading. If some relationships specify ``lazy='joined'`` or ``lazy='subquery'``, for example, using ``default_strategy(lazy='select')`` will unilaterally cause all those relationships to use ``'select'`` loading. The option does not supercede loader options stated in the -query, such as :func:`.eagerload`, +query, such as :func:`.eagerload`, :func:`.subqueryload`, etc. The query below will still use joined loading for the ``widget`` relationship:: session.query(MyClass).options( - lazyload('*'), + lazyload('*'), joinedload(MyClass.widget) ) @@ -199,7 +197,7 @@ The Zen of Eager Loading ------------------------- The philosophy behind loader strategies is that any set of loading schemes can be -applied to a particular query, and *the results don't change* - only the number +applied to a particular query, and *the results don't change* - only the number of SQL statements required to fully load related objects and collections changes. A particular query might start out using all lazy loads. After using it in context, it might be revealed that particular attributes or collections are always accessed, and that it would be more @@ -220,7 +218,7 @@ is not valid - the ``Address`` entity is not named in the query: >>> jack = session.query(User).\ ... options(joinedload(User.addresses)).\ ... filter(User.name=='jack').\ - ... order_by(Address.email_address).all() + ... order_by(Address.email_address).all() {opensql}SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password @@ -228,7 +226,7 @@ is not valid - the ``Address`` entity is not named in the query: WHERE users.name = ? ORDER BY addresses.email_address <-- this part is wrong ! ['jack'] -Above, ``ORDER BY addresses.email_address`` is not valid since ``addresses`` is not in the +Above, ``ORDER BY addresses.email_address`` is not valid since ``addresses`` is not in the FROM list. The correct way to load the ``User`` records and order by email address is to use :meth:`.Query.join`: @@ -237,7 +235,7 @@ address is to use :meth:`.Query.join`: >>> jack = session.query(User).\ ... join(User.addresses).\ ... filter(User.name=='jack').\ - ... order_by(Address.email_address).all() + ... order_by(Address.email_address).all() {opensql} SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password @@ -247,7 +245,7 @@ address is to use :meth:`.Query.join`: The statement above is of course not the same as the previous one, in that the columns from ``addresses`` are not included in the result at all. We can add :func:`.joinedload` back in, so that -there are two joins - one is that which we are ordering on, the other is used anonymously to +there are two joins - one is that which we are ordering on, the other is used anonymously to load the contents of the ``User.addresses`` collection: .. sourcecode:: python+sql @@ -256,7 +254,7 @@ load the contents of the ``User.addresses`` collection: ... join(User.addresses).\ ... options(joinedload(User.addresses)).\ ... filter(User.name=='jack').\ - ... order_by(Address.email_address).all() + ... order_by(Address.email_address).all() {opensql}SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password @@ -269,8 +267,8 @@ What we see above is that our usage of :meth:`.Query.join` is to supply JOIN cla to use in subsequent query criterion, whereas our usage of :func:`.joinedload` only concerns itself with the loading of the ``User.addresses`` collection, for each ``User`` in the result. In this case, the two joins most probably appear redundant - which they are. If we -wanted to use just one JOIN for collection loading as well as ordering, we use the -:func:`.contains_eager` option, described in :ref:`contains_eager` below. But +wanted to use just one JOIN for collection loading as well as ordering, we use the +:func:`.contains_eager` option, described in :ref:`contains_eager` below. But to see why :func:`joinedload` does what it does, consider if we were **filtering** on a particular ``Address``: @@ -281,7 +279,7 @@ particular ``Address``: ... options(joinedload(User.addresses)).\ ... filter(User.name=='jack').\ ... filter(Address.email_address=='someaddress@foo.com').\ - ... all() + ... all() {opensql}SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password @@ -308,14 +306,14 @@ the actual ``User`` rows we want. Below we change :func:`.joinedload` into ... options(subqueryload(User.addresses)).\ ... filter(User.name=='jack').\ ... filter(Address.email_address=='someaddress@foo.com').\ - ... all() + ... all() {opensql}SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password FROM users JOIN addresses ON users.id = addresses.user_id WHERE users.name = ? AND addresses.email_address = ? ['jack', 'someaddress@foo.com'] - # ... subqueryload() emits a SELECT in order + # ... subqueryload() emits a SELECT in order # to load all address records ... When using joined eager loading, if the @@ -323,8 +321,8 @@ query contains a modifier that impacts the rows returned externally to the joins, such as when using DISTINCT, LIMIT, OFFSET or equivalent, the completed statement is first wrapped inside a subquery, and the joins used specifically for joined eager -loading are applied to the subquery. SQLAlchemy's -joined eager loading goes the extra mile, and then ten miles further, to +loading are applied to the subquery. SQLAlchemy's +joined eager loading goes the extra mile, and then ten miles further, to absolutely ensure that it does not affect the end result of the query, only the way collections and related objects are loaded, no matter what the format of the query is. @@ -344,18 +342,18 @@ references a scalar many-to-one reference. simple SELECT without any joins. * When using joined loading, the load of 100 objects and their collections will emit only one SQL - statement. However, the - total number of rows fetched will be equal to the sum of the size of all the collections, plus one + statement. However, the + total number of rows fetched will be equal to the sum of the size of all the collections, plus one extra row for each parent object that has an empty collection. Each row will also contain the full set of columns represented by the parents, repeated for each collection item - SQLAlchemy does not - re-fetch these columns other than those of the primary key, however most DBAPIs (with some - exceptions) will transmit the full data of each parent over the wire to the client connection in - any case. Therefore joined eager loading only makes sense when the size of the collections are + re-fetch these columns other than those of the primary key, however most DBAPIs (with some + exceptions) will transmit the full data of each parent over the wire to the client connection in + any case. Therefore joined eager loading only makes sense when the size of the collections are relatively small. The LEFT OUTER JOIN can also be performance intensive compared to an INNER join. * When using subquery loading, the load of 100 objects will emit two SQL statements. The second statement will fetch a total number of rows equal to the sum of the size of all collections. An - INNER JOIN is used, and a minimum of parent columns are requested, only the primary keys. So a + INNER JOIN is used, and a minimum of parent columns are requested, only the primary keys. So a subquery load makes sense when the collections are larger. * When multiple levels of depth are used with joined or subquery loading, loading collections-within- @@ -367,7 +365,7 @@ references a scalar many-to-one reference. * When using the default lazy loading, a load of 100 objects will like in the case of the collection emit as many as 101 SQL statements. However - there is a significant exception to this, in that if the many-to-one reference is a simple foreign key reference to the target's primary key, each - reference will be checked first in the current identity map using :meth:`.Query.get`. So here, + reference will be checked first in the current identity map using :meth:`.Query.get`. So here, if the collection of objects references a relatively small set of target objects, or the full set of possible target objects have already been loaded into the session and are strongly referenced, using the default of `lazy='select'` is by far the most efficient way to go. @@ -393,7 +391,7 @@ Routing Explicit Joins/Statements into Eagerly Loaded Collections ------------------------------------------------------------------ The behavior of :func:`~sqlalchemy.orm.joinedload()` is such that joins are -created automatically, using anonymous aliases as targets, the results of which +created automatically, using anonymous aliases as targets, the results of which are routed into collections and scalar references on loaded objects. It is often the case that a query already includes the necessary joins which represent a particular collection or scalar diff --git a/doc/build/orm/session.rst b/doc/build/orm/session.rst index 52cee92fb1..9a2a90335d 100644 --- a/doc/build/orm/session.rst +++ b/doc/build/orm/session.rst @@ -1,5 +1,3 @@ -.. _session_toplevel: - ================= Using the Session ================= diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst index 59dcc77c00..0e954946db 100644 --- a/doc/build/orm/tutorial.rst +++ b/doc/build/orm/tutorial.rst @@ -69,7 +69,7 @@ the core interface to the database, adapted through a **dialect** that handles t of the database and DBAPI in use. In this case the SQLite dialect will interpret instructions to the Python built-in ``sqlite3`` module. -The :class:`.Engine` has not actually tried to connect to the database yet; that happens +The :class:`.Engine` has not actually tried to connect to the database yet; that happens only the first time it is asked to perform a task against the database. We can illustrate this by asking it to perform a simple SELECT statement: @@ -80,8 +80,8 @@ this by asking it to perform a simple SELECT statement: () {stop}1 -As the :meth:`.Engine.execute` method is called, the :class:`.Engine` establishes a connection to the -SQLite database, which is then used to emit the SQL. The connection is then returned to an internal +As the :meth:`.Engine.execute` method is called, the :class:`.Engine` establishes a connection to the +SQLite database, which is then used to emit the SQL. The connection is then returned to an internal connection pool where it will be reused on subsequent statement executions. While we illustrate direct usage of the :class:`.Engine` here, this isn't typically necessary when using the ORM, where the :class:`.Engine`, once created, is used behind the scenes by the ORM as we'll see shortly. @@ -91,13 +91,13 @@ Declare a Mapping When using the ORM, the configurational process starts by describing the database tables we'll be dealing with, and then by defining our own classes which will -be mapped to those tables. In modern SQLAlchemy, +be mapped to those tables. In modern SQLAlchemy, these two tasks are usually performed together, using a system known as :ref:`declarative_toplevel`, which allows us to create classes that include directives to describe the actual database table they will be mapped to. -Classes mapped using the Declarative system are defined in terms of a base class which +Classes mapped using the Declarative system are defined in terms of a base class which maintains a catalog of classes and tables relative to that base - this is known as the **declarative base class**. Our application will usually have just one instance of this base in a commonly @@ -111,10 +111,10 @@ function, as follows:: Now that we have a "base", we can define any number of mapped classes in terms of it. We will start with just a single table called ``users``, which will store records for the end-users using our application. -A new class called ``User`` will be the class to which we map this table. The +A new class called ``User`` will be the class to which we map this table. The imports we'll need to accomplish this include objects that represent the components -of our table, including the :class:`.Column` class which represents a database column, -as well as the :class:`.Integer` and :class:`.String` classes that +of our table, including the :class:`.Column` class which represents a database column, +as well as the :class:`.Integer` and :class:`.String` classes that represent basic datatypes used in columns:: >>> from sqlalchemy import Column, Integer, String @@ -143,16 +143,16 @@ to be at least one column denoted as a primary key column; multiple-column, i.e. are of course entirely feasible as well. We define a constructor via ``__init__()`` and also a ``__repr__()`` method - both are optional. The -class of course can have any number of other methods and attributes as required by the application, +class of course can have any number of other methods and attributes as required by the application, as it's basically just a plain Python class. Inheriting from ``Base`` is also only a requirement -of the declarative configurational system, which itself is optional and relatively open ended; at its -core, the SQLAlchemy ORM only requires that a class be a so-called "new style class", that is, it inherits +of the declarative configurational system, which itself is optional and relatively open ended; at its +core, the SQLAlchemy ORM only requires that a class be a so-called "new style class", that is, it inherits from ``object`` in Python 2, in order to be mapped. All classes in Python 3 are "new style" classes. .. topic:: The Non Opinionated Philosophy In our ``User`` mapping example, it was required that we identify the name of the table - in use, as well as the names and characteristics of all columns which we care about, + in use, as well as the names and characteristics of all columns which we care about, including which column or columns represent the primary key, as well as some basic information about the types in use. SQLAlchemy never makes assumptions about these decisions - the developer must @@ -164,19 +164,19 @@ from ``object`` in Python 2, in order to be mapped. All classes in Python 3 ar With our ``User`` class constructed via the Declarative system, we have defined information about our table, known as **table metadata**, as well as a user-defined class which is linked to this -table, known as a **mapped class**. Declarative has provided for us a shorthand system for what in SQLAlchemy is +table, known as a **mapped class**. Declarative has provided for us a shorthand system for what in SQLAlchemy is called a "Classical Mapping", which specifies these two units separately and is discussed -in :ref:`classical_mapping`. The table +in :ref:`classical_mapping`. The table is actually represented by a datastructure known as :class:`.Table`, and the mapping represented -by a :class:`.Mapper` object generated by a function called :func:`.mapper`. Declarative performs both of +by a :class:`.Mapper` object generated by a function called :func:`.mapper`. Declarative performs both of these steps for us, making available the :class:`.Table` it has created via the ``__table__`` attribute:: >>> User.__table__ # doctest: +NORMALIZE_WHITESPACE Table('users', MetaData(None), - Column('id', Integer(), table=, primary_key=True, nullable=False), - Column('name', String(), table=), - Column('fullname', String(), table=), + Column('id', Integer(), table=, primary_key=True, nullable=False), + Column('name', String(), table=), + Column('fullname', String(), table=), Column('password', String(), table=), schema=None) and while rarely needed, making available the :class:`.Mapper` object via the ``__mapper__`` attribute:: @@ -191,7 +191,7 @@ new tables that have yet to be created in our SQLite database, so one helpful fe the :class:`.MetaData` object offers is the ability to issue CREATE TABLE statements to the database for all tables that don't yet exist. We illustrate this by calling the :meth:`.MetaData.create_all` method, passing in our :class:`.Engine` -as a source of database connectivity. We will see that special commands are +as a source of database connectivity. We will see that special commands are first emitted to check for the presence of the ``users`` table, and following that the actual ``CREATE TABLE`` statement: @@ -232,7 +232,7 @@ the actual ``CREATE TABLE`` statement: from sqlalchemy import Sequence Column(Integer, Sequence('user_id_seq'), primary_key=True) - A full, foolproof :class:`~sqlalchemy.schema.Table` generated via our declarative + A full, foolproof :class:`~sqlalchemy.schema.Table` generated via our declarative mapping is therefore:: class User(Base): @@ -270,11 +270,11 @@ With mappings complete, let's now create and inspect a ``User`` object:: 'None' The ``id`` attribute, which while not defined by our ``__init__()`` method, -exists with a value of ``None`` on our ``User`` instance due to the ``id`` +exists with a value of ``None`` on our ``User`` instance due to the ``id`` column we declared in our mapping. By default, the ORM creates class attributes for all columns present in the table being mapped. These class attributes exist as -`Python descriptors `_, and +`Python descriptors `_, and define **instrumentation** for the mapped class. The functionality of this instrumentation includes the ability to fire on change events, track modifications, and to automatically load new data from the database when @@ -350,10 +350,10 @@ session object. The business of acquiring a :class:`.Session` has a good deal of variety based on the variety of types of applications and frameworks out there. Keep in mind the :class:`.Session` is just a workspace for your objects, - local to a particular database connection - if you think of + local to a particular database connection - if you think of an application thread as a guest at a dinner party, the :class:`.Session` - is the guest's plate and the objects it holds are the food - (and the database...the kitchen?)! Hints on + is the guest's plate and the objects it holds are the food + (and the database...the kitchen?)! Hints on how :class:`.Session` is integrated into an application are at :ref:`session_faq`. @@ -384,9 +384,9 @@ added: BEGIN (implicit) INSERT INTO users (name, fullname, password) VALUES (?, ?, ?) ('ed', 'Ed Jones', 'edspassword') - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name = ? @@ -403,7 +403,7 @@ that which we just added:: >>> ed_user is our_user True -The ORM concept at work here is known as an `identity map `_ +The ORM concept at work here is known as an `identity map `_ and ensures that all operations upon a particular row within a :class:`~sqlalchemy.orm.session.Session` operate upon the same set of data. @@ -476,9 +476,9 @@ If we look at Ed's ``id`` attribute, which earlier was ``None``, it now has a va {sql}>>> ed_user.id # doctest: +NORMALIZE_WHITESPACE BEGIN (implicit) - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.id = ? @@ -492,7 +492,7 @@ load-on-first-access. In this case, the entire row was re-loaded on access because a new transaction was begun after we issued :meth:`~.Session.commit`. SQLAlchemy by default refreshes data from a previous transaction the first time it's accessed within a new transaction, so that the most recent state is available. -The level of reloading is configurable as is described in :ref:`session_toplevel`. +The level of reloading is configurable as is described in :doc:`/orm/session`. .. topic:: Session Object States @@ -500,7 +500,7 @@ The level of reloading is configurable as is described in :ref:`session_toplevel inside the :class:`.Session` without a primary key, to actually being inserted, it moved between three out of four available "object states" - **transient**, **pending**, and **persistent**. - Being aware of these states and what they mean is always a good idea - + Being aware of these states and what they mean is always a good idea - be sure to read :ref:`session_object_states` for a quick overview. Rolling Back @@ -529,9 +529,9 @@ Querying the session, we can see that they're flushed into the current transacti ('Edwardo', 1) INSERT INTO users (name, fullname, password) VALUES (?, ?, ?) ('fakeuser', 'Invalid', '12345') - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name IN (?, ?) @@ -549,9 +549,9 @@ Rolling back, we can see that ``ed_user``'s name is back to ``ed``, and {sql}>>> ed_user.name #doctest: +NORMALIZE_WHITESPACE BEGIN (implicit) - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.id = ? @@ -565,9 +565,9 @@ issuing a SELECT illustrates the changes made to the database: .. sourcecode:: python+sql {sql}>>> session.query(User).filter(User.name.in_(['ed', 'fakeuser'])).all() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name IN (?, ?) @@ -592,9 +592,9 @@ returned: {sql}>>> for instance in session.query(User).order_by(User.id): # doctest: +NORMALIZE_WHITESPACE ... print instance.name, instance.fullname - SELECT users.id AS users_id, + SELECT users.id AS users_id, users.name AS users_name, - users.fullname AS users_fullname, + users.fullname AS users_fullname, users.password AS users_password FROM users ORDER BY users.id () @@ -613,7 +613,7 @@ is expressed as tuples: {sql}>>> for name, fullname in session.query(User.name, User.fullname): # doctest: +NORMALIZE_WHITESPACE ... print name, fullname - SELECT users.name AS users_name, + SELECT users.name AS users_name, users.fullname AS users_fullname FROM users () @@ -631,9 +631,9 @@ class: {sql}>>> for row in session.query(User, User.name).all(): #doctest: +NORMALIZE_WHITESPACE ... print row.User, row.name - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users () @@ -670,9 +670,9 @@ entities are present in the call to :meth:`~.Session.query`, can be controlled u {sql}>>> for row in session.query(user_alias, user_alias.name).all(): #doctest: +NORMALIZE_WHITESPACE ... print row.user_alias - SELECT user_alias.id AS user_alias_id, - user_alias.name AS user_alias_name, - user_alias.fullname AS user_alias_fullname, + SELECT user_alias.id AS user_alias_id, + user_alias.name AS user_alias_name, + user_alias.fullname AS user_alias_fullname, user_alias.password AS user_alias_password FROM users AS user_alias (){stop} @@ -689,9 +689,9 @@ conjunction with ORDER BY: {sql}>>> for u in session.query(User).order_by(User.id)[1:3]: #doctest: +NORMALIZE_WHITESPACE ... print u - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users ORDER BY users.id LIMIT ? OFFSET ? @@ -739,9 +739,9 @@ users named "ed" with a full name of "Ed Jones", you can call ... filter(User.name=='ed').\ ... filter(User.fullname=='Ed Jones'): # doctest: +NORMALIZE_WHITESPACE ... print user - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name = ? AND users.fullname = ? @@ -818,9 +818,9 @@ non-iterator value. :meth:`~sqlalchemy.orm.query.Query.all()` returns a list: >>> query = session.query(User).filter(User.name.like('%ed')).order_by(User.id) {sql}>>> query.all() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name LIKE ? ORDER BY users.id @@ -833,9 +833,9 @@ the first result as a scalar: .. sourcecode:: python+sql {sql}>>> query.first() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name LIKE ? ORDER BY users.id @@ -854,9 +854,9 @@ an error: ... user = query.one() ... except MultipleResultsFound, e: ... print e - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name LIKE ? ORDER BY users.id @@ -870,9 +870,9 @@ an error: ... user = query.filter(User.id == 99).one() ... except NoResultFound, e: ... print e - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name LIKE ? AND users.id = ? ORDER BY users.id @@ -894,9 +894,9 @@ to SQLAlchemy clause constructs. For example, ... filter("id<224").\ ... order_by("id").all(): #doctest: +NORMALIZE_WHITESPACE ... print user.name - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE id<224 ORDER BY id @@ -914,9 +914,9 @@ method: {sql}>>> session.query(User).filter("id<:value and name=:name").\ ... params(value=224, name='fred').order_by(User.id).one() # doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE id>> q = session.query(User.id, User.name) {sql}>>> q.order_by("name").all() - SELECT users.id AS users_id, users.name AS users_name + SELECT users.id AS users_id, users.name AS users_name FROM users ORDER BY name () {stop}[(1, u'ed'), (4, u'fred'), (3, u'mary'), (2, u'wendy')] @@ -976,7 +976,7 @@ completely "raw", using string names to identify desired columns: Perfectly fine. But suppose, before we got a hold of the :class:`.Query`, some sophisticated transformations were applied to it, such as below where we use :meth:`~.Query.from_self`, a particularly advanced - method, to retrieve pairs of user names with + method, to retrieve pairs of user names with different numbers of characters:: >>> from sqlalchemy import func @@ -985,53 +985,53 @@ completely "raw", using string names to identify desired columns: ... filter(User.name < ua.name).\ ... filter(func.length(ua.name) != func.length(User.name)) - The :class:`.Query` now represents a select from a subquery, where + The :class:`.Query` now represents a select from a subquery, where ``User`` is represented twice both inside and outside of the subquery. Telling the :class:`.Query` to order by "name" doesn't really give - us much guarantee which "name" it's going to order on. In this + us much guarantee which "name" it's going to order on. In this case it assumes "name" is against the outer "aliased" ``User`` construct: .. sourcecode:: python+sql {sql}>>> q.order_by("name").all() #doctest: +NORMALIZE_WHITESPACE - SELECT anon_1.users_id AS anon_1_users_id, - anon_1.users_name AS anon_1_users_name, - users_1.name AS users_1_name - FROM (SELECT users.id AS users_id, users.name AS users_name - FROM users) AS anon_1, users AS users_1 - WHERE anon_1.users_name < users_1.name - AND length(users_1.name) != length(anon_1.users_name) + SELECT anon_1.users_id AS anon_1_users_id, + anon_1.users_name AS anon_1_users_name, + users_1.name AS users_1_name + FROM (SELECT users.id AS users_id, users.name AS users_name + FROM users) AS anon_1, users AS users_1 + WHERE anon_1.users_name < users_1.name + AND length(users_1.name) != length(anon_1.users_name) ORDER BY name () {stop}[(1, u'ed', u'fred'), (1, u'ed', u'mary'), (1, u'ed', u'wendy'), (3, u'mary', u'wendy'), (4, u'fred', u'wendy')] Only if we use the SQL element directly, in this case ``User.name`` - or ``ua.name``, do we give :class:`.Query` enough information to know + or ``ua.name``, do we give :class:`.Query` enough information to know for sure which "name" we'd like to order on, where we can see we get different results for each: .. sourcecode:: python+sql {sql}>>> q.order_by(ua.name).all() #doctest: +NORMALIZE_WHITESPACE - SELECT anon_1.users_id AS anon_1_users_id, - anon_1.users_name AS anon_1_users_name, - users_1.name AS users_1_name - FROM (SELECT users.id AS users_id, users.name AS users_name - FROM users) AS anon_1, users AS users_1 - WHERE anon_1.users_name < users_1.name - AND length(users_1.name) != length(anon_1.users_name) + SELECT anon_1.users_id AS anon_1_users_id, + anon_1.users_name AS anon_1_users_name, + users_1.name AS users_1_name + FROM (SELECT users.id AS users_id, users.name AS users_name + FROM users) AS anon_1, users AS users_1 + WHERE anon_1.users_name < users_1.name + AND length(users_1.name) != length(anon_1.users_name) ORDER BY users_1.name () {stop}[(1, u'ed', u'fred'), (1, u'ed', u'mary'), (1, u'ed', u'wendy'), (3, u'mary', u'wendy'), (4, u'fred', u'wendy')] {sql}>>> q.order_by(User.name).all() #doctest: +NORMALIZE_WHITESPACE - SELECT anon_1.users_id AS anon_1_users_id, - anon_1.users_name AS anon_1_users_name, - users_1.name AS users_1_name - FROM (SELECT users.id AS users_id, users.name AS users_name - FROM users) AS anon_1, users AS users_1 - WHERE anon_1.users_name < users_1.name - AND length(users_1.name) != length(anon_1.users_name) + SELECT anon_1.users_id AS anon_1_users_id, + anon_1.users_name AS anon_1_users_name, + users_1.name AS users_1_name + FROM (SELECT users.id AS users_id, users.name AS users_name + FROM users) AS anon_1, users AS users_1 + WHERE anon_1.users_name < users_1.name + AND length(users_1.name) != length(anon_1.users_name) ORDER BY anon_1.users_name () {stop}[(1, u'ed', u'wendy'), (1, u'ed', u'mary'), (1, u'ed', u'fred'), (4, u'fred', u'wendy'), (3, u'mary', u'wendy')] @@ -1045,26 +1045,26 @@ counting called :meth:`~sqlalchemy.orm.query.Query.count()`: .. sourcecode:: python+sql {sql}>>> session.query(User).filter(User.name.like('%ed')).count() #doctest: +NORMALIZE_WHITESPACE - SELECT count(*) AS count_1 - FROM (SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, - users.password AS users_password - FROM users + SELECT count(*) AS count_1 + FROM (SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, + users.password AS users_password + FROM users WHERE users.name LIKE ?) AS anon_1 ('%ed',) {stop}2 The :meth:`~.Query.count()` method is used to determine how many rows the SQL statement would return. Looking -at the generated SQL above, SQLAlchemy always places whatever it is we are +at the generated SQL above, SQLAlchemy always places whatever it is we are querying into a subquery, then counts the rows from that. In some cases this can be reduced to a simpler ``SELECT count(*) FROM table``, however modern versions of SQLAlchemy don't try to guess when this is appropriate, as the exact SQL can be emitted using more explicit means. For situations where the "thing to be counted" needs -to be indicated specifically, we can specify the "count" function +to be indicated specifically, we can specify the "count" function directly using the expression ``func.count()``, available from the :attr:`~sqlalchemy.sql.expression.func` construct. Below we use it to return the count of each distinct user name: @@ -1083,7 +1083,7 @@ To achieve our simple ``SELECT count(*) FROM table``, we can apply it as: .. sourcecode:: python+sql {sql}>>> session.query(func.count('*')).select_from(User).scalar() - SELECT count(?) AS count_1 + SELECT count(?) AS count_1 FROM users ('*',) {stop}4 @@ -1103,7 +1103,7 @@ Building a Relationship ======================= Let's consider how a second table, related to ``User``, can be mapped and -queried. Users in our system +queried. Users in our system can store any number of email addresses associated with their username. This implies a basic one to many association from the ``users`` to a new table which stores email addresses, which we will call ``addresses``. Using @@ -1139,7 +1139,7 @@ those values in the ``users.id`` column, i.e. its primary key. A second directive, known as :func:`.relationship`, tells the ORM that the ``Address`` class itself should be linked -to the ``User`` class, using the attribute ``Address.user``. +to the ``User`` class, using the attribute ``Address.user``. :func:`.relationship` uses the foreign key relationships between the two tables to determine the nature of this linkage, determining that ``Address.user`` will be **many-to-one**. @@ -1156,14 +1156,14 @@ are referred to as a **bidirectional relationship**, and is a key feature of the SQLAlchemy ORM. The section :ref:`relationships_backref` discusses the "backref" feature in detail. -Arguments to :func:`.relationship` which concern the remote class -can be specified using strings, assuming the Declarative system is in +Arguments to :func:`.relationship` which concern the remote class +can be specified using strings, assuming the Declarative system is in use. Once all mappings are complete, these strings are evaluated -as Python expressions in order to produce the actual argument, in the -above case the ``User`` class. The names which are allowed during +as Python expressions in order to produce the actual argument, in the +above case the ``User`` class. The names which are allowed during this evaluation include, among other things, the names of all classes which have been created in terms of the declared base. Below we illustrate creation -of the same "addresses/user" bidirectional relationship in terms of ``User`` instead of +of the same "addresses/user" bidirectional relationship in terms of ``User`` instead of ``Address``:: class User(Base): @@ -1174,7 +1174,7 @@ See the docstring for :func:`.relationship` for more detail on argument style. .. topic:: Did you know ? - * a FOREIGN KEY constraint in most (though not all) relational databases can + * a FOREIGN KEY constraint in most (though not all) relational databases can only link to a primary key column, or a column that has a UNIQUE constraint. * a FOREIGN KEY constraint that refers to a multiple column primary key, and itself has multiple columns, is known as a "composite foreign key". It can also @@ -1227,7 +1227,7 @@ just assign a full list directly: .. sourcecode:: python+sql >>> jack.addresses = [ - ... Address(email_address='jack@google.com'), + ... Address(email_address='jack@google.com'), ... Address(email_address='j25@yahoo.com')] When using a bidirectional relationship, elements added in one direction @@ -1266,9 +1266,9 @@ Querying for Jack, we get just Jack back. No SQL is yet issued for Jack's addre {sql}>>> jack = session.query(User).\ ... filter_by(name='jack').one() #doctest: +NORMALIZE_WHITESPACE BEGIN (implicit) - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name = ? @@ -1282,9 +1282,9 @@ Let's look at the ``addresses`` collection. Watch the SQL: .. sourcecode:: python+sql {sql}>>> jack.addresses #doctest: +NORMALIZE_WHITESPACE - SELECT addresses.id AS addresses_id, - addresses.email_address AS - addresses_email_address, + SELECT addresses.id AS addresses_id, + addresses.email_address AS + addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE ? = addresses.user_id ORDER BY addresses.id @@ -1303,12 +1303,12 @@ Querying with Joins Now that we have two tables, we can show some more features of :class:`.Query`, specifically how to create queries that deal with both tables at the same time. -The `Wikipedia page on SQL JOIN -`_ offers a good introduction to +The `Wikipedia page on SQL JOIN +`_ offers a good introduction to join techniques, several of which we'll illustrate here. To construct a simple implicit join between ``User`` and ``Address``, -we can use :meth:`.Query.filter()` to equate their related columns together. +we can use :meth:`.Query.filter()` to equate their related columns together. Below we load the ``User`` and ``Address`` entities at once using this method: .. sourcecode:: python+sql @@ -1318,15 +1318,15 @@ Below we load the ``User`` and ``Address`` entities at once using this method: ... filter(Address.email_address=='jack@google.com').\ ... all(): # doctest: +NORMALIZE_WHITESPACE ... print u, a - SELECT users.id AS users_id, - users.name AS users_name, + SELECT users.id AS users_id, + users.name AS users_name, users.fullname AS users_fullname, - users.password AS users_password, + users.password AS users_password, addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, + addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM users, addresses - WHERE users.id = addresses.user_id + WHERE users.id = addresses.user_id AND addresses.email_address = ? ('jack@google.com',) {stop} @@ -1339,9 +1339,9 @@ method: {sql}>>> session.query(User).join(Address).\ ... filter(Address.email_address=='jack@google.com').\ ... all() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users JOIN addresses ON users.id = addresses.user_id WHERE addresses.email_address = ? @@ -1358,7 +1358,7 @@ works better when one of the following forms are used:: query.join(Address, User.addresses) # same, with explicit target query.join('addresses') # same, using a string -As you would expect, the same idea is used for "outer" joins, using the +As you would expect, the same idea is used for "outer" joins, using the :meth:`~.Query.outerjoin` function:: query.outerjoin(User.addresses) # LEFT OUTER JOIN @@ -1392,14 +1392,14 @@ same time: ... filter(adalias1.email_address=='jack@google.com').\ ... filter(adalias2.email_address=='j25@yahoo.com'): ... print username, email1, email2 # doctest: +NORMALIZE_WHITESPACE - SELECT users.name AS users_name, + SELECT users.name AS users_name, addresses_1.email_address AS addresses_1_email_address, addresses_2.email_address AS addresses_2_email_address - FROM users JOIN addresses AS addresses_1 + FROM users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id - JOIN addresses AS addresses_2 + JOIN addresses AS addresses_2 ON users.id = addresses_2.user_id - WHERE addresses_1.email_address = ? + WHERE addresses_1.email_address = ? AND addresses_2.email_address = ? ('jack@google.com', 'j25@yahoo.com') {stop}jack jack@google.com j25@yahoo.com @@ -1415,7 +1415,7 @@ ids, and JOIN to the parent. In this case we use a LEFT OUTER JOIN so that we get rows back for those users who don't have any addresses, e.g.:: SELECT users.*, adr_count.address_count FROM users LEFT OUTER JOIN - (SELECT user_id, count(*) AS address_count + (SELECT user_id, count(*) AS address_count FROM addresses GROUP BY user_id) AS adr_count ON users.id=adr_count.user_id @@ -1445,14 +1445,14 @@ accessible through an attribute called ``c``: {sql}>>> for u, count in session.query(User, stmt.c.address_count).\ ... outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id): # doctest: +NORMALIZE_WHITESPACE ... print u, count - SELECT users.id AS users_id, + SELECT users.id AS users_id, users.name AS users_name, - users.fullname AS users_fullname, + users.fullname AS users_fullname, users.password AS users_password, anon_1.address_count AS anon_1_address_count - FROM users LEFT OUTER JOIN + FROM users LEFT OUTER JOIN (SELECT addresses.user_id AS user_id, count(?) AS address_count - FROM addresses GROUP BY addresses.user_id) AS anon_1 + FROM addresses GROUP BY addresses.user_id) AS anon_1 ON users.id = anon_1.user_id ORDER BY users.id ('*',) @@ -1478,19 +1478,19 @@ to associate an "alias" of a mapped class to a subquery: >>> for user, address in session.query(User, adalias).\ ... join(adalias, User.addresses): # doctest: +NORMALIZE_WHITESPACE ... print user, address - SELECT users.id AS users_id, - users.name AS users_name, + SELECT users.id AS users_id, + users.name AS users_name, users.fullname AS users_fullname, - users.password AS users_password, + users.password AS users_password, anon_1.id AS anon_1_id, - anon_1.email_address AS anon_1_email_address, + anon_1.email_address AS anon_1_email_address, anon_1.user_id AS anon_1_user_id - FROM users JOIN - (SELECT addresses.id AS id, - addresses.email_address AS email_address, + FROM users JOIN + (SELECT addresses.id AS id, + addresses.email_address AS email_address, addresses.user_id AS user_id FROM addresses - WHERE addresses.email_address != ?) AS anon_1 + WHERE addresses.email_address != ?) AS anon_1 ON users.id = anon_1.user_id ('j25@yahoo.com',) {stop} @@ -1559,7 +1559,7 @@ usage of EXISTS automatically. Above, the statement can be expressed along the {sql}>>> session.query(Address).\ ... filter(~Address.user.has(User.name=='jack')).all() # doctest: +NORMALIZE_WHITESPACE - SELECT addresses.id AS addresses_id, + SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses @@ -1612,7 +1612,7 @@ Eager Loading Recall earlier that we illustrated a **lazy loading** operation, when we accessed the ``User.addresses`` collection of a ``User`` and SQL -was emitted. If you want to reduce the number of queries (dramatically, in many cases), +was emitted. If you want to reduce the number of queries (dramatically, in many cases), we can apply an **eager load** to the query operation. SQLAlchemy offers three types of eager loading, two of which are automatic, and a third which involves custom criterion. All three are usually invoked via functions known @@ -1626,9 +1626,9 @@ In this case we'd like to indicate that ``User.addresses`` should load eagerly. A good choice for loading a set of objects as well as their related collections is the :func:`.orm.subqueryload` option, which emits a second SELECT statement that fully loads the collections associated with the results just loaded. -The name "subquery" originates from the fact that the SELECT statement +The name "subquery" originates from the fact that the SELECT statement constructed directly via the :class:`.Query` is re-used, embedded as a subquery -into a SELECT against the related table. This is a little elaborate but +into a SELECT against the related table. This is a little elaborate but very easy to use: .. sourcecode:: python+sql @@ -1637,20 +1637,20 @@ very easy to use: {sql}>>> jack = session.query(User).\ ... options(subqueryload(User.addresses)).\ ... filter_by(name='jack').one() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, - users.password AS users_password - FROM users + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, + users.password AS users_password + FROM users WHERE users.name = ? ('jack',) - SELECT addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, - addresses.user_id AS addresses_user_id, - anon_1.users_id AS anon_1_users_id - FROM (SELECT users.id AS users_id - FROM users WHERE users.name = ?) AS anon_1 - JOIN addresses ON anon_1.users_id = addresses.user_id + SELECT addresses.id AS addresses_id, + addresses.email_address AS addresses_email_address, + addresses.user_id AS addresses_user_id, + anon_1.users_id AS anon_1_users_id + FROM (SELECT users.id AS users_id + FROM users WHERE users.name = ?) AS anon_1 + JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id, addresses.id ('jack',) {stop}>>> jack @@ -1667,7 +1667,7 @@ The other automatic eager loading function is more well known and is called a LEFT OUTER JOIN, so that the lead object as well as the related object or collection is loaded in one step. We illustrate loading the same ``addresses`` collection in this way - note that even though the ``User.addresses`` -collection on ``jack`` is actually populated right now, the query +collection on ``jack`` is actually populated right now, the query will emit the extra join regardless: .. sourcecode:: python+sql @@ -1677,14 +1677,14 @@ will emit the extra join regardless: {sql}>>> jack = session.query(User).\ ... options(joinedload(User.addresses)).\ ... filter_by(name='jack').one() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, + SELECT users.id AS users_id, + users.name AS users_name, users.fullname AS users_fullname, - users.password AS users_password, - addresses_1.id AS addresses_1_id, - addresses_1.email_address AS addresses_1_email_address, + users.password AS users_password, + addresses_1.id AS addresses_1_id, + addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id - FROM users + FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? ORDER BY addresses_1.id ('jack',) @@ -1711,11 +1711,11 @@ for both the lead and the related object. The join created by :func:`.joinedload` is anonymously aliased such that it **does not affect the query results**. An :meth:`.Query.order_by` or :meth:`.Query.filter` call **cannot** reference these aliased - tables - so-called "user space" joins are constructed using + tables - so-called "user space" joins are constructed using :meth:`.Query.join`. The rationale for this is that :func:`.joinedload` is only applied in order to affect how related objects or collections are loaded as an optimizing detail - it can be added or removed with no impact - on actual results. See the section :ref:`zen_of_eager_loading` for + on actual results. See the section :ref:`zen_of_eager_loading` for a detailed description of how this is used. Explicit Join + Eagerload @@ -1739,14 +1739,14 @@ attribute: ... filter(User.name=='jack').\ ... options(contains_eager(Address.user)).\ ... all() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, - users.password AS users_password, - addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, - addresses.user_id AS addresses_user_id - FROM addresses JOIN users ON users.id = addresses.user_id + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, + users.password AS users_password, + addresses.id AS addresses_id, + addresses.email_address AS addresses_email_address, + addresses.user_id AS addresses_user_id + FROM addresses JOIN users ON users.id = addresses.user_id WHERE users.name = ? ('jack',) @@ -1757,7 +1757,7 @@ attribute: For more information on eager loading, including how to configure various forms -of loading by default, see the section :ref:`loading_toplevel`. +of loading by default, see the section :doc:`/orm/loading`. Deleting ======== @@ -1775,12 +1775,12 @@ the session, then we'll issue a ``count`` query to see that no rows remain: (None, 2) DELETE FROM users WHERE users.id = ? (5,) - SELECT count(*) AS count_1 - FROM (SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, - users.password AS users_password - FROM users + SELECT count(*) AS count_1 + FROM (SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, + users.password AS users_password + FROM users WHERE users.name = ?) AS anon_1 ('jack',) {stop}0 @@ -1792,11 +1792,11 @@ So far, so good. How about Jack's ``Address`` objects ? {sql}>>> session.query(Address).filter( ... Address.email_address.in_(['jack@google.com', 'j25@yahoo.com']) ... ).count() # doctest: +NORMALIZE_WHITESPACE - SELECT count(*) AS count_1 - FROM (SELECT addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, - addresses.user_id AS addresses_user_id - FROM addresses + SELECT count(*) AS count_1 + FROM (SELECT addresses.id AS addresses_id, + addresses.email_address AS addresses_email_address, + addresses.user_id AS addresses_user_id + FROM addresses WHERE addresses.email_address IN (?, ?)) AS anon_1 ('jack@google.com', 'j25@yahoo.com') {stop}2 @@ -1858,9 +1858,9 @@ removing an address from his ``addresses`` collection will result in that # load Jack by primary key {sql}>>> jack = session.query(User).get(5) #doctest: +NORMALIZE_WHITESPACE BEGIN (implicit) - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.id = ? @@ -1869,8 +1869,8 @@ removing an address from his ``addresses`` collection will result in that # remove one Address (lazy load fires off) {sql}>>> del jack.addresses[1] #doctest: +NORMALIZE_WHITESPACE - SELECT addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, + SELECT addresses.id AS addresses_id, + addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE ? = addresses.user_id @@ -1883,11 +1883,11 @@ removing an address from his ``addresses`` collection will result in that ... ).count() # doctest: +NORMALIZE_WHITESPACE DELETE FROM addresses WHERE addresses.id = ? (2,) - SELECT count(*) AS count_1 - FROM (SELECT addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, - addresses.user_id AS addresses_user_id - FROM addresses + SELECT count(*) AS count_1 + FROM (SELECT addresses.id AS addresses_id, + addresses.email_address AS addresses_email_address, + addresses.user_id AS addresses_user_id + FROM addresses WHERE addresses.email_address IN (?, ?)) AS anon_1 ('jack@google.com', 'j25@yahoo.com') {stop}1 @@ -1903,12 +1903,12 @@ Deleting Jack will delete both Jack and his remaining ``Address``: (1,) DELETE FROM users WHERE users.id = ? (5,) - SELECT count(*) AS count_1 - FROM (SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, - users.password AS users_password - FROM users + SELECT count(*) AS count_1 + FROM (SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, + users.password AS users_password + FROM users WHERE users.name = ?) AS anon_1 ('jack',) {stop}0 @@ -1916,11 +1916,11 @@ Deleting Jack will delete both Jack and his remaining ``Address``: {sql}>>> session.query(Address).filter( ... Address.email_address.in_(['jack@google.com', 'j25@yahoo.com']) ... ).count() # doctest: +NORMALIZE_WHITESPACE - SELECT count(*) AS count_1 - FROM (SELECT addresses.id AS addresses_id, - addresses.email_address AS addresses_email_address, - addresses.user_id AS addresses_user_id - FROM addresses + SELECT count(*) AS count_1 + FROM (SELECT addresses.id AS addresses_id, + addresses.email_address AS addresses_email_address, + addresses.user_id AS addresses_user_id + FROM addresses WHERE addresses.email_address IN (?, ?)) AS anon_1 ('jack@google.com', 'j25@yahoo.com') {stop}0 @@ -1928,7 +1928,7 @@ Deleting Jack will delete both Jack and his remaining ``Address``: .. topic:: More on Cascades Further detail on configuration of cascades is at :ref:`unitofwork_cascades`. - The cascade functionality can also integrate smoothly with + The cascade functionality can also integrate smoothly with the ``ON DELETE CASCADE`` functionality of the relational database. See :ref:`passive_deletes` for details. @@ -1952,7 +1952,7 @@ to serve as the association table. This looks like the following:: Above, we can see declaring a :class:`.Table` directly is a little different than declaring a mapped class. :class:`.Table` is a constructor function, so -each individual :class:`.Column` argument is separated by a comma. The +each individual :class:`.Column` argument is separated by a comma. The :class:`.Column` object is also given its name explicitly, rather than it being taken from an assigned attribute name. @@ -2062,9 +2062,9 @@ Usage is not too different from what we've been doing. Let's give Wendy some bl {sql}>>> wendy = session.query(User).\ ... filter_by(name='wendy').\ ... one() #doctest: +NORMALIZE_WHITESPACE - SELECT users.id AS users_id, - users.name AS users_name, - users.fullname AS users_fullname, + SELECT users.id AS users_id, + users.name AS users_name, + users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name = ? @@ -2098,15 +2098,15 @@ keyword string 'firstpost'": (2, "Wendy's Blog Post", 'This is a test') INSERT INTO post_keywords (post_id, keyword_id) VALUES (?, ?) ((1, 1), (1, 2)) - SELECT posts.id AS posts_id, - posts.user_id AS posts_user_id, - posts.headline AS posts_headline, + SELECT posts.id AS posts_id, + posts.user_id AS posts_user_id, + posts.headline AS posts_headline, posts.body AS posts_body FROM posts WHERE EXISTS (SELECT 1 FROM post_keywords, keywords - WHERE posts.id = post_keywords.post_id - AND keywords.id = post_keywords.keyword_id + WHERE posts.id = post_keywords.post_id + AND keywords.id = post_keywords.keyword_id AND keywords.keyword = ?) ('firstpost',) {stop}[BlogPost("Wendy's Blog Post", 'This is a test', )] @@ -2120,15 +2120,15 @@ to her as a parent: ... filter(BlogPost.author==wendy).\ ... filter(BlogPost.keywords.any(keyword='firstpost')).\ ... all() #doctest: +NORMALIZE_WHITESPACE - SELECT posts.id AS posts_id, - posts.user_id AS posts_user_id, - posts.headline AS posts_headline, + SELECT posts.id AS posts_id, + posts.user_id AS posts_user_id, + posts.headline AS posts_headline, posts.body AS posts_body FROM posts WHERE ? = posts.user_id AND (EXISTS (SELECT 1 FROM post_keywords, keywords - WHERE posts.id = post_keywords.post_id - AND keywords.id = post_keywords.keyword_id + WHERE posts.id = post_keywords.post_id + AND keywords.id = post_keywords.keyword_id AND keywords.keyword = ?)) (2, 'firstpost') {stop}[BlogPost("Wendy's Blog Post", 'This is a test', )] @@ -2141,15 +2141,15 @@ relationship, to query straight from there: {sql}>>> wendy.posts.\ ... filter(BlogPost.keywords.any(keyword='firstpost')).\ ... all() #doctest: +NORMALIZE_WHITESPACE - SELECT posts.id AS posts_id, - posts.user_id AS posts_user_id, - posts.headline AS posts_headline, + SELECT posts.id AS posts_id, + posts.user_id AS posts_user_id, + posts.headline AS posts_headline, posts.body AS posts_body FROM posts WHERE ? = posts.user_id AND (EXISTS (SELECT 1 FROM post_keywords, keywords - WHERE posts.id = post_keywords.post_id - AND keywords.id = post_keywords.keyword_id + WHERE posts.id = post_keywords.post_id + AND keywords.id = post_keywords.keyword_id AND keywords.keyword = ?)) (2, 'firstpost') {stop}[BlogPost("Wendy's Blog Post", 'This is a test', )] @@ -2163,4 +2163,4 @@ Mapper Reference: :ref:`mapper_config_toplevel` Relationship Reference: :ref:`relationship_config_toplevel` -Session Reference: :ref:`session_toplevel` +Session Reference: :doc:`/orm/session` diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 3deb437bbf..f02e79f331 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -122,7 +122,7 @@ def create_engine(*args, **kwargs): See also: - :ref:`engines_toplevel` + :doc:`/core/engines` :ref:`connections_toplevel` diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index bf664413ca..797158b3b7 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1292,7 +1292,7 @@ class Engine(Connectable, log.Identified): See also: - :ref:`engines_toplevel` + :doc:`/core/engines` :ref:`connections_toplevel` diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 8c98ee3e49..ea8e5a36e2 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -170,9 +170,6 @@ def create_session(bind=None, **kwargs): def relationship(argument, secondary=None, **kwargs): """Provide a relationship of a primary Mapper to a secondary Mapper. - .. versionchanged:: 0.6 - :func:`relationship` is historically known as :func:`relation`. - This corresponds to a parent-child or associative table relationship. The constructed class is an instance of :class:`.RelationshipProperty`. @@ -450,7 +447,7 @@ def relationship(argument, secondary=None, **kwargs): * None - a synonym for 'noload' - Detailed discussion of loader strategies is at :ref:`loading_toplevel`. + Detailed discussion of loader strategies is at :doc:`/orm/loading`. :param load_on_pending=False: Indicates loading behavior for transient or pending parent objects. @@ -625,6 +622,9 @@ def relationship(argument, secondary=None, **kwargs): not compile into eager or lazy loaders properly. If this is the case, use an alternative method. + .. versionchanged:: 0.6 + :func:`relationship` was renamed from its previous name :func:`relation`. + """ return RelationshipProperty(argument, secondary=secondary, **kwargs) diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 9289c01e1e..b129685a28 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -747,7 +747,7 @@ class Query(object): foreign-key-to-primary-key criterion, will also use an operation equivalent to :meth:`~.Query.get` in order to retrieve the target value from the local identity map - before querying the database. See :ref:`loading_toplevel` + before querying the database. See :doc:`/orm/loading` for further details on relationship loading. :param ident: A scalar or tuple value representing @@ -1039,7 +1039,7 @@ class Query(object): Most supplied options regard changing how column- and relationship-mapped attributes are loaded. See the sections - :ref:`deferred` and :ref:`loading_toplevel` for reference + :ref:`deferred` and :doc:`/orm/loading` for reference documentation. """ diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 70d3bc0bb1..15a78b842c 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -428,7 +428,7 @@ class SessionTransaction(object): class Session(_SessionClassMethods): """Manages persistence operations for ORM-mapped objects. - The Session's usage paradigm is described at :ref:`session_toplevel`. + The Session's usage paradigm is described at :doc:`/orm/session`. """ diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index cf3238b153..dfa3ef8523 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -698,7 +698,7 @@ def with_polymorphic(base, classes, selectable=False, :param base: Base class to be aliased. - :param cls_or_mappers: a single class or mapper, or list of + :param classes: a single class or mapper, or list of class/mappers, which inherit from the base class. Alternatively, it may also be the string ``'*'``, in which case all descending mapped classes will be added to the FROM clause. diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 8c5b9b3d52..88f56e5ec0 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -128,6 +128,10 @@ class Operators(object): .. versionadded:: 0.8 - added the 'precedence' argument. + .. seealso:: + + :ref:`types_operators` + """ operator = custom_op(opstring, precedence) @@ -505,7 +509,7 @@ class ColumnOperators(Operators): def __radd__(self, other): """Implement the ``+`` operator in reverse. - See :meth:`__add__`. + See :meth:`.ColumnOperators.__add__`. """ return self.reverse_operate(add, other) @@ -513,7 +517,7 @@ class ColumnOperators(Operators): def __rsub__(self, other): """Implement the ``-`` operator in reverse. - See :meth:`__sub__`. + See :meth:`.ColumnOperators.__sub__`. """ return self.reverse_operate(sub, other) @@ -521,7 +525,7 @@ class ColumnOperators(Operators): def __rmul__(self, other): """Implement the ``*`` operator in reverse. - See :meth:`__mul__`. + See :meth:`.ColumnOperators.__mul__`. """ return self.reverse_operate(mul, other) @@ -529,7 +533,7 @@ class ColumnOperators(Operators): def __rdiv__(self, other): """Implement the ``/`` operator in reverse. - See :meth:`__div__`. + See :meth:`.ColumnOperators.__div__`. """ return self.reverse_operate(div, other) @@ -550,7 +554,7 @@ class ColumnOperators(Operators): if the parent object has non-string affinity. If the parent object has a string affinity, produces the concatenation operator, ``a || b`` - - see :meth:`concat`. + see :meth:`.ColumnOperators.concat`. """ return self.operate(add, other) @@ -598,7 +602,7 @@ class ColumnOperators(Operators): def __rtruediv__(self, other): """Implement the ``//`` operator in reverse. - See :meth:`__truediv__`. + See :meth:`.ColumnOperators.__truediv__`. """ return self.reverse_operate(truediv, other) diff --git a/test/orm/test_of_type.py b/test/orm/test_of_type.py index 28bcc6e811..17ffebc3d8 100644 --- a/test/orm/test_of_type.py +++ b/test/orm/test_of_type.py @@ -98,6 +98,19 @@ class _PolymorphicTestBase(object): ) def test_with_polymorphic_join_exec_contains_eager_one(self): + sess = Session() + def go(): + wp = with_polymorphic(Person, [Engineer, Manager], aliased=True) + eq_( + sess.query(Company).join( + Company.employees.of_type(wp) + ).order_by(Company.company_id, wp.person_id).\ + options(contains_eager(Company.employees.of_type(wp))).all(), + [self.c1, self.c2] + ) + self.assert_sql_count(testing.db, go, 1) + + def test_with_polymorphic_join_exec_contains_eager_two(self): sess = Session() def go(): wp = with_polymorphic(Person, [Engineer, Manager], aliased=True) -- 2.47.3