summaryrefslogtreecommitdiff
path: root/doc/build/orm
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-09-25 22:31:16 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2020-10-31 13:44:53 -0400
commit654b462d668a2ced4e87077b9babb2590acbf983 (patch)
tree8b6023480423e990c9bbca7c280cb1cb58e012fc /doc/build/orm
parent841eb216644202567ebddfc0badc51a3a35e98c3 (diff)
downloadsqlalchemy-review/mike_bayer/tutorial20.tar.gz
Add SelectBase.exists() method as it seems strange this is not available already. The Exists construct itself does not provide full SELECT-building capabilities so it makes sense this should be used more like a scalar_subquery. Make sure stream_results is getting set up when yield_per is used, for 2.0 style statements as well. this was hardcoded inside of Query.yield_per() and is now moved to take place within QueryContext. Change-Id: Icafcd4fd9b708772343d56edf40995c9e8f835d6
Diffstat (limited to 'doc/build/orm')
-rw-r--r--doc/build/orm/extensions/declarative/api.rst11
-rw-r--r--doc/build/orm/extensions/declarative/index.rst13
-rw-r--r--doc/build/orm/extensions/declarative/inheritance.rst3
-rw-r--r--doc/build/orm/loading_columns.rst13
-rw-r--r--doc/build/orm/loading_objects.rst22
-rw-r--r--doc/build/orm/loading_relationships.rst7
-rw-r--r--doc/build/orm/mapping_styles.rst2
-rw-r--r--doc/build/orm/query.rst4
-rw-r--r--doc/build/orm/queryguide.rst882
-rw-r--r--doc/build/orm/session_basics.rst12
-rw-r--r--doc/build/orm/tutorial.rst43
11 files changed, 968 insertions, 44 deletions
diff --git a/doc/build/orm/extensions/declarative/api.rst b/doc/build/orm/extensions/declarative/api.rst
index 6e413a07e..e41e735d3 100644
--- a/doc/build/orm/extensions/declarative/api.rst
+++ b/doc/build/orm/extensions/declarative/api.rst
@@ -21,13 +21,6 @@ API Reference
* :meth:`_orm.as_declarative`
-.. autofunction:: instrument_declarative
-
-.. autoclass:: AbstractConcreteBase
-
-.. autoclass:: ConcreteBase
-
-.. autoclass:: DeferredReflection
- :members:
-
+See :ref:`declarative_toplevel` for the remaining Declarative extension
+classes.
diff --git a/doc/build/orm/extensions/declarative/index.rst b/doc/build/orm/extensions/declarative/index.rst
index 36700f812..7ef2551c6 100644
--- a/doc/build/orm/extensions/declarative/index.rst
+++ b/doc/build/orm/extensions/declarative/index.rst
@@ -21,3 +21,16 @@ mapping API.
.. autoclass:: DeferredReflection
:members:
+
+.. these pages have all been integrated into the main ORM documentation
+ however are still here as placeholder docs with links to where they've moved
+
+.. toctree::
+ :hidden:
+
+ api
+ basic_use
+ inheritance
+ mixins
+ relationships
+ table_config \ No newline at end of file
diff --git a/doc/build/orm/extensions/declarative/inheritance.rst b/doc/build/orm/extensions/declarative/inheritance.rst
index 70148986b..d65cafd35 100644
--- a/doc/build/orm/extensions/declarative/inheritance.rst
+++ b/doc/build/orm/extensions/declarative/inheritance.rst
@@ -1,3 +1,6 @@
.. _declarative_inheritance:
+Declarative Inheritance
+=======================
+
See :ref:`inheritance_toplevel` for this section.
diff --git a/doc/build/orm/loading_columns.rst b/doc/build/orm/loading_columns.rst
index a0759e768..9566d0efc 100644
--- a/doc/build/orm/loading_columns.rst
+++ b/doc/build/orm/loading_columns.rst
@@ -1,3 +1,5 @@
+.. _loading_columns:
+
.. currentmodule:: sqlalchemy.orm
===============
@@ -277,11 +279,9 @@ Column Deferral API
Column Bundles
==============
-The :class:`.Bundle` may be used to query for groups of columns under one
+The :class:`_orm.Bundle` may be used to query for groups of columns under one
namespace.
-.. versionadded:: 0.9.0
-
The bundle allows columns to be grouped together::
from sqlalchemy.orm import Bundle
@@ -292,7 +292,7 @@ The bundle allows columns to be grouped together::
The bundle can be subclassed to provide custom behaviors when results
are fetched. The method :meth:`.Bundle.create_row_processor` is given
-the :class:`_query.Query` and a set of "row processor" functions at query execution
+the statement object and a set of "row processor" functions at query execution
time; these processor functions when given a result row will return the
individual attribute value, which can then be adapted into any kind of
return data structure. Below illustrates replacing the usual :class:`.Row`
@@ -309,6 +309,11 @@ return structure with a straight Python dictionary::
)
return proc
+.. note::
+
+ The :class:`_orm.Bundle` construct only applies to column expressions.
+ It does not apply to ORM attributes mapped using :func:`_orm.relationship`.
+
.. versionchanged:: 1.0
The ``proc()`` callable passed to the ``create_row_processor()``
diff --git a/doc/build/orm/loading_objects.rst b/doc/build/orm/loading_objects.rst
index b26b32087..3075ea9f4 100644
--- a/doc/build/orm/loading_objects.rst
+++ b/doc/build/orm/loading_objects.rst
@@ -1,14 +1,26 @@
-===============
-Loading Objects
-===============
+===============================
+Querying Data, Loading Objects
+===============================
-Notes and features regarding the general loading of mapped objects.
+The following sections refer to techniques for emitting SELECT statements within
+an ORM context. This involves primarily statements that return instances of
+ORM mapped objects, but also involves calling forms that deliver individual
+column or groups of columns as well.
-For an in-depth introduction to querying with the SQLAlchemy ORM, please see the :ref:`ormtutorial_toplevel`.
+For an introduction to querying with the SQLAlchemy ORM, one of the
+following tutorials shoud be consulted:
+
+* :doc:`/tutorial/index` - for :term:`2.0 style` usage
+
+* :doc:`/orm/tutorial` - for :term:`1.x style` usage.
+
+As SQLAlchemy 1.4 represents a transition from 1.x to 2.0 style, the below
+sections are currently mixed as far as which style they are using.
.. toctree::
:maxdepth: 3
+ queryguide
loading_columns
loading_relationships
inheritance_loading
diff --git a/doc/build/orm/loading_relationships.rst b/doc/build/orm/loading_relationships.rst
index 5a0902945..4b436f9c2 100644
--- a/doc/build/orm/loading_relationships.rst
+++ b/doc/build/orm/loading_relationships.rst
@@ -66,6 +66,7 @@ The primary forms of relationship loading are:
uncommon loader option.
+.. _relationship_lazy_option:
Configuring Loader Strategies at Mapping Time
---------------------------------------------
@@ -776,7 +777,7 @@ order to load related associations:
.. sourcecode:: python+sql
>>> jack = session.query(User).\
- ... options(selectinload('addresses')).\
+ ... options(selectinload(User.addresses)).\
... filter(or_(User.name == 'jack', User.name == 'ed')).all()
{opensql}SELECT
users.id AS users_id,
@@ -818,7 +819,7 @@ loaded on the parent objects and would otherwise need to be loaded:
.. sourcecode:: python+sql
>>> session.query(Address).\
- ... options(selectinload('user')).all()
+ ... options(selectinload(Address.user)).all()
{opensql}SELECT
addresses.id AS addresses_id,
addresses.email_address AS addresses_email_address,
@@ -1204,7 +1205,7 @@ to a string SQL statement::
Creating Custom Load Rules
--------------------------
-.. warning:: This is an advanced technique! Great care and testing
+.. deepalchemy:: This is an advanced technique! Great care and testing
should be applied.
The ORM has various edge cases where the value of an attribute is locally
diff --git a/doc/build/orm/mapping_styles.rst b/doc/build/orm/mapping_styles.rst
index 5e3c5154a..b88934094 100644
--- a/doc/build/orm/mapping_styles.rst
+++ b/doc/build/orm/mapping_styles.rst
@@ -588,6 +588,8 @@ Mapped Class Behavior
Across all styles of mapping using the :class:`_orm.registry` object,
the following behaviors are common:
+.. _mapped_class_default_constructor:
+
Default Constructor
-------------------
diff --git a/doc/build/orm/query.rst b/doc/build/orm/query.rst
index 592004e86..d7711671c 100644
--- a/doc/build/orm/query.rst
+++ b/doc/build/orm/query.rst
@@ -38,10 +38,10 @@ ORM-Specific Query Constructs
.. autoclass:: sqlalchemy.orm.util.AliasedInsp
-.. autoclass:: sqlalchemy.orm.util.Bundle
+.. autoclass:: sqlalchemy.orm.Bundle
:members:
-.. autoclass:: sqlalchemy.orm.strategy_options.Load
+.. autoclass:: sqlalchemy.orm.Load
:members:
.. autofunction:: sqlalchemy.orm.with_loader_criteria
diff --git a/doc/build/orm/queryguide.rst b/doc/build/orm/queryguide.rst
new file mode 100644
index 000000000..5576882a6
--- /dev/null
+++ b/doc/build/orm/queryguide.rst
@@ -0,0 +1,882 @@
+.. highlight:: pycon+sql
+
+.. _queryguide_toplevel:
+
+==================
+ORM Querying Guide
+==================
+
+This section provides an overview of emitting queries with the
+SQLAlchemy ORM using :term:`2.0 style` usage.
+
+Readers of this section should be familiar with the SQLAlchemy overview
+at :ref:`unified_tutorial`, and in particular most of the content here expands
+upon the content at :ref:`tutorial_selecting_data`.
+
+
+.. Setup code, not for display
+
+ >>> from sqlalchemy import create_engine
+ >>> engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True)
+ >>> from sqlalchemy import MetaData, Table, Column, Integer, String
+ >>> metadata = MetaData()
+ >>> user_table = Table(
+ ... "user_account",
+ ... metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('name', String(30)),
+ ... Column('fullname', String)
+ ... )
+ >>> from sqlalchemy import ForeignKey
+ >>> address_table = Table(
+ ... "address",
+ ... metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('user_id', None, ForeignKey('user_account.id')),
+ ... Column('email_address', String, nullable=False)
+ ... )
+ >>> orders_table = Table(
+ ... "user_order",
+ ... metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('user_id', None, ForeignKey('user_account.id')),
+ ... Column('email_address', String, nullable=False)
+ ... )
+ >>> order_items_table = Table(
+ ... "order_items",
+ ... metadata,
+ ... Column("order_id", ForeignKey("user_order.id"), primary_key=True),
+ ... Column("item_id", ForeignKey("item.id"), primary_key=True)
+ ... )
+ >>> items_table = Table(
+ ... "item",
+ ... metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('name', String),
+ ... Column('description', String)
+ ... )
+ >>> metadata.create_all(engine)
+ BEGIN (implicit)
+ ...
+ >>> from sqlalchemy.orm import declarative_base
+ >>> Base = declarative_base()
+ >>> from sqlalchemy.orm import relationship
+ >>> class User(Base):
+ ... __table__ = user_table
+ ...
+ ... addresses = relationship("Address", back_populates="user")
+ ... orders = relationship("Order")
+ ...
+ ... def __repr__(self):
+ ... return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
+
+ >>> class Address(Base):
+ ... __table__ = address_table
+ ...
+ ... user = relationship("User", back_populates="addresses")
+ ...
+ ... def __repr__(self):
+ ... return f"Address(id={self.id!r}, email_address={self.email_address!r})"
+
+ >>> class Order(Base):
+ ... __table__ = orders_table
+ ... items = relationship("Item", secondary=order_items_table)
+
+ >>> class Item(Base):
+ ... __table__ = items_table
+
+ >>> conn = engine.connect()
+ >>> from sqlalchemy.orm import Session
+ >>> session = Session(conn)
+ >>> session.add_all([
+ ... User(name="spongebob", fullname="Spongebob Squarepants", addresses=[
+ ... Address(email_address="spongebob@sqlalchemy.org")
+ ... ]),
+ ... User(name="sandy", fullname="Sandy Cheeks", addresses=[
+ ... Address(email_address="sandy@sqlalchemy.org"),
+ ... Address(email_address="squirrel@squirrelpower.org")
+ ... ]),
+ ... User(name="patrick", fullname="Patrick Star", addresses=[
+ ... Address(email_address="pat999@aol.com")
+ ... ]),
+ ... User(name="squidward", fullname="Squidward Tentacles", addresses=[
+ ... Address(email_address="stentcl@sqlalchemy.org")
+ ... ]),
+ ... User(name="ehkrabs", fullname="Eugene H. Krabs"),
+ ... ])
+ >>> session.commit()
+ BEGIN ...
+ >>> conn.begin()
+ BEGIN ...
+
+
+SELECT statements
+=================
+
+SELECT statements are produced by the :func:`_sql.select` function which
+returns a :class:`_sql.Select` object::
+
+ >>> from sqlalchemy import select
+ >>> stmt = select(User).where(User.name == 'spongebob')
+
+To invoke a :class:`_sql.Select` with the ORM, it is passed to
+:meth:`_orm.Session.execute`::
+
+ {sql}>>> result = session.execute(stmt)
+ SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ WHERE user_account.name = ?
+ [...] ('spongebob',){stop}
+ >>> for user_obj in result.scalars():
+ ... print(f"{user_obj.name} {user_obj.fullname}")
+ spongebob Spongebob Squarepants
+
+
+
+.. _orm_queryguide_select_columns:
+
+Selecting ORM Entities and Attributes
+--------------------------------------
+
+The :func:`_sql.select` construct accepts ORM entities, including mapped
+classes as well as class-level attributes representing mapped columns, which
+are converted into ORM-annotated :class:`_sql.FromClause` and
+:class:`_sql.ColumnElement` elements at construction time.
+
+A :class:`_sql.Select` object that contains ORM-annotated entities is normally
+executed using a :class:`_orm.Session` object, and not a :class:`_future.Connection`
+object, so that ORM-related features may take effect.
+
+Below we select from the ``User`` entity, producing a :class:`_sql.Select`
+that selects from the mapped :class:`_schema.Table` to which ``User`` is mapped::
+
+ {sql}>>> result = session.execute(select(User).order_by(User.id))
+ SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account ORDER BY user_account.id
+ [...] (){stop}
+
+When selecting from ORM entities, the entity itself is returned in the result
+as a single column value; for example above, the :class:`_engine.Result`
+returns :class:`_engine.Row` objects that have just a single column, that column
+holding onto a ``User`` object::
+
+ >>> result.fetchone()
+ (User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)
+
+When selecting a list of single-column ORM entities, it is typical to skip
+the generation of :class:`_engine.Row` objects and instead receive
+ORM entities directly, which is achieved using the :meth:`_engine.Result.scalars`
+method::
+
+ >>> result.scalars().all()
+ [User(id=2, name='sandy', fullname='Sandy Cheeks'),
+ User(id=3, name='patrick', fullname='Patrick Star'),
+ User(id=4, name='squidward', fullname='Squidward Tentacles'),
+ User(id=5, name='ehkrabs', fullname='Eugene H. Krabs')]
+
+ORM Entities are named in the result row based on their class name,
+such as below where we SELECT from both ``User`` and ``Address`` at the
+same time::
+
+ >>> stmt = select(User, Address).join(User.addresses).order_by(User.id, Address.id)
+
+ {sql}>>> for row in session.execute(stmt):
+ ... print(f"{row.User.name} {row.Address.email_address}")
+ SELECT user_account.id, user_account.name, user_account.fullname,
+ address.id AS id_1, address.user_id, address.email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ ORDER BY user_account.id, address.id
+ [...] (){stop}
+ spongebob spongebob@sqlalchemy.org
+ sandy sandy@sqlalchemy.org
+ sandy squirrel@squirrelpower.org
+ patrick pat999@aol.com
+ squidward stentcl@sqlalchemy.org
+
+
+Selecting Individual Attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The attributes on a mapped class, such as ``User.name`` and ``Address.email_address``,
+have a similar behavior as that of the entity class itself such as ``User``
+in that they are automatically converted into ORM-annotated Core objects
+when passed to :func:`_sql.select`. They may be used in the same way
+as table columns are used::
+
+ {sql}>>> result = session.execute(
+ ... select(User.name, Address.email_address).
+ ... join(User.addresses).
+ ... order_by(User.id, Address.id)
+ ... )
+ SELECT user_account.name, address.email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ ORDER BY user_account.id, address.id
+ [...] (){stop}
+
+ORM attributes, themselves known as :class:`_orm.InstrumentedAttribute`
+objects, can be used in the same way as any :class:`_sql.ColumnElement`,
+and are delivered in result rows just the same way, such as below
+where we refer to their values by column name within each row::
+
+ >>> for row in result:
+ ... print(f"{row.name} {row.email_address}")
+ spongebob spongebob@sqlalchemy.org
+ sandy sandy@sqlalchemy.org
+ sandy squirrel@squirrelpower.org
+ patrick pat999@aol.com
+ squidward stentcl@sqlalchemy.org
+
+Grouping Selected Attributes with Bundles
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The :class:`_orm.Bundle` construct is an extensible ORM-only construct that
+allows sets of column expressions to be grouped in result rows::
+
+ >>> from sqlalchemy.orm import Bundle
+ >>> stmt = select(
+ ... Bundle("user", User.name, User.fullname),
+ ... Bundle("email", Address.email_address)
+ ... ).join_from(User, Address)
+ {sql}>>> for row in session.execute(stmt):
+ ... print(f"{row.user.name} {row.email.email_address}")
+ SELECT user_account.name, user_account.fullname, address.email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ [...] (){stop}
+ spongebob spongebob@sqlalchemy.org
+ sandy sandy@sqlalchemy.org
+ sandy squirrel@squirrelpower.org
+ patrick pat999@aol.com
+ squidward stentcl@sqlalchemy.org
+
+
+The :class:`_orm.Bundle` is potentially useful for creating lightweight
+views as well as custom column groupings such as mappings.
+
+.. seealso::
+
+ :ref:`bundles` - in the ORM loading documentation.
+
+
+Selecting ORM Aliases
+^^^^^^^^^^^^^^^^^^^^^
+
+As discussed in the tutorial at :ref:`tutorial_using_aliases`, to create a
+SQL alias of an ORM entity is achieved using the :func:`_orm.aliased`
+construct against a mapped class::
+
+ >>> from sqlalchemy.orm import aliased
+ >>> u1 = aliased(User)
+ >>> print(select(u1).order_by(u1.id))
+ {opensql}SELECT user_account_1.id, user_account_1.name, user_account_1.fullname
+ FROM user_account AS user_account_1 ORDER BY user_account_1.id
+
+As is the case when using :meth:`_schema.Table.alias`, the SQL alias
+is anonymously named. For the case of selecting the entity from a row
+with an explicit name, the :paramref:`_orm.aliased.name` parameter may be
+passed as well::
+
+ >>> from sqlalchemy.orm import aliased
+ >>> u1 = aliased(User, name="u1")
+ >>> stmt = select(u1).order_by(u1.id)
+ {sql}>>> row = session.execute(stmt).first()
+ SELECT u1.id, u1.name, u1.fullname
+ FROM user_account AS u1 ORDER BY u1.id
+ [...] (){stop}
+ >>> print(f"{row.u1.name}")
+ spongebob
+
+The :class:`_orm.aliased` construct is also central to making use of subqueries
+with the ORM; the section :ref:`orm_queryguide_subqueries` discusses this further.
+
+.. _orm_queryguide_selecting_text:
+
+Getting ORM Results from Textual and Core Statements
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ORM supports loading of entities from SELECT statements that come from other
+sources. The typical use case is that of a textual SELECT statement, which
+in SQLAlchemy is represented using the :func:`_sql.text` construct. The
+:func:`_sql.text` construct, once constructed, can be augmented with
+information
+about the ORM-mapped columns that the statement would load; this can then be
+associated with the ORM entity itself so that ORM objects can be loaded based
+on this statement.
+
+Given a textual SQL statement we'd like to load from::
+
+ >>> from sqlalchemy import text
+ >>> textual_sql = text("SELECT id, name, fullname FROM user_account ORDER BY id")
+
+We can add column information to the statement by using the
+:meth:`_sql.TextClause.columns` method; when this method is invoked, the
+:class:`_sql.TextClause` object is converted into a :class:`_sql.TextualSelect`
+object, which takes on a role that is comparable to the :class:`_sql.Select`
+construct. The :meth:`_sql.TextClause.columns` method
+is typically passed :class:`_schema.Column` objects or equivalent, and in this
+case we can make use of the ORM-mapped attributes on the ``User`` class
+directly::
+
+ >>> textual_sql = textual_sql.columns(User.id, User.name, User.fullname)
+
+We now have an ORM-configured SQL construct that as given, can load the "id",
+"name" and "fullname" columns separately. To use this SELECT statement as a
+source of complete ``User`` entities instead, we can link these columns to a
+regular ORM-enabled
+:class:`_sql.Select` construct using the :meth:`_sql.Select.from_statement`
+method::
+
+ >>> # using from_statement()
+ >>> orm_sql = select(User).from_statement(textual_sql)
+ >>> for user_obj in session.execute(orm_sql).scalars():
+ ... print(user_obj)
+ {opensql}SELECT id, name, fullname FROM user_account ORDER BY id
+ [...] (){stop}
+ User(id=1, name='spongebob', fullname='Spongebob Squarepants')
+ User(id=2, name='sandy', fullname='Sandy Cheeks')
+ User(id=3, name='patrick', fullname='Patrick Star')
+ User(id=4, name='squidward', fullname='Squidward Tentacles')
+ User(id=5, name='ehkrabs', fullname='Eugene H. Krabs')
+
+The same :class:`_sql.TextualSelect` object can also be converted into
+a subquery using the :meth:`_sql.TextualSelect.subquery` method,
+and linked to the ``User`` entity to it using the :func:`_orm.aliased`
+construct, in a similar manner as discussed below in :ref:`orm_queryguide_subqueries`::
+
+ >>> # using aliased() to select from a subquery
+ >>> orm_subquery = aliased(User, textual_sql.subquery())
+ >>> stmt = select(orm_subquery)
+ >>> for user_obj in session.execute(stmt).scalars():
+ ... print(user_obj)
+ {opensql}SELECT anon_1.id, anon_1.name, anon_1.fullname
+ FROM (SELECT id, name, fullname FROM user_account ORDER BY id) AS anon_1
+ [...] (){stop}
+ User(id=1, name='spongebob', fullname='Spongebob Squarepants')
+ User(id=2, name='sandy', fullname='Sandy Cheeks')
+ User(id=3, name='patrick', fullname='Patrick Star')
+ User(id=4, name='squidward', fullname='Squidward Tentacles')
+ User(id=5, name='ehkrabs', fullname='Eugene H. Krabs')
+
+The difference between using the :class:`_sql.TextualSelect` directly with
+:meth:`_sql.Select.from_statement` versus making use of :func:`_sql.aliased`
+is that in the former case, no subuqery is produced in the resulting SQL.
+This can in some scenarios be advantageous from a performance or complexity
+perspective.
+
+.. _orm_queryguide_joins:
+
+Joins
+-----
+
+The :meth:`_sql.Select.join` and :meth:`_sql.Select.join_from` methods
+are used to construct SQL JOINs against a SELECT statement.
+
+This section will detail ORM use cases for these methods. For a general
+overview of their use from a Core perspective, see :ref:`tutorial_select_join`
+in the :ref:`unified_tutorial`.
+
+The usage of :meth:`_sql.Select.join` in an ORM context for :term:`2.0 style`
+queries is mostly equivalent, minus legacy use cases, to the usage of the
+:meth:`_orm.Query.join` method in :term:`1.x style` queries.
+
+Simple Relationship Joins
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Consider a mapping between two classes ``User`` and ``Address``,
+with a relationship ``User.addresses`` representing a collection
+of ``Address`` objects associated with each ``User``. The most
+common usage of :meth:`_sql.Select.join`
+is to create a JOIN along this
+relationship, using the ``User.addresses`` attribute as an indicator
+for how this should occur::
+
+ >>> stmt = select(User).join(User.addresses)
+
+Where above, the call to :meth:`_sql.Select.join` along
+``User.addresses`` will result in SQL approximately equivalent to::
+
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account JOIN address ON user_account.id = address.user_id
+
+In the above example we refer to ``User.addresses`` as passed to
+:meth:`_sql.Select.join` as the "on clause", that is, it indicates
+how the "ON" portion of the JOIN should be constructed.
+
+Chaining Multiple Joins
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+To construct a chain of joins, multiple :meth:`_sql.Select.join` calls may be
+used. The relationship-bound attribute implies both the left and right side of
+the join at once. Consider additional entities ``Order`` and ``Item``, where
+the ``User.orders`` relationship refers to the ``Order`` entity, and the
+``Order.items`` relationship refers to the ``Item`` entity, via an association
+table ``order_items``. Two :meth:`_sql.Select.join` calls will result in
+a JOIN first from ``User`` to ``Order``, and a second from ``Order`` to
+``Item``. However, since ``Order.items`` is a :ref:`many to many <relationships_many_to_many>`
+relationship, it results in two separate JOIN elements, for a total of three
+JOIN elements in the resulting SQL::
+
+ >>> stmt = (
+ ... select(User).
+ ... join(User.orders).
+ ... join(Order.items)
+ ... )
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ JOIN user_order ON user_account.id = user_order.user_id
+ JOIN order_items AS order_items_1 ON user_order.id = order_items_1.order_id
+ JOIN item ON item.id = order_items_1.item_id
+
+.. tip::
+
+ as seen in the above example, **the order in which each call to the join()
+ method occurs is important**. Query would not, for example, know how to
+ join correctly if we were to specify ``User``, then ``Item``, then
+ ``Order``, in our chain of joins; in such a case, depending on the
+ arguments passed, it may raise an error that it doesn't know how to join,
+ or it may produce invalid SQL in which case the database will raise an
+ error. In correct practice, the :meth:`_sql.Select.join` method is invoked
+ in such a way that lines up with how we would want the JOIN clauses in SQL
+ to be rendered, and each call should represent a clear link from what
+ precedes it.
+
+Joins to a Target Entity or Selectable
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A second form of :meth:`_sql.Select.join` allows any mapped entity or core
+selectable construct as a target. In this usage, :meth:`_sql.Select.join`
+will attempt to **infer** the ON clause for the JOIN, using the natural foreign
+key relationship between two entities::
+
+ >>> stmt = select(User).join(Address)
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account JOIN address ON user_account.id = address.user_id
+
+In the above calling form, :meth:`_sql.Select.join` is called upon to infer
+the "on clause" automatically. This calling form will ultimately raise
+an error if either there are no :class:`_schema.ForeignKeyConstraint` setup
+between the two mapped :class:`_schema.Table` constructs, or if there are multiple
+:class:`_schema.ForeignKeyConstraint` linakges between them such that the
+appropriate constraint to use is ambiguous.
+
+.. note:: When making use of :meth:`_sql.Select.join` or :meth:`_sql.Select.join_from`
+ without indicating an ON clause, ORM
+ configured :func:`_orm.relationship` constructs are **not taken into account**.
+ Only the configured :class:`_schema.ForeignKeyConstraint` relationships between
+ the entities at the level of the mapped :class:`_schema.Table` objects are consulted
+ when an attempt is made to infer an ON clause for the JOIN.
+
+Joins to a Target with an ON Clause
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The third calling form allows both the target entity as well
+as the ON clause to be passed explicitly. A example that includes
+a SQL expression as the ON clause is as follows::
+
+ >>> stmt = select(User).join(Address, User.id==Address.user_id)
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account JOIN address ON user_account.id = address.user_id
+
+The expression-based ON clause may also be the relationship-bound
+attribute; this form in fact states the target of ``Address`` twice, however
+this is accepted::
+
+ >>> stmt = select(User).join(Address, User.addresses)
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account JOIN address ON user_account.id = address.user_id
+
+The above syntax has more functionality if we use it in terms of aliased
+entities. The default target for ``User.addresses`` is the ``Address``
+class, however if we pass aliased forms using :func:`_orm.aliased`, the
+:func:`_orm.aliased` form will be used as the target, as in the example
+below::
+
+ >>> a1 = aliased(Address)
+ >>> a2 = aliased(Address)
+ >>> stmt = (
+ ... select(User).
+ ... join(a1, User.addresses).
+ ... join(a2, User.addresses).
+ ... where(a1.email_address == 'ed@foo.com').
+ ... where(a2.email_address == 'ed@bar.com')
+ ... )
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ JOIN address AS address_1 ON user_account.id = address_1.user_id
+ JOIN address AS address_2 ON user_account.id = address_2.user_id
+ WHERE address_1.email_address = :email_address_1
+ AND address_2.email_address = :email_address_2
+
+When using relationship-bound attributes, the target entity can also be
+substituted with an aliased entity by using the
+:meth:`_orm.PropComparator.of_type` method. The same example using
+this method would be::
+
+ >>> stmt = (
+ ... select(User).
+ ... join(User.addresses.of_type(a1)).
+ ... join(User.addresses.of_type(a2)).
+ ... where(a1.email_address == 'ed@foo.com').
+ ... where(a2.email_address == 'ed@bar.com')
+ ... )
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ JOIN address AS address_1 ON user_account.id = address_1.user_id
+ JOIN address AS address_2 ON user_account.id = address_2.user_id
+ WHERE address_1.email_address = :email_address_1
+ AND address_2.email_address = :email_address_2
+
+.. _orm_queryguide_join_on_augmented:
+
+Augmenting Built-in ON Clauses
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As a substitute for providing a full custom ON condition for an
+existing relationship, the :meth:`_orm.PropComparator.and_` function
+may be applied to a relationship attribute to augment additional
+criteria into the ON clause; the additional criteria will be combined
+with the default criteria using AND. Below, the ON criteria between
+``user_account`` and ``address`` contains two separate elements joined
+by ``AND``, the first one being the natural join along the foreign key,
+and the second being a custom limiting criteria::
+
+ >>> stmt = (
+ ... select(User).
+ ... join(User.addresses.and_(Address.email_address != 'foo@bar.com'))
+ ... )
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ JOIN address ON user_account.id = address.user_id
+ AND address.email_address != :email_address_1
+
+.. seealso::
+
+ The :meth:`_orm.PropComparator.and_` method also works with loader
+ strategies. See the section :ref:`loader_option_criteria` for an example.
+
+.. _orm_queryguide_subqueries:
+
+Joining to Subqueries
+^^^^^^^^^^^^^^^^^^^^^^^
+
+The target of a join may be any "selectable" entity which usefully includes
+subuqeries. When using the ORM, it is typical
+that these targets are stated in terms of an
+:func:`_orm.aliased` construct, but this is not strictly required particularly
+if the joined entity is not being returned in the results. For example, to join from the
+``User`` entity to the ``Address`` entity, where the ``Address`` entity
+is represented as a row limited subquery, we first construct a :class:`_sql.Subquery`
+object using :meth:`_sql.Select.subquery`, which may then be used as the
+target of the :meth:`_sql.Select.join` method::
+
+ >>> subq = (
+ ... select(Address).
+ ... where(Address.email_address == 'pat999@aol.com').
+ ... subquery()
+ ... )
+ >>> stmt = select(User).join(subq, User.id == subq.c.user_id)
+ >>> print(stmt)
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ JOIN (SELECT address.id AS id,
+ address.user_id AS user_id, address.email_address AS email_address
+ FROM address
+ WHERE address.email_address = :email_address_1) AS anon_1
+ ON user_account.id = anon_1.user_id{stop}
+
+The above SELECT statement when invoked via :meth:`_orm.Session.execute`
+will return rows that contain ``User`` entities, but not ``Address`` entities.
+In order to add ``Address`` entities to the set of entities that would be
+returned in result sets, we construct an :func:`_orm.aliased` object against
+the ``Address`` entity and the custom subquery. Note we also apply a name
+``"address"`` to the :func:`_orm.aliased` construct so that we may
+refer to it by name in the result row::
+
+
+ >>> address_subq = aliased(Address, subq, name="address")
+ >>> stmt = select(User, address_subq).join(address_subq)
+ >>> for row in session.execute(stmt):
+ ... print(f"{row.User} {row.address}")
+ {opensql}SELECT user_account.id, user_account.name, user_account.fullname,
+ anon_1.id AS id_1, anon_1.user_id, anon_1.email_address
+ FROM user_account
+ JOIN (SELECT address.id AS id,
+ address.user_id AS user_id, address.email_address AS email_address
+ FROM address
+ WHERE address.email_address = ?) AS anon_1 ON user_account.id = anon_1.user_id
+ [...] ('pat999@aol.com',){stop}
+ User(id=3, name='patrick', fullname='Patrick Star') Address(id=4, email_address='pat999@aol.com')
+
+The same subquery may be referred towards by multiple entities as well,
+for a subquery that represents more than one entity. The subquery itself
+will remain unique within the statement, while the entities that are linked
+to it using :class:`_orm.aliased` refer to distinct sets of columns::
+
+ >>> user_address_subq = (
+ ... select(User.id, User.name, Address.id, Address.email_address).
+ ... join_from(User, Address).
+ ... where(Address.email_address.in_(['pat999@aol.com', 'squirrel@squirrelpower.org'])).
+ ... subquery()
+ ... )
+ >>> user_alias = aliased(User, user_address_subq, name="user")
+ >>> address_alias = aliased(Address, user_address_subq, name="address")
+ >>> stmt = select(user_alias, address_alias).where(user_alias.name == 'sandy')
+ >>> for row in session.execute(stmt):
+ ... print(f"{row.user} {row.address}")
+ {opensql}SELECT anon_1.id, anon_1.name, anon_1.id_1, anon_1.email_address
+ FROM (SELECT user_account.id AS id, user_account.name AS name, address.id AS id_1, address.email_address AS email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ WHERE address.email_address IN (?, ?)) AS anon_1
+ WHERE anon_1.name = ?
+ [...] ('pat999@aol.com', 'squirrel@squirrelpower.org', 'sandy'){stop}
+ User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='squirrel@squirrelpower.org')
+
+
+
+Controlling what to Join From
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In cases where the left side of the current state of
+:class:`_sql.Select` is not in line with what we want to join from,
+the :meth:`_sql.Select.join_from` method may be used::
+
+ >>> stmt = select(Address).join_from(User, User.addresses).where(User.name == 'sandy')
+ >>> print(stmt)
+ SELECT address.id, address.user_id, address.email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ WHERE user_account.name = :name_1
+
+The :meth:`_sql.Select.join_from` method accepts two or three arguments, either
+in the form ``<join from>, <onclause>``, or ``<join from>, <join to>,
+[<onclause>]``::
+
+ >>> stmt = select(Address).join_from(User, Address).where(User.name == 'sandy')
+ >>> print(stmt)
+ SELECT address.id, address.user_id, address.email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ WHERE user_account.name = :name_1
+
+To set up the initial FROM clause for a SELECT such that :meth:`_sql.Select.join`
+can be used subsequent, the :meth:`_sql.Select.select_from` method may also
+be used::
+
+
+ >>> stmt = select(Address).select_from(User).join(User.addresses).where(User.name == 'sandy')
+ >>> print(stmt)
+ SELECT address.id, address.user_id, address.email_address
+ FROM user_account JOIN address ON user_account.id = address.user_id
+ WHERE user_account.name = :name_1
+
+
+
+
+Special Relationship Operators
+------------------------------
+
+As detailed in the :ref:`unified_tutorial` at
+:ref:`tutorial_select_relationships`, ORM attributes mapped by
+:func:`_orm.relationship` may be used in a variety of ways as SQL construction
+helpers. In addition to the above documentation on
+:ref:`orm_queryguide_joins`, relationships may produce criteria to be used in
+the WHERE clause as well. See the linked sections below.
+
+.. seealso::
+
+ Sections in the :ref:`tutorial_orm_related_objects` section of the
+ :ref:`unified_tutorial`:
+
+ * :ref:`tutorial_relationship_exists` - helpers to generate EXISTS clauses
+ using :func:`_orm.relationship`
+
+
+ * :ref:`tutorial_relationship_operators` - helpers to create comparisons in
+ terms of a :func:`_orm.relationship` in reference to a specific object
+ instance
+
+
+ORM Loader Options
+-------------------
+
+Loader options are objects that are passed to the :meth:`_sql.Select.options`
+method which affect the loading of both column and relationship-oriented
+attributes. The majority of loader options descend from the :class:`_orm.Load`
+hierarchy. For a complete overview of using loader options, see the linked
+sections below.
+
+.. seealso::
+
+ * :ref:`loading_columns` - details mapper and loading options that affect
+ how column and SQL-expression mapped attributes are loaded
+
+ * :ref:`loading_toplevel` - details relationship and loading options that
+ affect how :func:`_orm.relationship` mapped attributes are loaded
+
+
+ORM Execution Options
+---------------------
+
+Execution options are keyword arguments that are passed to an
+"execution_options" method, which take place at the level of statement
+execution. The primary "execution option" method is in Core at
+:meth:`_engine.Connection.execution_options`. In the ORM, execution options may
+also be passed to :meth:`_orm.Session.execute` using the
+:paramref:`_orm.Session.execute.execution_options` parameter. Perhaps more
+succinctly, most execution options, including those specific to the ORM, can be
+assigned to a statement directly, using the
+:meth:`_sql.Executable.execution_options` method, so that the options may be
+associated directly with the statement instead of being configured separately.
+The examples below will use this form.
+
+.. _orm_queryguide_populate_existing:
+
+Populate Existing
+^^^^^^^^^^^^^^^^^^
+
+The ``populate_existing`` execution option ensures that for all rows
+loaded, the corresponding instances in the :class:`_orm.Session` will
+be fully refreshed, erasing any existing data within the objects
+(including pending changes) and replacing with the data loaded from the
+result.
+
+Example use looks like::
+
+ >>> stmt = select(User).execution_options(populate_existing=True)
+ {sql}>>> result = session.execute(stmt)
+ SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ ...
+
+Normally, ORM objects are only loaded once, and if they are matched up
+to the primary key in a subsequent result row, the row is not applied to the
+object. This is both to preserve pending, unflushed changes on the object
+as well as to avoid the overhead and complexity of refreshing data which
+is already there. The :class:`_orm.Session` assumes a default working
+model of a highly isolated transaction, and to the degree that data is
+expected to change within the transaction outside of the local changes being
+made, those use cases would be handled using explicit steps such as this method.
+
+Another use case for ``populate_existing`` is in support of various
+attribute loading features that can change how an attribute is loaded on
+a per-query basis. Options for which this apply include:
+
+* The :func:`_orm.with_expression` option
+
+* The :meth:`_orm.PropComparator.and_` method that can modify what a loader
+ strategy loads
+
+* The :func:`_orm.contains_eager` option
+
+* The :func:`_orm.with_loader_criteria` option
+
+The ``populate_existing`` execution option is equvialent to the
+:meth:`_orm.Query.populate_existing` method in :term:`1.x style` ORM queries.
+
+.. seealso::
+
+ :ref:`faq_session_identity` - in :doc:`/faq/index`
+
+ :ref:`session_expire` - in the ORM :class:`_orm.Session`
+ documentation
+
+.. _orm_queryguide_autoflush:
+
+Autoflush
+^^^^^^^^^^
+
+This option when passed as ``False`` will cause the :class:`_orm.Session`
+to not invoke the "autoflush" step. It's equivalent to using the
+:attr:`_orm.Session.no_autoflush` context manager to disable autoflush::
+
+ >>> stmt = select(User).execution_options(autoflush=False)
+ {sql}>>> session.execute(stmt)
+ SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ ...
+
+This option will also work on ORM-enabled :class:`_sql.Update` and
+:class:`_sql.Delete` queries.
+
+The ``autoflush`` execution option is equvialent to the
+:meth:`_orm.Query.autoflush` method in :term:`1.x style` ORM queries.
+
+.. seealso::
+
+ :ref:`session_flushing`
+
+.. _orm_queryguide_yield_per:
+
+Yield Per
+^^^^^^^^^^
+
+The ``yield_per`` execution option is an integer value which will cause the
+:class:`_engine.Result` to yield only a fixed count of rows at a time. It is
+often useful to use with a result partitioning method such as
+:meth:`_engine.Result.partitions`, e.g.::
+
+ >>> stmt = select(User).execution_options(yield_per=10)
+ {sql}>>> for partition in session.execute(stmt).partitions(10):
+ ... for row in partition:
+ ... print(row)
+ SELECT user_account.id, user_account.name, user_account.fullname
+ FROM user_account
+ [...] (){stop}
+ (User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)
+ ...
+
+The purpose of this method is when fetching very large result sets
+(> 10K rows), to batch results in sub-collections and yield them
+out partially, so that the Python interpreter doesn't need to declare
+very large areas of memory which is both time consuming and leads
+to excessive memory use. The performance from fetching hundreds of
+thousands of rows can often double when a suitable yield-per setting
+(e.g. approximately 1000) is used, even with DBAPIs that buffer
+rows (which are most).
+
+When ``yield_per`` is used, the
+:paramref:`_engine.Connection.execution_options.stream_results` option is also
+set for the Core execution, so that a streaming / server side cursor will be
+used if the backend supports it (currently known are
+:mod:`~sqlalchemy.dialects.postgresql.psycopg2`,
+:mod:`~sqlalchemy.dialects.mysql.mysqldb` and
+:mod:`~sqlalchemy.dialects.mysql.pymysql`. Other backends will pre buffer all
+rows. The memory use of raw database rows is much less than that of an
+ORM-mapped object, but should still be taken into consideration when
+benchmarking.
+
+
+The ``yield_per`` execution option **is not compatible subqueryload eager
+loading or joinedload eager loading when using collections**. It is
+potentially compatible with "select in" eager loading, **provided the database
+driver supports multiple, independent cursors** (pysqlite and psycopg2 are
+known to work, MySQL and SQL Server ODBC drivers do not).
+
+The ``yield_per`` execution option is equvialent to the
+:meth:`_orm.Query.yield_per` method in :term:`1.x style` ORM queries.
+
+.. seealso::
+
+ :ref:`engine_stream_results`
+
+
+
+
+ORM Update / Delete with Arbitrary WHERE clause
+================================================
+
+The :meth:`_orm.Session.execute` method, in addition to handling ORM-enabled
+:class:`_sql.Select` objects, can also accommodate ORM-enabled
+:class:`_sql.Update` and :class:`_sql.Delete` objects, which UPDATE or DELETE
+any number of database rows while also being able to synchronize the state of
+matching objects locally present in the :class:`_orm.Session`. See the section
+:ref:`orm_expression_update_delete` for background on this feature.
+
+
+
+
diff --git a/doc/build/orm/session_basics.rst b/doc/build/orm/session_basics.rst
index 79af8f27f..8cec8a18e 100644
--- a/doc/build/orm/session_basics.rst
+++ b/doc/build/orm/session_basics.rst
@@ -492,6 +492,7 @@ ways to refresh its contents with new data from the current transaction:
session.expire(u1)
u1.some_attribute # <-- lazy loads from the transaction
+
..
* **the refresh() method** - closely related is the :meth:`_orm.Session.refresh`
@@ -539,7 +540,7 @@ time refresh locally present objects which match those rows.
To emit an ORM-enabled UPDATE in :term:`1.x style`, the :meth:`_query.Query.update` method
may be used::
- session.query(User).filter(User.nane == "squidward").\
+ session.query(User).filter(User.name == "squidward").\
update({"name": "spongebob"}, synchronize_session="fetch")
Above, an UPDATE will be emitted against all rows that match the name
@@ -551,10 +552,7 @@ objects locally present in memory will be updated in memory based on these
primary key identities.
For ORM-enabled UPDATEs in :term:`2.0 style`, :meth:`_orm.Session.execute` is used with the
-Core :class:`_sql.Update` construct. The :meth:`_orm.Session` must
-be configured with :paramref:`_orm.Session.future` set to ``True``::
-
- session = Session(future=True)
+Core :class:`_sql.Update` construct::
from sqlalchemy import update
@@ -575,10 +573,8 @@ ORM-enabled delete, :term:`1.x style`::
session.query(User).filter(User.nane == "squidward").\
delete(synchronize_session="fetch")
-ORM-enabled delete, :term:`2.0 style`. The :meth:`_orm.Session` must
-be configured with :paramref:`_orm.Session.future` set to ``True``::
+ORM-enabled delete, :term:`2.0 style`::
- session = Session(future=True)
from sqlalchemy import delete
stmt = delete(User).where(User.nane == "squidward").execution_options(synchronize_session="fetch")
diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst
index 2025268c9..ae62d256a 100644
--- a/doc/build/orm/tutorial.rst
+++ b/doc/build/orm/tutorial.rst
@@ -4,6 +4,35 @@
Object Relational Tutorial (1.x API)
====================================
+.. admonition:: About this document
+
+ This tutorial covers the well known SQLAlchemy ORM API
+ that has been in use for many years. As of SQLAlchemy 1.4, there are two
+ distinct styles of ORM use known as :term:`1.x style` and :term:`2.0
+ style`, the latter of which makes a wide range of changes most prominently
+ around how ORM queries are constructed and executed.
+
+ The plan is that in SQLAlchemy 2.0, the 1.x style of ORM use will be
+ considered legacy and no longer featured in documentation and many
+ aspects of it will be removed. However, the most central element of
+ :term:`1.x style` ORM use, the :class:`_orm.Query` object, will still
+ remain available for long-term legacy use cases.
+
+ This tutorial is applicable to users who want to learn how SQLAlchemy has
+ been used for many years, particularly those users working with existing
+ applications or related learning material that is in 1.x style.
+
+ For an introduction to SQLAlchemy from the new 1.4/2.0 perspective,
+ see :ref:`unified_tutorial`.
+
+ .. seealso::
+
+ :ref:`change_5159`
+
+ :ref:`migration_20_toplevel`
+
+ :ref:`unified_tutorial`
+
The SQLAlchemy Object Relational Mapper presents a method of associating
user-defined Python classes with database tables, and instances of those
classes (objects) with rows in their corresponding tables. It includes a
@@ -168,7 +197,7 @@ this information for a specific table is called the :class:`_schema.Table` objec
one for us. We can see this object by inspecting the ``__table__`` attribute::
>>> User.__table__ # doctest: +NORMALIZE_WHITESPACE
- Table('users', MetaData(bind=None),
+ Table('users', MetaData(),
Column('id', Integer(), table=<users>, primary_key=True, nullable=False),
Column('name', String(), table=<users>),
Column('fullname', String(), table=<users>),
@@ -217,8 +246,6 @@ the actual ``CREATE TABLE`` statement:
nickname VARCHAR,
PRIMARY KEY (id)
)
- <BLANKLINE>
- <BLANKLINE>
[...] ()
COMMIT
@@ -1223,8 +1250,6 @@ already been created:
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES users (id)
)
- <BLANKLINE>
- <BLANKLINE>
[...] ()
COMMIT
@@ -2089,10 +2114,7 @@ Create new tables:
PRIMARY KEY (id),
UNIQUE (keyword)
)
- <BLANKLINE>
- <BLANKLINE>
[...] ()
- <BLANKLINE>
CREATE TABLE posts (
id INTEGER NOT NULL,
user_id INTEGER,
@@ -2101,10 +2123,7 @@ Create new tables:
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES users (id)
)
- <BLANKLINE>
- <BLANKLINE>
[...] ()
- <BLANKLINE>
CREATE TABLE post_keywords (
post_id INTEGER NOT NULL,
keyword_id INTEGER NOT NULL,
@@ -2112,8 +2131,6 @@ Create new tables:
FOREIGN KEY(post_id) REFERENCES posts (id),
FOREIGN KEY(keyword_id) REFERENCES keywords (id)
)
- <BLANKLINE>
- <BLANKLINE>
[...] ()
COMMIT