From f9f1e8b6c5890eb17b6ba055ff563087cac20d0e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 13 Jun 2022 11:46:28 -0400 Subject: [PATCH] rework ORM mapping docs prepare docs for newly incoming mapper styles, including new dataclass mapping. move the existing dataclass/attrs docs all into their own section and try to improve organization and wording into the relatively recent "mapping styles" document. Change-Id: I0b5e2a5b6a70db65ab19b5bb0a2bb7df20e0b498 --- doc/build/changelog/migration_13.rst | 2 +- doc/build/changelog/migration_14.rst | 2 +- doc/build/core/inspection.rst | 11 + doc/build/dialects/oracle.rst | 3 + doc/build/glossary.rst | 14 + doc/build/index.rst | 2 +- doc/build/orm/classical.rst | 2 +- doc/build/orm/dataclasses.rst | 517 +++++++++++++++++++ doc/build/orm/declarative_config.rst | 97 +++- doc/build/orm/declarative_mixins.rst | 14 +- doc/build/orm/declarative_styles.rst | 301 ----------- doc/build/orm/declarative_tables.rst | 107 ++-- doc/build/orm/inheritance.rst | 2 +- doc/build/orm/mapper_config.rst | 20 +- doc/build/orm/mapping_columns.rst | 127 ++++- doc/build/orm/mapping_styles.rst | 309 +++++------ doc/build/orm/session_state_management.rst | 25 +- lib/sqlalchemy/ext/automap.py | 14 +- lib/sqlalchemy/ext/declarative/extensions.py | 5 + lib/sqlalchemy/orm/mapper.py | 36 +- lib/sqlalchemy/orm/state.py | 4 +- 21 files changed, 1065 insertions(+), 549 deletions(-) create mode 100644 doc/build/orm/dataclasses.rst diff --git a/doc/build/changelog/migration_13.rst b/doc/build/changelog/migration_13.rst index cdef36e12f..169f67df5b 100644 --- a/doc/build/changelog/migration_13.rst +++ b/doc/build/changelog/migration_13.rst @@ -85,7 +85,7 @@ Relationship to AliasedClass replaces the need for non primary mappers ----------------------------------------------------------------------- The "non primary mapper" is a :class:`_orm.Mapper` created in the -:ref:`classical_mapping` style, which acts as an additional mapper against an +:ref:`orm_imperative_mapping` style, which acts as an additional mapper against an already mapped class against a different kind of selectable. The non primary mapper has its roots in the 0.1, 0.2 series of SQLAlchemy where it was anticipated that the :class:`_orm.Mapper` object was to be the primary query diff --git a/doc/build/changelog/migration_14.rst b/doc/build/changelog/migration_14.rst index 7001acb055..7f8b8f6ba5 100644 --- a/doc/build/changelog/migration_14.rst +++ b/doc/build/changelog/migration_14.rst @@ -288,7 +288,7 @@ the :class:`_orm.registry` object, and fall into these categories: * Declarative Table * Imperative Table (Hybrid) * :ref:`orm_declarative_dataclasses` -* :ref:`Imperative (a.k.a. "classical" mapping) ` +* :ref:`Imperative (a.k.a. "classical" mapping) ` * Using :meth:`_orm.registry.map_imperatively` * :ref:`orm_imperative_dataclasses` diff --git a/doc/build/core/inspection.rst b/doc/build/core/inspection.rst index eab1288422..7816cd3fd8 100644 --- a/doc/build/core/inspection.rst +++ b/doc/build/core/inspection.rst @@ -25,8 +25,18 @@ Below is a listing of many of the most common inspection targets. to per attribute state via the :class:`.AttributeState` interface as well as the per-flush "history" of any attribute via the :class:`.History` object. + + .. seealso:: + + :ref:`orm_mapper_inspection_instancestate` + * ``type`` (i.e. a class) - a class given will be checked by the ORM for a mapping - if so, a :class:`_orm.Mapper` for that class is returned. + + .. seealso:: + + :ref:`orm_mapper_inspection_mapper` + * mapped attribute - passing a mapped attribute to :func:`_sa.inspect`, such as ``inspect(MyClass.some_attribute)``, returns a :class:`.QueryableAttribute` object, which is the :term:`descriptor` associated with a mapped class. @@ -36,3 +46,4 @@ Below is a listing of many of the most common inspection targets. attribute. * :class:`.AliasedClass` - returns an :class:`.AliasedInsp` object. + diff --git a/doc/build/dialects/oracle.rst b/doc/build/dialects/oracle.rst index a2595aad73..62e72eb829 100644 --- a/doc/build/dialects/oracle.rst +++ b/doc/build/dialects/oracle.rst @@ -50,12 +50,15 @@ construction arguments, are as follows: .. autoclass:: RAW :members: __init__ +.. _cx_oracle: cx_Oracle --------- .. automodule:: sqlalchemy.dialects.oracle.cx_oracle +.. _oracledb: + python-oracledb --------------- diff --git a/doc/build/glossary.rst b/doc/build/glossary.rst index c73dcabce0..35ce311b95 100644 --- a/doc/build/glossary.rst +++ b/doc/build/glossary.rst @@ -25,6 +25,20 @@ Glossary :ref:`migration_20_toplevel` + reflection + reflected + In SQLAlchemy, this term refers to the feature of querying a database's + schema catalogs in order to load information about existing tables, + columns, constraints, and other constructs. SQLAlchemy includes + features that can both provide raw data for this information, as well + as that it can construct Core/ORM usable :class:`.Table` objects + from database schema catalogs automatically. + + .. seealso:: + + :ref:`metadata_reflection_toplevel` - complete background on + database reflection. + imperative declarative diff --git a/doc/build/index.rst b/doc/build/index.rst index cfab3c5433..9274935a5e 100644 --- a/doc/build/index.rst +++ b/doc/build/index.rst @@ -70,7 +70,7 @@ SQLAlchemy Documentation **SQLAlchemy ORM** * **ORM Configuration:** - :doc:`Mapper Configuration ` | + :doc:`Mapped Class Configuration ` | :doc:`Relationship Configuration ` * **ORM Usage:** diff --git a/doc/build/orm/classical.rst b/doc/build/orm/classical.rst index 3fd149f928..a0bc70d890 100644 --- a/doc/build/orm/classical.rst +++ b/doc/build/orm/classical.rst @@ -1,5 +1,5 @@ :orphan: -Moved! :ref:`classical_mapping` +Moved! :ref:`orm_imperative_mapping` diff --git a/doc/build/orm/dataclasses.rst b/doc/build/orm/dataclasses.rst new file mode 100644 index 0000000000..5331efd2df --- /dev/null +++ b/doc/build/orm/dataclasses.rst @@ -0,0 +1,517 @@ +.. _orm_dataclasses_toplevel: + +====================================== +Integration with dataclasses and attrs +====================================== + +SQLAlchemy 1.4 has limited support for ORM mappings that are established +against classes that have already been pre-instrumented using either Python's +built-in dataclasses_ library or the attrs_ third party integration library. + +.. tip:: SQLAlchemy 2.0 will include a new dataclass integration feature which + allows for a particular class to be mapped and converted into a Python + dataclass simultaneously, with full support for SQLAlchemy's declarative + syntax. Within the scope of the 1.4 release, the ``@dataclass`` decorator + is used separately as documented in this section. + +.. _orm_declarative_dataclasses: + +Applying ORM Mappings to an existing dataclass +---------------------------------------------- + +The dataclasses_ module, added in Python 3.7, provides a ``@dataclass`` class +decorator to automatically generate boilerplate definitions of common object +methods including ``__init__()``, ``__repr()__``, and other methods. SQLAlchemy +supports the application of ORM mappings to a class after it has been processed +with the ``@dataclass`` decorator, by using either the +:meth:`_orm.registry.mapped` class decorator, or the +:meth:`_orm.registry.map_imperatively` method to apply ORM mappings to the +class using Imperative. + +.. versionadded:: 1.4 Added support for direct mapping of Python dataclasses + +To map an existing dataclass, SQLAlchemy's "inline" declarative directives +cannot be used directly; ORM directives are assigned using one of three +techniques: + +* Using "Declarative with Imperative Table", the table / column to be mapped + is defined using a :class:`_schema.Table` object assigned to the + ``__table__`` attribute of the class; relationships are defined within + ``__mapper_args__`` dictionary. The class is mapped using the + :meth:`_orm.registry.mapped` decorator. An example is below at + :ref:`orm_declarative_dataclasses_imperative_table`. + +* Using full "Declarative", the Declarative-interpreted directives such as + :class:`_schema.Column`, :func:`_orm.relationship` are added to the + ``.metadata`` dictionary of the ``dataclasses.field()`` construct, where + they are consumed by the declarative process. The class is again + mapped using the :meth:`_orm.registry.mapped` decorator. See the example + below at :ref:`orm_declarative_dataclasses_declarative_table`. + +* An "Imperative" mapping can be applied to an existing dataclass using + the :meth:`_orm.registry.map_imperatively` method to produce the mapping + in exactly the same way as described at :ref:`orm_imperative_mapping`. + This is illustrated below at :ref:`orm_imperative_dataclasses`. + +The general process by which SQLAlchemy applies mappings to a dataclass +is the same as that of an ordinary class, but also includes that +SQLAlchemy will detect class-level attributes that were part of the +dataclasses declaration process and replace them at runtime with +the usual SQLAlchemy ORM mapped attributes. The ``__init__`` method that +would have been generated by dataclasses is left intact, as is the same +for all the other methods that dataclasses generates such as +``__eq__()``, ``__repr__()``, etc. + +.. _orm_declarative_dataclasses_imperative_table: + +Mapping dataclasses using Declarative With Imperative Table +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An example of a mapping using ``@dataclass`` using +:ref:`orm_imperative_table_configuration` is below. A complete +:class:`_schema.Table` object is constructed explicitly and assigned to the +``__table__`` attribute. Instance fields are defined using normal dataclass +syntaxes. Additional :class:`.MapperProperty` +definitions such as :func:`.relationship`, are placed in the +:ref:`__mapper_args__ ` class-level +dictionary underneath the ``properties`` key, corresponding to the +:paramref:`_orm.Mapper.properties` parameter:: + + from __future__ import annotations + + from dataclasses import dataclass, field + from typing import List, Optional + + from sqlalchemy import Column, ForeignKey, Integer, String, Table + from sqlalchemy.orm import registry, relationship + + mapper_registry = registry() + + + @mapper_registry.mapped + @dataclass + class User: + __table__ = Table( + "user", + mapper_registry.metadata, + Column("id", Integer, primary_key=True), + Column("name", String(50)), + Column("fullname", String(50)), + Column("nickname", String(12)), + ) + id: int = field(init=False) + name: Optional[str] = None + fullname: Optional[str] = None + nickname: Optional[str] = None + addresses: List[Address] = field(default_factory=list) + + __mapper_args__ = { # type: ignore + "properties": { + "addresses": relationship("Address"), + } + } + + + @mapper_registry.mapped + @dataclass + class Address: + __table__ = Table( + "address", + mapper_registry.metadata, + Column("id", Integer, primary_key=True), + Column("user_id", Integer, ForeignKey("user.id")), + Column("email_address", String(50)), + ) + id: int = field(init=False) + user_id: int = field(init=False) + email_address: Optional[str] = None + +In the above example, the ``User.id``, ``Address.id``, and ``Address.user_id`` +attributes are defined as ``field(init=False)``. This means that parameters for +these won't be added to ``__init__()`` methods, but +:class:`.Session` will still be able to set them after getting their values +during flush from autoincrement or other default value generator. To +allow them to be specified in the constructor explicitly, they would instead +be given a default value of ``None``. + +For a :func:`_orm.relationship` to be declared separately, it needs to be +specified directly within the :paramref:`_orm.Mapper.properties` dictionary +which itself is specified within the ``__mapper_args__`` dictionary, so that it +is passed to the constructor for :class:`_orm.Mapper`. An alternative to this +approach is in the next example. + +.. _orm_declarative_dataclasses_declarative_table: + +Mapping dataclasses using Declarative Mapping +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The fully declarative approach requires that :class:`_schema.Column` objects +are declared as class attributes, which when using dataclasses would conflict +with the dataclass-level attributes. An approach to combine these together +is to make use of the ``metadata`` attribute on the ``dataclass.field`` +object, where SQLAlchemy-specific mapping information may be supplied. +Declarative supports extraction of these parameters when the class +specifies the attribute ``__sa_dataclass_metadata_key__``. This also +provides a more succinct method of indicating the :func:`_orm.relationship` +association:: + + + from __future__ import annotations + + from dataclasses import dataclass, field + from typing import List + + from sqlalchemy import Column, ForeignKey, Integer, String + from sqlalchemy.orm import registry, relationship + + mapper_registry = registry() + + + @mapper_registry.mapped + @dataclass + class User: + __tablename__ = "user" + + __sa_dataclass_metadata_key__ = "sa" + id: int = field( + init=False, metadata={"sa": Column(Integer, primary_key=True)} + ) + name: str = field(default=None, metadata={"sa": Column(String(50))}) + fullname: str = field(default=None, metadata={"sa": Column(String(50))}) + nickname: str = field(default=None, metadata={"sa": Column(String(12))}) + addresses: List[Address] = field( + default_factory=list, metadata={"sa": relationship("Address")} + ) + + + @mapper_registry.mapped + @dataclass + class Address: + __tablename__ = "address" + __sa_dataclass_metadata_key__ = "sa" + id: int = field( + init=False, metadata={"sa": Column(Integer, primary_key=True)} + ) + user_id: int = field( + init=False, metadata={"sa": Column(ForeignKey("user.id"))} + ) + email_address: str = field( + default=None, metadata={"sa": Column(String(50))} + ) + +.. _orm_imperative_dataclasses: + +Mapping dataclasses using Imperative Mapping +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As described previously, a class which is set up as a dataclass using the +``@dataclass`` decorator can then be further decorated using the +:meth:`_orm.registry.mapped` decorator in order to apply declarative-style +mapping to the class. As an alternative to using the +:meth:`_orm.registry.mapped` decorator, we may also pass the class through the +:meth:`_orm.registry.map_imperatively` method instead, so that we may pass all +:class:`_schema.Table` and :class:`_orm.Mapper` configuration imperatively to +the function rather than having them defined on the class itself as class +variables:: + + from __future__ import annotations + + from dataclasses import dataclass + from dataclasses import field + from typing import List + + from sqlalchemy import Column + from sqlalchemy import ForeignKey + from sqlalchemy import Integer + from sqlalchemy import MetaData + from sqlalchemy import String + from sqlalchemy import Table + from sqlalchemy.orm import registry + from sqlalchemy.orm import relationship + + mapper_registry = registry() + + @dataclass + class User: + id: int = field(init=False) + name: str = None + fullname: str = None + nickname: str = None + addresses: List[Address] = field(default_factory=list) + + @dataclass + class Address: + id: int = field(init=False) + user_id: int = field(init=False) + email_address: str = None + + metadata_obj = MetaData() + + user = Table( + 'user', + metadata_obj, + Column('id', Integer, primary_key=True), + Column('name', String(50)), + Column('fullname', String(50)), + Column('nickname', String(12)), + ) + + address = Table( + 'address', + metadata_obj, + Column('id', Integer, primary_key=True), + Column('user_id', Integer, ForeignKey('user.id')), + Column('email_address', String(50)), + ) + + mapper_registry.map_imperatively(User, user, properties={ + 'addresses': relationship(Address, backref='user', order_by=address.c.id), + }) + + mapper_registry.map_imperatively(Address, address) + +.. _orm_declarative_dataclasses_mixin: + +Using Declarative Mixins with Dataclasses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the section :ref:`orm_mixins_toplevel`, Declarative Mixin classes +are introduced. One requirement of declarative mixins is that certain +constructs that can't be easily duplicated must be given as callables, +using the :class:`_orm.declared_attr` decorator, such as in the +example at :ref:`orm_declarative_mixins_relationships`:: + + class RefTargetMixin: + @declared_attr + def target_id(cls): + return Column("target_id", ForeignKey("target.id")) + + @declared_attr + def target(cls): + return relationship("Target") + +This form is supported within the Dataclasses ``field()`` object by using +a lambda to indicate the SQLAlchemy construct inside the ``field()``. +Using :func:`_orm.declared_attr` to surround the lambda is optional. +If we wanted to produce our ``User`` class above where the ORM fields +came from a mixin that is itself a dataclass, the form would be:: + + @dataclass + class UserMixin: + __tablename__ = "user" + + __sa_dataclass_metadata_key__ = "sa" + + id: int = field( + init=False, metadata={"sa": Column(Integer, primary_key=True)} + ) + + addresses: List[Address] = field( + default_factory=list, metadata={"sa": lambda: relationship("Address")} + ) + + + @dataclass + class AddressMixin: + __tablename__ = "address" + __sa_dataclass_metadata_key__ = "sa" + id: int = field( + init=False, metadata={"sa": Column(Integer, primary_key=True)} + ) + user_id: int = field( + init=False, metadata={"sa": lambda: Column(ForeignKey("user.id"))} + ) + email_address: str = field( + default=None, metadata={"sa": Column(String(50))} + ) + + + @mapper_registry.mapped + class User(UserMixin): + pass + + + @mapper_registry.mapped + class Address(AddressMixin): + pass + +.. versionadded:: 1.4.2 Added support for "declared attr" style mixin attributes, + namely :func:`_orm.relationship` constructs as well as :class:`_schema.Column` + objects with foreign key declarations, to be used within "Dataclasses + with Declarative Table" style mappings. + + + +.. _orm_declarative_attrs_imperative_table: + +Applying ORM mappings to an existing attrs class +------------------------------------------------- + +The attrs_ library is a popular third party library that provides similar +features as dataclasses, with many additional features provided not +found in ordinary dataclasses. + +A class augmented with attrs_ uses the ``@define`` decorator. This decorator +initiates a process to scan the class for attributes that define the class' +behavior, which are then used to generate methods, documentation, and +annotations. + +The SQLAlchemy ORM supports mapping an attrs_ class using **Declarative with +Imperative Table** or **Imperative** mapping. The general form of these two +styles is fully equivalent to the +:ref:`orm_declarative_dataclasses_declarative_table` and +:ref:`orm_declarative_attrs_imperative_table` mapping forms used with +dataclasses, where the inline attribute directives used by dataclasses or attrs +are unchanged, and SQLAlchemy's table-oriented instrumentation is applied at +runtime. + +The ``@define`` decorator of attrs_ by default replaces the annotated class +with a new __slots__ based class, which is not supported. When using the old +style annotation ``@attr.s`` or using ``define(slots=False)``, the class +does not get replaced. Furthermore attrs removes its own class-bound attributes +after the decorator runs, so that SQLAlchemy's mapping process takes over these +attributes without any issue. Both decorators, ``@attr.s`` and ``@define(slots=False)`` +work with SQLAlchemy. + +Mapping attrs with Declarative "Imperative Table" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the "Declarative with Imperative Table" style, a :class:`_schema.Table` +object is declared inline with the declarative class. The +``@define`` decorator is applied to the class first, then the +:meth:`_orm.registry.mapped` decorator second:: + + + from __future__ import annotations + + from typing import List + + from attrs import define + from sqlalchemy import Column + from sqlalchemy import ForeignKey + from sqlalchemy import Integer + from sqlalchemy import MetaData + from sqlalchemy import String + from sqlalchemy import Table + from sqlalchemy.orm import registry + from sqlalchemy.orm import relationship + + mapper_registry = registry() + + + @mapper_registry.mapped + @define(slots=False) + class User: + __table__ = Table( + "user", + mapper_registry.metadata, + Column("id", Integer, primary_key=True), + Column("name", String(50)), + Column("fullname", String(50)), + Column("nickname", String(12)), + ) + id: int + name: str + fullname: str + nickname: str + addresses: List[Address] + + __mapper_args__ = { # type: ignore + "properties": { + "addresses": relationship("Address"), + } + } + + @mapper_registry.mapped + @define(slots=False) + class Address: + __table__ = Table( + "address", + mapper_registry.metadata, + Column("id", Integer, primary_key=True), + Column("user_id", Integer, ForeignKey("user.id")), + Column("email_address", String(50)), + ) + id: int + user_id: int + email_address: Optional[str] + + +.. note:: The ``attrs`` ``slots=True`` option, which enables ``__slots__`` on + a mapped class, cannot be used with SQLAlchemy mappings without fully + implementing alternative + :ref:`attribute instrumentation `, as mapped + classes normally rely upon direct access to ``__dict__`` for state storage. + Behavior is undefined when this option is present. + + + +Mapping attrs with Imperative Mapping +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Just as is the case with dataclasses, we can make use of +:meth:`_orm.registry.map_imperatively` to map an existing ``attrs`` class +as well:: + + from __future__ import annotations + + from typing import List + + from attrs import define + from sqlalchemy import Column + from sqlalchemy import ForeignKey + from sqlalchemy import Integer + from sqlalchemy import MetaData + from sqlalchemy import String + from sqlalchemy import Table + from sqlalchemy.orm import registry + from sqlalchemy.orm import relationship + + mapper_registry = registry() + + @define(slots=False) + class User: + id: int + name: str + fullname: str + nickname: str + addresses: List[Address] + + @define(slots=False) + class Address: + id: int + user_id: int + email_address: Optional[str] + + metadata_obj = MetaData() + + user = Table( + 'user', + metadata_obj, + Column('id', Integer, primary_key=True), + Column('name', String(50)), + Column('fullname', String(50)), + Column('nickname', String(12)), + ) + + address = Table( + 'address', + metadata_obj, + Column('id', Integer, primary_key=True), + Column('user_id', Integer, ForeignKey('user.id')), + Column('email_address', String(50)), + ) + + mapper_registry.map_imperatively(User, user, properties={ + 'addresses': relationship(Address, backref='user', order_by=address.c.id), + }) + + mapper_registry.map_imperatively(Address, address) + +The above form is equivalent to the previous example using +Declarative with Imperative Table. + + + +.. _dataclasses: https://docs.python.org/3/library/dataclasses.html +.. _attrs: https://pypi.org/project/attrs/ diff --git a/doc/build/orm/declarative_config.rst b/doc/build/orm/declarative_config.rst index 38defaa9e4..3a4aeb6092 100644 --- a/doc/build/orm/declarative_config.rst +++ b/doc/build/orm/declarative_config.rst @@ -174,10 +174,35 @@ using the ``__mapper_args__`` declarative class variable, which is a dictionary that is passed as keyword arguments to the :class:`_orm.Mapper` function. Some examples: +**Map Specific Primary Key Columns** + +The example below illustrates Declarative-level settings for the +:paramref:`_orm.Mapper.primary_key` parameter, which establishes +particular columns as part of what the ORM should consider to be a primary +key for the class, independently of schema-level primary key constraints:: + + class GroupUsers(Base): + __tablename__ = 'group_users' + + user_id = Column(String(40)) + group_id = Column(String(40)) + + __mapper_args__ = { + "primary_key": [user_id, group_id] + } + +.. seealso:: + + :ref:`mapper_primary_key` - further background on ORM mapping of explicit + columns as primary key columns + **Version ID Column** -The :paramref:`_orm.Mapper.version_id_col` and -:paramref:`_orm.Mapper.version_id_generator` parameters:: +The example below illustrates Declarative-level settings for the +:paramref:`_orm.Mapper.version_id_col` and +:paramref:`_orm.Mapper.version_id_generator` parameters, which configure +an ORM-maintained version counter that is updated and checked within the +:term:`unit of work` flush process:: from datetime import datetime @@ -193,10 +218,16 @@ The :paramref:`_orm.Mapper.version_id_col` and "version_id_generator": lambda v: datetime.now(), } +.. seealso:: + + :ref:`mapper_version_counter` - background on the ORM version counter feature + **Single Table Inheritance** -The :paramref:`_orm.Mapper.polymorphic_on` and -:paramref:`_orm.Mapper.polymorphic_identity` parameters:: +The example below illustrates Declarative-level settings for the +:paramref:`_orm.Mapper.polymorphic_on` and +:paramref:`_orm.Mapper.polymorphic_identity` parameters, which are used when +configuring a single-table inheritance mapping:: class Person(Base): __tablename__ = "person" @@ -215,15 +246,69 @@ The :paramref:`_orm.Mapper.polymorphic_on` and polymorphic_identity="employee", ) + +.. seealso:: + + :ref:`single_inheritance` - background on the ORM single table inheritance + mapping feature. + +Constructing mapper arguments dynamically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + The ``__mapper_args__`` dictionary may be generated from a class-bound descriptor method rather than from a fixed dictionary by making use of the -:func:`_orm.declared_attr` construct. The section :ref:`orm_mixins_toplevel` -discusses this concept further. +:func:`_orm.declared_attr` construct. This is useful to create arguments +for mappers that are programmatically derived from the table configuration +or other aspects of the mapped class. A dynamic ``__mapper_args__`` +attribute will typically be useful when using a Declarative Mixin or +abstract base class. + +For example, to omit from the mapping +any columns that have a special :attr:`.Column.info` value, a mixin +can use a ``__mapper_args__`` method that scans for these columns from the +``cls.__table__`` attribute and passes them to the :paramref:`_orm.Mapper.exclude_properties` +collection:: + + from sqlalchemy import Column + from sqlalchemy import Integer + from sqlalchemy import select + from sqlalchemy import String + from sqlalchemy.orm import declarative_base + from sqlalchemy.orm import declared_attr + + + class ExcludeColsWFlag: + @declared_attr + def __mapper_args__(cls): + return { + "exclude_properties": [ + column.key for column in cls.__table__.c if + column.info.get("exclude", False) + ] + } + + Base = declarative_base() + + class SomeClass(ExcludeColsWFlag, Base): + __tablename__ = 'some_table' + + id = Column(Integer, primary_key=True) + data = Column(String) + not_needed = Column(String, info={"exclude": True}) + + +Above, the ``ExcludeColsWFlag`` mixin provides a per-class ``__mapper_args__`` +hook that will scan for :class:`.Column` objects that include the key/value +``'exclude': True`` passed to the :paramref:`.Column.info` parameter, and then +add their string "key" name to the :paramref:`_orm.Mapper.exclude_properties` +collection which will prevent the resulting :class:`.Mapper` from considering +these columns for any SQL operations. .. seealso:: :ref:`orm_mixins_toplevel` + Other Declarative Mapping Directives -------------------------------------- diff --git a/doc/build/orm/declarative_mixins.rst b/doc/build/orm/declarative_mixins.rst index 2e07646e43..0e536e7b5a 100644 --- a/doc/build/orm/declarative_mixins.rst +++ b/doc/build/orm/declarative_mixins.rst @@ -14,6 +14,14 @@ usage of mixin classes, as well as via augmenting the declarative base produced by either the :meth:`_orm.registry.generate_base` method or :func:`_orm.declarative_base` functions. +When using mixins or abstract base classes with Declarative, a decorator +known as :func:`_orm.declared_attr` is frequently used. This decorator +allows the creation of class methods that produce a parameter or ORM construct that will be +part of a declarative mapping. Generating constructs using a callable +allows for Declarative to get a new copy of a particular kind of object +each time it calls upon the mixin or abstract base on behalf of a new +class that's being mapped. + An example of some commonly mixed-in idioms is below:: from sqlalchemy.orm import declarative_mixin, declared_attr @@ -37,7 +45,11 @@ An example of some commonly mixed-in idioms is below:: Where above, the class ``MyModel`` will contain an "id" column as the primary key, a ``__tablename__`` attribute that derives from the name of the class itself, as well as ``__table_args__`` -and ``__mapper_args__`` defined by the ``MyMixin`` mixin class. +and ``__mapper_args__`` defined by the ``MyMixin`` mixin class. The +:func:`_orm.declared_attr` decorator applied to a class method called +``def __tablename__(cls):`` has the effect of turning the method into a class +method while also indicating to Declarative that this attribute is significant +within the mapping. .. tip:: diff --git a/doc/build/orm/declarative_styles.rst b/doc/build/orm/declarative_styles.rst index e9af7b4d32..8a67f8ecbf 100644 --- a/doc/build/orm/declarative_styles.rst +++ b/doc/build/orm/declarative_styles.rst @@ -187,304 +187,3 @@ The decorator form of mapping is particularly useful when combining a SQLAlchemy declarative mapping with other forms of class declaration, notably the Python ``dataclasses`` module. See the next section. -.. _orm_declarative_dataclasses: - -Declarative Mapping with Dataclasses and Attrs ----------------------------------------------- - -The dataclasses_ module, added in Python 3.7, provides a ``@dataclass`` class -decorator to automatically generate boilerplate definitions of ``__init__()``, -``__eq__()``, ``__repr()__``, etc. methods. Another very popular library that does -the same, and much more, is attrs_, which uses the ``@define`` decorator. -Both libraries make use of class decorators in order to scan a class for -attributes that define the class' behavior, which are then used to generate -methods, documentation, and annotations. - -The :meth:`_orm.registry.mapped` class decorator allows the declarative mapping -of a class to occur after the class has been fully constructed, allowing the -class to be processed by other class decorators first. The ``@dataclass`` -and ``@define`` decorators may therefore be applied first before the -ORM mapping process proceeds via the :meth:`_orm.registry.mapped` decorator -or via the :meth:`_orm.registry.map_imperatively` method discussed in a -later section. - -Mapping with ``@dataclass`` or ``@define`` may be used in a straightforward -way with :ref:`orm_imperative_table_configuration` style, where the -the :class:`_schema.Table`, which means that it is defined separately and -associated with the class via the ``__table__``. For dataclasses specifically, -:ref:`orm_declarative_table` is also supported. - -.. versionadded:: 1.4.0b2 Added support for full declarative mapping when using - dataclasses. - -When attributes are defined using ``dataclasses``, the ``@dataclass`` -decorator consumes them but leaves them in place on the class. -SQLAlchemy's mapping process, when it encounters an attribute that normally -is to be mapped to a :class:`_schema.Column`, checks explicitly if the -attribute is part of a Dataclasses setup, and if so will **replace** -the class-bound dataclass attribute with its usual mapped -properties. The ``__init__`` method created by ``@dataclass`` is left -intact. The ``@define`` decorator of attrs_ by default replaces the annotated class -with a new __slots__ based class, which is not supported. When using the old -style annotation ``@attr.s`` or using ``define(slots=False)``, the class -does not get replaced. Furthermore attrs removes its own class-bound attributes -after the decorator runs, so that SQLAlchemy's mapping process takes over these -attributes without any issue. Both decorators, ``@attr.s`` and ``@define(slots=False)`` -work with SQLAlchemy. - -.. versionadded:: 1.4 Added support for direct mapping of Python dataclasses, - where the :class:`_orm.Mapper` will now detect attributes that are specific - to the ``@dataclasses`` module and replace them at mapping time, rather - than skipping them as is the default behavior for any class attribute - that's not part of the mapping. - -.. _orm_declarative_dataclasses_imperative_table: - -Example One - Dataclasses with Imperative Table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An example of a mapping using ``@dataclass`` using -:ref:`orm_imperative_table_configuration` is as follows:: - - from __future__ import annotations - - from dataclasses import dataclass, field - from typing import List, Optional - - from sqlalchemy import Column, ForeignKey, Integer, String, Table - from sqlalchemy.orm import registry, relationship - - mapper_registry = registry() - - - @mapper_registry.mapped - @dataclass - class User: - __table__ = Table( - "user", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("name", String(50)), - Column("fullname", String(50)), - Column("nickname", String(12)), - ) - id: int = field(init=False) - name: Optional[str] = None - fullname: Optional[str] = None - nickname: Optional[str] = None - addresses: List[Address] = field(default_factory=list) - - __mapper_args__ = { # type: ignore - "properties": { - "addresses": relationship("Address"), - } - } - - - @mapper_registry.mapped - @dataclass - class Address: - __table__ = Table( - "address", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("user_id", Integer, ForeignKey("user.id")), - Column("email_address", String(50)), - ) - id: int = field(init=False) - user_id: int = field(init=False) - email_address: Optional[str] = None - -In the above example, the ``User.id``, ``Address.id``, and ``Address.user_id`` -attributes are defined as ``field(init=False)``. This means that parameters for -these won't be added to ``__init__()`` methods, but -:class:`.Session` will still be able to set them after getting their values -during flush from autoincrement or other default value generator. To -allow them to be specified in the constructor explicitly, they would instead -be given a default value of ``None``. - -For a :func:`_orm.relationship` to be declared separately, it needs to be -specified directly within the :paramref:`_orm.Mapper.properties` dictionary -which itself is specified within the ``__mapper_args__`` dictionary, so that it -is passed to the constructor for :class:`_orm.Mapper`. An alternative to this -approach is in the next example. - -.. _orm_declarative_dataclasses_declarative_table: - -Example Two - Dataclasses with Declarative Table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The fully declarative approach requires that :class:`_schema.Column` objects -are declared as class attributes, which when using dataclasses would conflict -with the dataclass-level attributes. An approach to combine these together -is to make use of the ``metadata`` attribute on the ``dataclass.field`` -object, where SQLAlchemy-specific mapping information may be supplied. -Declarative supports extraction of these parameters when the class -specifies the attribute ``__sa_dataclass_metadata_key__``. This also -provides a more succinct method of indicating the :func:`_orm.relationship` -association:: - - - from __future__ import annotations - - from dataclasses import dataclass, field - from typing import List - - from sqlalchemy import Column, ForeignKey, Integer, String - from sqlalchemy.orm import registry, relationship - - mapper_registry = registry() - - - @mapper_registry.mapped - @dataclass - class User: - __tablename__ = "user" - - __sa_dataclass_metadata_key__ = "sa" - id: int = field( - init=False, metadata={"sa": Column(Integer, primary_key=True)} - ) - name: str = field(default=None, metadata={"sa": Column(String(50))}) - fullname: str = field(default=None, metadata={"sa": Column(String(50))}) - nickname: str = field(default=None, metadata={"sa": Column(String(12))}) - addresses: List[Address] = field( - default_factory=list, metadata={"sa": relationship("Address")} - ) - - - @mapper_registry.mapped - @dataclass - class Address: - __tablename__ = "address" - __sa_dataclass_metadata_key__ = "sa" - id: int = field( - init=False, metadata={"sa": Column(Integer, primary_key=True)} - ) - user_id: int = field( - init=False, metadata={"sa": Column(ForeignKey("user.id"))} - ) - email_address: str = field( - default=None, metadata={"sa": Column(String(50))} - ) - -.. _orm_declarative_dataclasses_mixin: - -Using Declarative Mixins with Dataclasses -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In the section :ref:`orm_mixins_toplevel`, Declarative Mixin classes -are introduced. One requirement of declarative mixins is that certain -constructs that can't be easily duplicated must be given as callables, -using the :class:`_orm.declared_attr` decorator, such as in the -example at :ref:`orm_declarative_mixins_relationships`:: - - class RefTargetMixin: - @declared_attr - def target_id(cls): - return Column("target_id", ForeignKey("target.id")) - - @declared_attr - def target(cls): - return relationship("Target") - -This form is supported within the Dataclasses ``field()`` object by using -a lambda to indicate the SQLAlchemy construct inside the ``field()``. -Using :func:`_orm.declared_attr` to surround the lambda is optional. -If we wanted to produce our ``User`` class above where the ORM fields -came from a mixin that is itself a dataclass, the form would be:: - - @dataclass - class UserMixin: - __tablename__ = "user" - - __sa_dataclass_metadata_key__ = "sa" - - id: int = field( - init=False, metadata={"sa": Column(Integer, primary_key=True)} - ) - - addresses: List[Address] = field( - default_factory=list, metadata={"sa": lambda: relationship("Address")} - ) - - - @dataclass - class AddressMixin: - __tablename__ = "address" - __sa_dataclass_metadata_key__ = "sa" - id: int = field( - init=False, metadata={"sa": Column(Integer, primary_key=True)} - ) - user_id: int = field( - init=False, metadata={"sa": lambda: Column(ForeignKey("user.id"))} - ) - email_address: str = field( - default=None, metadata={"sa": Column(String(50))} - ) - - - @mapper_registry.mapped - class User(UserMixin): - pass - - - @mapper_registry.mapped - class Address(AddressMixin): - pass - -.. versionadded:: 1.4.2 Added support for "declared attr" style mixin attributes, - namely :func:`_orm.relationship` constructs as well as :class:`_schema.Column` - objects with foreign key declarations, to be used within "Dataclasses - with Declarative Table" style mappings. - -.. _orm_declarative_attrs_imperative_table: - -Example Three - attrs with Imperative Table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A mapping using ``@define`` from attrs_, in conjunction with imperative table:: - - import attr - from sqlalchemy.orm import registry - - # other imports - - - mapper_registry = registry() - - - @mapper_registry.mapped - @define(slots=False) - class User: - __table__ = Table( - "user", - mapper_registry.metadata, - Column("id", Integer, primary_key=True), - Column("name", String(50)), - Column("fullname", String(50)), - Column("nickname", String(12)), - ) - id: int - name: str - fullname: str - nickname: str - addresses: List[Address] - - - # other classes... - - -``@dataclass`` and attrs_ mappings may also be used with classical mappings, i.e. -with the :meth:`_orm.registry.map_imperatively` function. See the section -:ref:`orm_imperative_dataclasses` for a similar example. - -.. note:: The ``attrs`` ``slots=True`` option, which enables ``__slots__`` on - a mapped class, cannot be used with SQLAlchemy mappings without fully - implementing alternative - :ref:`attribute instrumentation `, as mapped - classes normally rely upon direct access to ``__dict__`` for state storage. - Behavior is undefined when this option is present. - -.. _dataclasses: https://docs.python.org/3/library/dataclasses.html -.. _attrs: https://pypi.org/project/attrs/ diff --git a/doc/build/orm/declarative_tables.rst b/doc/build/orm/declarative_tables.rst index 72a48078d2..b8804a21d5 100644 --- a/doc/build/orm/declarative_tables.rst +++ b/doc/build/orm/declarative_tables.rst @@ -64,6 +64,11 @@ to produce a :class:`_schema.Table` that is equivalent to:: Column("nickname", String), ) +.. seealso:: + + :ref:`mapping_columns_toplevel` - contains additional notes on affecting + how :class:`_orm.Mapper` interprets incoming :class:`.Column` objects. + .. _orm_declarative_metadata: Accessing Table and Metadata @@ -183,36 +188,26 @@ or :func:`_orm.declarative_base`:: .. _orm_declarative_table_adding_columns: -Adding New Columns -^^^^^^^^^^^^^^^^^^^ - -The declarative table configuration allows the addition of new -:class:`_schema.Column` objects under two scenarios. The most basic -is that of simply assigning new :class:`_schema.Column` objects to the -class:: - - MyClass.some_new_column = Column("data", Unicode) - -The above operation performed against a declarative class that has been -mapped using the declarative base (note, not the decorator form of declarative) -will add the above :class:`_schema.Column` to the :class:`_schema.Table` -using the :meth:`_schema.Table.append_column` method and will also add the -column to the :class:`_orm.Mapper` to be fully mapped. +Appending additional columns to an existing Declarative mapped class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. note:: assignment of new columns to an existing declaratively mapped class - will only function correctly if the "declarative base" class is used, which - also provides for a metaclass-driven ``__setattr__()`` method which will - intercept these operations. It will **not** work if the declarative - decorator provided by - :meth:`_orm.registry.mapped` is used, nor will it work for an imperatively - mapped class mapped by :meth:`_orm.registry.map_imperatively`. +A declarative table configuration allows the addition of new +:class:`_schema.Column` objects an existing mapping after the :class:`.Table` +metadata has already been generated. +For a declarative class that is declared using a declarative base class, +the underlying metaclass :class:`.DeclarativeMeta` includes a ``__setattr__()`` +method that will intercept additional :class:`.Column` objects and +add them to both the :class:`.Table` using :meth:`.Table.append_column` +as well as to the existing :class:`.Mapper` using :meth:`.Mapper.add_property`:: -The other scenario where a :class:`_schema.Column` is added on the fly is -when an inheriting subclass that has no table of its own indicates -additional columns; these columns will be added to the superclass table. -The section :ref:`single_inheritance` discusses single table inheritance. + MyClass.some_new_column = Column("data", Unicode) +Additional :class:`_schema.Column` objects may also be added to a mapping +in the specific circumstance of using single table inheritance, where +additional columns are present on mapped subclasses that have +no :class:`.Table` of their own. This is illustrated in the section +:ref:`single_inheritance`. .. _orm_imperative_table_configuration: @@ -344,10 +339,15 @@ use a declarative hybrid mapping, passing the :paramref:`_schema.Table.autoload_with` parameter to the :class:`_schema.Table`:: + from sqlalchemy import create_engine + from sqlalchemy import Table + from sqlalchemy.orm import declarative_base + engine = create_engine( "postgresql+psycopg2://user:pass@hostname/my_existing_database" ) + Base = declarative_base() class MyClass(Base): __table__ = Table( @@ -356,17 +356,47 @@ use a declarative hybrid mapping, passing the autoload_with=engine, ) -A major downside of the above approach however is that it requires the database +A variant on the above pattern that scales much better is to use the +:meth:`.MetaData.reflect` method to reflect a full set of :class:`.Table` +objects at once, then refer to them from the :class:`.MetaData`:: + + + from sqlalchemy import create_engine + from sqlalchemy import Table + from sqlalchemy.orm import declarative_base + + engine = create_engine( + "postgresql+psycopg2://user:pass@hostname/my_existing_database" + ) + + Base = declarative_base() + + Base.metadata.reflect(engine) + + class MyClass(Base): + __table__ = Base.metadata.tables['mytable'] + +.. seealso:: + + :ref:`mapper_automated_reflection_schemes` - further notes on using + table reflection with mapped classes + +A major downside to the above approach is that the mapped classes cannot +be declared until the tables have been reflected, which requires the database connectivity source to be present while the application classes are being declared; it's typical that classes are declared as the modules of an application are being imported, but database connectivity isn't available until the application starts running code so that it can consume configuration -information and create an engine. +information and create an engine. There are currently two approaches +to working around this. + +.. _orm_declarative_reflected_deferred_reflection: Using DeferredReflection ^^^^^^^^^^^^^^^^^^^^^^^^^ -To accommodate this case, a simple extension called the +To accommodate the use case of declaring mapped classes where reflection of +table metadata can occur afterwards, a simple extension called the :class:`.DeferredReflection` mixin is available, which alters the declarative mapping process to be delayed until a special class-level :meth:`.DeferredReflection.prepare` method is called, which will perform @@ -408,17 +438,22 @@ complete until we do so, given an :class:`_engine.Engine`:: The purpose of the ``Reflected`` class is to define the scope at which classes should be reflectively mapped. The plugin will search among the subclass tree of the target against which ``.prepare()`` is called and reflect -all tables. +all tables which are named by declared classes; tables in the target database +that are not part of mappings and are not related to the target tables +via foreign key constraint will not be reflected. Using Automap ^^^^^^^^^^^^^^ -A more automated solution to mapping against an existing database where -table reflection is to be used is to use the :ref:`automap_toplevel` -extension. This extension will generate entire mapped classes from a -database schema, and allows several hooks for customization including the -ability to explicitly map some or all classes while still making use of -reflection to fill in the remaining columns. +A more automated solution to mapping against an existing database where table +reflection is to be used is to use the :ref:`automap_toplevel` extension. This +extension will generate entire mapped classes from a database schema, including +relationships between classes based on observed foreign key constraints. While +it includes hooks for customization, such as hooks that allow custom +class naming and relationship naming schemes, automap is oriented towards an +expedient zero-configuration style of working. If an application wishes to have +a fully explicit model that makes use of table reflection, the +:ref:`orm_declarative_reflected_deferred_reflection` may be preferable. .. seealso:: diff --git a/doc/build/orm/inheritance.rst b/doc/build/orm/inheritance.rst index 18bf98c4ed..483dba8e30 100644 --- a/doc/build/orm/inheritance.rst +++ b/doc/build/orm/inheritance.rst @@ -995,7 +995,7 @@ that we omit the ``employee`` table. .. seealso:: - :ref:`classical_mapping` - background information on "classical" mappings + :ref:`orm_imperative_mapping` - background information on imperative, or "classical" mappings diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index 4de0869032..13d2ce860d 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -1,13 +1,20 @@ .. _mapper_config_toplevel: -==================== -Mapper Configuration -==================== +=============================== +ORM Mapped Class Configuration +=============================== -This section describes a variety of configurational patterns that are usable -with mappers. It assumes you've worked through :ref:`ormtutorial_toplevel` and -know how to construct and use rudimentary mappers and relationships. +Detailed reference for ORM configuration, not including +relationships, which are detailed at +:ref:`relationship_config_toplevel`. + +For a quick look at a typical ORM configuration, start with +:ref:`orm_quickstart`. + +For an introduction to the concept of object relational mapping as implemented +in SQLAlchemy, it's first introduced in the :ref:`unified_tutorial` at +:ref:`tutorial_orm_table_metadata`. .. toctree:: @@ -15,6 +22,7 @@ know how to construct and use rudimentary mappers and relationships. mapping_styles declarative_mapping + dataclasses scalar_mapping inheritance nonstandard_mappings diff --git a/doc/build/orm/mapping_columns.rst b/doc/build/orm/mapping_columns.rst index 66fb22e01b..188a5fc093 100644 --- a/doc/build/orm/mapping_columns.rst +++ b/doc/build/orm/mapping_columns.rst @@ -5,11 +5,30 @@ Mapping Table Columns ===================== -The default behavior of :class:`_orm.Mapper` is to assemble all the columns in -the mapped :class:`_schema.Table` into mapped object attributes, each of which are -named according to the name of the column itself (specifically, the ``key`` -attribute of :class:`_schema.Column`). This behavior can be -modified in several ways. +Introductory background on mapping to columns falls under the subject of +:class:`.Table` configuration; the general form falls under one of three +forms: + +* :ref:`orm_declarative_table` - :class:`.Column` objects are associated with a + :class:`.Table` as well as with an ORM mapping in one step by declaring + them inline as class attributes. +* :ref:`orm_imperative_table_configuration` - :class:`.Column` objects are + associated directly with their :class:`.Table` object, as detailed at + :ref:`metadata_describing_toplevel`; the columns are then mapped by + the Declarative process by associating the :class:`.Table` with the + class to be mapped via the ``__table__`` attribute. +* :ref:`orm_imperative_mapping` - like "Imperative Table", :class:`.Column` + objects are associated directly with their :class:`.Table` object; the + columns are then mapped by the Imperative process using + :meth:`_orm.registry.map_imperatively`. + +In all of the above cases, the :class:`_orm.Mapper` constructor is ultimately +invoked with a completed :class:`.Table` object passed as the selectable unit +to be mapped. The behavior of :class:`_orm.Mapper` then is to assemble all the +columns in the mapped :class:`_schema.Table` into mapped object attributes, +each of which are named according to the name of the column itself +(specifically, the ``key`` attribute of :class:`_schema.Column`). This behavior +can be modified in several ways. .. _mapper_column_distinct_names: @@ -51,7 +70,6 @@ dictionary with the desired key:: 'name': user_table.c.user_name, }) -In the next section we'll examine the usage of ``.key`` more closely. .. _mapper_automated_reflection_schemes: @@ -62,7 +80,7 @@ In the previous section :ref:`mapper_column_distinct_names`, we showed how a :class:`_schema.Column` explicitly mapped to a class can have a different attribute name than the column. But what if we aren't listing out :class:`_schema.Column` objects explicitly, and instead are automating the production of :class:`_schema.Table` -objects using reflection (e.g. as described in :ref:`metadata_reflection_toplevel`)? +objects using reflection (i.e. as described in :ref:`metadata_reflection_toplevel`)? In this case we can make use of the :meth:`_events.DDLEvents.column_reflect` event to intercept the production of :class:`_schema.Column` objects and provide them with the :attr:`_schema.Column.key` of our choice. The event is most easily @@ -82,33 +100,19 @@ with our event that adds a new ".key" element, such as in a mapping as below:: __table__ = Table("some_table", Base.metadata, autoload_with=some_engine) -The approach also works with the :ref:`automap_toplevel` extension. See -the section :ref:`automap_intercepting_columns` for background. +The approach also works with both the :class:`.DeferredReflection` base class +as well as with the :ref:`automap_toplevel` extension. For automap +specifically, see the section :ref:`automap_intercepting_columns` for +background. .. seealso:: + :ref:`orm_declarative_reflected` + :meth:`_events.DDLEvents.column_reflect` :ref:`automap_intercepting_columns` - in the :ref:`automap_toplevel` documentation -.. _column_prefix: - -Naming All Columns with a Prefix --------------------------------- - -A quick approach to prefix column names, typically when mapping -to an existing :class:`_schema.Table` object, is to use ``column_prefix``:: - - class User(Base): - __table__ = user_table - __mapper_args__ = {'column_prefix':'_'} - -The above will place attribute names such as ``_user_id``, ``_user_name``, -``_password`` etc. on the mapped ``User`` class. - -This approach is uncommon in modern usage. For dealing with reflected -tables, a more flexible approach is to use that described in -:ref:`mapper_automated_reflection_schemes`. .. _column_property_options: @@ -162,6 +166,75 @@ See examples of this usage at :ref:`mapper_sql_expressions`. .. autofunction:: column_property +.. _mapper_primary_key: + +Mapping to an Explicit Set of Primary Key Columns +------------------------------------------------- + +The :class:`.Mapper` construct in order to successfully map a table always +requires that at least one column be identified as the "primary key" for +that selectable. This is so that when an ORM object is loaded or persisted, +it can be placed in the :term:`identity map` with an appropriate +:term:`identity key`. + +To support this use case, all :class:`.FromClause` objects (where +:class:`.FromClause` is the common base for objects such as :class:`.Table`, +:class:`.Join`, :class:`.Subquery`, etc.) have an attribute +:attr:`.FromClause.primary_key` which returns a collection of those +:class:`.Column` objects that indicate they are part of a "primary key", +which is derived from each :class:`.Column` object being a member of a +:class:`.PrimaryKeyConstraint` collection that's associated with the +:class:`.Table` from which they ultimately derive. + +In those cases where the selectable being mapped does not include columns +that are explicitly part of the primary key constraint on their parent table, +a user-defined set of primary key columns must be defined. The +:paramref:`.Mapper.primary_key` parameter is used for this purpose. + +Given the following example of a :ref:`Imperative Table ` +mapping against an existing :class:`.Table` object, as would occur in a scenario +such as when the :class:`.Table` were :term:`reflected` from an existing +database, where the table does not have any declared primary key, we may +map such a table as in the following example:: + + from sqlalchemy import Column + from sqlalchemy import MetaData + from sqlalchemy import String + from sqlalchemy import Table + from sqlalchemy import UniqueConstraint + from sqlalchemy.orm import declarative_base + + + metadata = MetaData() + group_users = Table( + "group_users", + metadata, + Column("user_id", String(40), nullable=False), + Column("group_id", String(40), nullable=False), + UniqueConstraint("user_id", "group_id") + ) + + + Base = declarative_base() + + + class GroupUsers(Base): + __table__ = group_users + __mapper_args__ = { + "primary_key": [group_users.c.user_id, group_users.c.group_id] + } + +Above, the ``group_users`` table is an association table of some kind +with string columns ``user_id`` and ``group_id``, but no primary key is set up; +instead, there is only a :class:`.UniqueConstraint` establishing that the +two columns represent a unique key. The :class:`.Mapper` does not automatically +inspect unique constraints for primary keys; instead, we make use of the +:paramref:`.Mapper.primary_key` parameter, passing a collection of +``[group_users.c.user_id, group_users.c.group_id]``, indicating that these two +columns should be used in order to construct the identity key for instances +of the ``GroupUsers`` class. + + .. _include_exclude_cols: Mapping a Subset of Table Columns diff --git a/doc/build/orm/mapping_styles.rst b/doc/build/orm/mapping_styles.rst index e1b2b78c8f..ea01653b21 100644 --- a/doc/build/orm/mapping_styles.rst +++ b/doc/build/orm/mapping_styles.rst @@ -1,51 +1,61 @@ .. _orm_mapping_classes_toplevel: -======================= -Mapping Python Classes -======================= +========================== +ORM Mapped Class Overview +========================== + +Overview of ORM class mapping configuration. + +For readers new to the SQLAlchemy ORM and/or new to Python in general, +it's recommended to browse through the +:ref:`orm_quickstart` and preferably to work through the +:ref:`unified_tutorial`, where ORM configuration is first introduced at +:ref:`tutorial_orm_table_metadata`. + + +ORM Mapping Styles +================== + +SQLAlchemy features two distinct styles of mapper configuration, which then +feature further sub-options for how they are set up. The variability in mapper +styles is present to suit a varied list of developer preferences, including +the degree of abstraction of a user-defined class from how it is to be +mapped to relational schema tables and columns, what kinds of class hierarchies +are in use, including whether or not custom metaclass schemes are present, +and finally if there are other class-instrumentation approaches present such +as if Python dataclasses_ are in use simultaneously. + +In modern SQLAlchemy, the difference between these styles is mostly +superficial; when a particular SQLAlchemy configurational style is used to +express the intent to map a class, the internal process of mapping the class +proceeds in mostly the same way for each, where the end result is always a +user-defined class that has a :class:`_orm.Mapper` configured against a +selectable unit, typically represented by a :class:`_schema.Table` object, and +the class itself has been :term:`instrumented` to include behaviors linked to +relational operations both at the level of the class as well as on instances of +that class. As the process is basically the same in all cases, classes mapped +from different styles are always fully interoperable with each other. -SQLAlchemy historically features two distinct styles of mapper configuration. The original mapping API is commonly referred to as "classical" style, whereas the more automated style of mapping is known as "declarative" style. SQLAlchemy now refers to these two mapping styles as **imperative mapping** and **declarative mapping**. -Both styles may be used interchangeably, as the end result of each is exactly -the same - a user-defined class that has a :class:`_orm.Mapper` configured -against a selectable unit, typically represented by a :class:`_schema.Table` -object. - -Both imperative and declarative mapping begin with an ORM :class:`_orm.registry` -object, which maintains a set of classes that are mapped. This registry -is present for all mappings. +Regardless of what style of mapping used, all ORM mappings as of SQLAlchemy 1.4 +originate from a single object known as :class:`_orm.registry`, which is a +registry of mapped classes. Using this registry, a set of mapper configurations +can be finalized as a group, and classes within a particular registry may refer +to each other by name within the configurational process. .. versionchanged:: 1.4 Declarative and classical mapping are now referred to as "declarative" and "imperative" mapping, and are unified internally, all originating from the :class:`_orm.registry` construct that represents a collection of related mappings. -The full suite of styles can be hierarchically organized as follows: - -* :ref:`orm_declarative_mapping` - * Using :func:`_orm.declarative_base` Base class w/ metaclass - * :ref:`orm_declarative_table` - * :ref:`Imperative Table (a.k.a. "hybrid table") ` - * Using :meth:`_orm.registry.mapped` Declarative Decorator - * :ref:`Declarative Table ` - combine :meth:`_orm.registry.mapped` - with ``__tablename__`` - * Imperative Table (Hybrid) - combine :meth:`_orm.registry.mapped` with ``__table__`` - * :ref:`orm_declarative_dataclasses` - * :ref:`orm_declarative_dataclasses_imperative_table` - * :ref:`orm_declarative_dataclasses_declarative_table` - * :ref:`orm_declarative_attrs_imperative_table` -* :ref:`Imperative (a.k.a. "classical" mapping) ` - * Using :meth:`_orm.registry.map_imperatively` - * :ref:`orm_imperative_dataclasses` - .. _orm_declarative_mapping: Declarative Mapping -=================== +------------------- The **Declarative Mapping** is the typical way that mappings are constructed in modern SQLAlchemy. The most common pattern @@ -73,11 +83,10 @@ Above, the :func:`_orm.declarative_base` callable returns a new base class from which new classes to be mapped may inherit from, as above a new mapped class ``User`` is constructed. -The base class refers to a -:class:`_orm.registry` object that maintains a collection of related mapped -classes. The :func:`_orm.declarative_base` function is in fact shorthand -for first creating the registry with the :class:`_orm.registry` -constructor, and then generating a base class using the +The base class refers to a :class:`_orm.registry` object that maintains a +collection of related mapped classes. The :func:`_orm.declarative_base` +function is in fact shorthand for first creating the registry with the +:class:`_orm.registry` constructor, and then generating a base class using the :meth:`_orm.registry.generate_base` method:: from sqlalchemy.orm import registry @@ -87,9 +96,7 @@ constructor, and then generating a base class using the mapper_registry = registry() Base = mapper_registry.generate_base() -The :class:`_orm.registry` is used directly in order to access a variety -of mapping styles to suit different use cases. The primary mapping styles -offered by :class:`_orm.registry` are further detailed in the following +The major Declarative mapping styles are further detailed in the following sections: * :ref:`orm_declarative_generated_base_class` - declarative mapping using a @@ -98,26 +105,25 @@ sections: * :ref:`orm_declarative_decorator` - declarative mapping using a decorator, rather than a base class. -* :ref:`orm_imperative_mapping` - imperative mapping, specifying all mapping - arguments directly rather than scanning a class. - -Documentation for Declarative mapping continues at :ref:`declarative_config_toplevel`. - -.. seealso:: - - * :ref:`declarative_config_toplevel` +Within the scope of a Declarative mapped class, there are also two varieties +of how the :class:`_schema.Table` metadata may be declared. These include: - * :ref:`orm_declarative_styles_toplevel` - * :ref:`orm_declarative_table_config_toplevel` - * :ref:`orm_declarative_mapper_config_toplevel` +* :ref:`orm_declarative_table` - individual :class:`_schema.Column` definitions + are combined with a table name and additional arguments, where the Declarative + mapping process will construct a :class:`_schema.Table` object to be mapped. +* :ref:`orm_imperative_table_configuration` - Instead of specifying table name + and attributes separately, an explicitly constructed :class:`_schema.Table` object + is associated with a class that is otherwise mapped declaratively. This + style of mapping is a hybrid of "declarative" and "imperative" mapping. -.. _orm_imperative_mapping: +Documentation for Declarative mapping continues at :ref:`declarative_config_toplevel`. .. _classical_mapping: +.. _orm_imperative_mapping: -Imperative (a.k.a. Classical) Mappings -====================================== +Imperative Mapping +------------------- An **imperative** or **classical** mapping refers to the configuration of a mapped class using the :meth:`_orm.registry.map_imperatively` method, @@ -181,83 +187,10 @@ user-defined class, linked together with a :class:`_orm.Mapper` object. When we as well - it's still used, just behind the scenes. - - -.. _orm_imperative_dataclasses: - -Imperative Mapping with Dataclasses and Attrs ---------------------------------------------- - -As described in the section :ref:`orm_declarative_dataclasses`, the -``@dataclass`` decorator and the ``attrs`` library both work as class -decorators that are applied to a class first, before it is passed to -SQLAlchemy for mapping. Just like we can use the -:meth:`_orm.registry.mapped` decorator in order to apply declarative-style -mapping to the class, we can also pass it to the :meth:`_orm.registry.map_imperatively` -method so that we may pass all :class:`_schema.Table` and :class:`_orm.Mapper` -configuration imperatively to the function rather than having them defined -on the class itself as declarative class variables:: - - from __future__ import annotations - - from dataclasses import dataclass - from dataclasses import field - from typing import List - - from sqlalchemy import Column - from sqlalchemy import ForeignKey - from sqlalchemy import Integer - from sqlalchemy import MetaData - from sqlalchemy import String - from sqlalchemy import Table - from sqlalchemy.orm import registry - from sqlalchemy.orm import relationship - - mapper_registry = registry() - - @dataclass - class User: - id: int = field(init=False) - name: str = None - fullname: str = None - nickname: str = None - addresses: List[Address] = field(default_factory=list) - - @dataclass - class Address: - id: int = field(init=False) - user_id: int = field(init=False) - email_address: str = None - - metadata_obj = MetaData() - - user = Table( - 'user', - metadata_obj, - Column('id', Integer, primary_key=True), - Column('name', String(50)), - Column('fullname', String(50)), - Column('nickname', String(12)), - ) - - address = Table( - 'address', - metadata_obj, - Column('id', Integer, primary_key=True), - Column('user_id', Integer, ForeignKey('user.id')), - Column('email_address', String(50)), - ) - - mapper_registry.map_imperatively(User, user, properties={ - 'addresses': relationship(Address, backref='user', order_by=address.c.id), - }) - - mapper_registry.map_imperatively(Address, address) - .. _orm_mapper_configuration_overview: -Mapper Configuration Overview -============================= +Mapped Class Essential Components +================================== With all mapping forms, the mapping of the class can be configured in many ways by passing construction arguments that become part of the :class:`_orm.Mapper` @@ -339,27 +272,17 @@ to :meth:`_orm.registry.map_imperatively`, which will pass it along to the Other mapper configuration parameters ------------------------------------- -These flags are documented at :class:`_orm.Mapper`. - When mapping with the :ref:`declarative ` mapping style, additional mapper configuration arguments are configured via the -``__mapper_args__`` class attribute, documented at -:ref:`orm_declarative_mapper_options` +``__mapper_args__`` class attribute. Examples of use are available +at :ref:`orm_declarative_mapper_options`. When mapping with the :ref:`imperative ` style, keyword arguments are passed to the to :meth:`_orm.registry.map_imperatively` method which passes them along to the :class:`_orm.Mapper` class. +The full range of parameters accepted are documented at :class:`_orm.Mapper`. -.. [1] When running under Python 2, a Python 2 "old style" class is the only - kind of class that isn't compatible. When running code on Python 2, - all classes must extend from the Python ``object`` class. Under - Python 3 this is always the case. - -.. [2] There is a legacy feature known as a "non primary mapper", where - additional :class:`_orm.Mapper` objects may be associated with a class - that's already mapped, however they don't apply instrumentation - to the class. This feature is deprecated as of SQLAlchemy 1.3. Mapped Class Behavior @@ -417,15 +340,17 @@ The constructor also applies to imperative mappings:: mapper_registry.map_imperatively(User, user_table) -The above class, mapped imperatively as described at :ref:`classical_mapping`, +The above class, mapped imperatively as described at :ref:`orm_imperative_mapping`, will also feature the default constructor associated with the :class:`_orm.registry`. .. versionadded:: 1.4 classical mappings now support a standard configuration-level constructor when they are mapped via the :meth:`_orm.registry.map_imperatively` method. -Runtime Introspection of Mapped classes and Mappers ---------------------------------------------------- +.. _orm_mapper_inspection: + +Runtime Introspection of Mapped classes, Instances and Mappers +--------------------------------------------------------------- A class that is mapped using :class:`_orm.registry` will also feature a few attributes that are common to all mappings: @@ -445,12 +370,12 @@ attributes that are common to all mappings: .. * The ``__table__`` attribute will refer to the :class:`_schema.Table`, or - more generically to the :class:`_schema.FromClause` object, to which the + more generically to the :class:`.FromClause` object, to which the class is mapped:: table = User.__table__ - This :class:`_schema.FromClause` is also what's returned when using the + This :class:`.FromClause` is also what's returned when using the :attr:`_orm.Mapper.local_table` attribute of the :class:`_orm.Mapper`:: table = inspect(User).local_table @@ -465,8 +390,10 @@ attributes that are common to all mappings: .. -Mapper Inspection Features --------------------------- +.. _orm_mapper_inspection_mapper: + +Inspection of Mapper objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As illustrated in the previous section, the :class:`_orm.Mapper` object is available from any mapped class, regardless of method, using the @@ -509,8 +436,90 @@ As well as :attr:`_orm.Mapper.column_attrs`:: .. seealso:: - :ref:`core_inspection_toplevel` + :class:`.Mapper` + +.. _orm_mapper_inspection_instancestate: + +Inspection of Mapped Instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`_sa.inspect` function also provides information about instances +of a mapped class. When applied to an instance of a mapped class, rather +than the class itself, the object returned is known as :class:`.InstanceState`, +which will provide links to not only the :class:`.Mapper` in use by the +class, but also a detailed interface that provides information on the state +of individual attributes within the instance including their current value +and how this relates to what their database-loaded value is. + +Given an instance of the ``User`` class loaded from the database:: + + >>> u1 = session.scalars(select(User)).first() + +The :func:`_sa.inspect` function will return to us an :class:`.InstanceState` +object:: + + >>> insp = inspect(u1) + >>> insp + + +With this object we can see elements such as the :class:`.Mapper`:: + + >>> insp.mapper + + +The :class:`_orm.Session` to which the object is :term:`attached`, if any:: + + >>> insp.session + + +Information about the current :ref:`persistence state ` +for the object:: + + >>> insp.persistent + True + >>> insp.pending + False - :class:`_orm.Mapper` +Attribute state information such as attributes that have not been loaded or +:term:`lazy loaded` (assume ``addresses`` refers to a :func:`_orm.relationship` +on the mapped class to a related class):: + + >>> insp.unloaded + {'addresses'} + +Information regarding the current in-Python status of attributes, such as +attributes that have not been modified since the last flush:: + + >>> insp.unmodified + {'nickname', 'name', 'fullname', 'id'} + +as well as specific history on modifications to attributes since the last flush:: + + >>> insp.attrs.nickname.value + 'nickname' + >>> u1.nickname = 'new nickname' + >>> insp.attrs.nickname.history + History(added=['new nickname'], unchanged=(), deleted=['nickname']) + +.. seealso:: :class:`.InstanceState` + + :attr:`.InstanceState.attrs` + + :class:`.AttributeState` + + +.. _dataclasses: https://docs.python.org/3/library/dataclasses.html +.. _attrs: https://pypi.org/project/attrs/ + +.. [1] When running under Python 2, a Python 2 "old style" class is the only + kind of class that isn't compatible. When running code on Python 2, + all classes must extend from the Python ``object`` class. Under + Python 3 this is always the case. + +.. [2] There is a legacy feature known as a "non primary mapper", where + additional :class:`_orm.Mapper` objects may be associated with a class + that's already mapped, however they don't apply instrumentation + to the class. This feature is deprecated as of SQLAlchemy 1.3. + diff --git a/doc/build/orm/session_state_management.rst b/doc/build/orm/session_state_management.rst index b3e15e7689..be991e182c 100644 --- a/doc/build/orm/session_state_management.rst +++ b/doc/build/orm/session_state_management.rst @@ -50,7 +50,19 @@ Getting the Current State of an Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The actual state of any mapped object can be viewed at any time using -the :func:`_sa.inspect` system:: +the :func:`_sa.inspect` function on a mapped instance; this function will +return the corresponding :class:`.InstanceState` object which manages the +internal ORM state for the object. :class:`.InstanceState` provides, among +other accessors, boolean attributes indicating the persistence state +of the object, including: + +* :attr:`.InstanceState.transient` +* :attr:`.InstanceState.pending` +* :attr:`.InstanceState.persistent` +* :attr:`.InstanceState.deleted` +* :attr:`.InstanceState.detached` + +E.g.:: >>> from sqlalchemy import inspect >>> insp = inspect(my_object) @@ -59,15 +71,8 @@ the :func:`_sa.inspect` system:: .. seealso:: - :attr:`.InstanceState.transient` - - :attr:`.InstanceState.pending` - - :attr:`.InstanceState.persistent` - - :attr:`.InstanceState.deleted` - - :attr:`.InstanceState.detached` + :ref:`orm_mapper_inspection_instancestate` - further examples of + :class:`.InstanceState` .. _session_attributes: diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index 19dadf29c4..6d441c9e34 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -10,8 +10,6 @@ r"""Define an extension to the :mod:`sqlalchemy.ext.declarative` system which automatically generates mapped classes and relationships from a database schema, typically though not necessarily one which is reflected. -.. versionadded:: 0.9.1 Added :mod:`sqlalchemy.ext.automap`. - It is hoped that the :class:`.AutomapBase` system provides a quick and modernized solution to the problem that the very famous `SQLSoup `_ @@ -22,6 +20,15 @@ Declarative class techniques, :class:`.AutomapBase` seeks to provide a well-integrated approach to the issue of expediently auto-generating ad-hoc mappings. +.. tip:: The :ref:`automap_toplevel` extension is geared towards a + "zero declaration" approach, where a complete ORM model including classes + and pre-named relationships can be generated on the fly from a database + schema. For applications that still want to use explicit class declarations + including explicit relationship definitions in conjunction with reflection + of tables, the :class:`.DeferredReflection` class, described at + :ref:`orm_declarative_reflected_deferred_reflection`, is a better choice. + + Basic Use ========= @@ -123,6 +130,9 @@ explicit table declaration:: Specifying Classes Explicitly ============================= +.. tip:: If explicit classes are expected to be prominent in an application, + consider using :class:`.DeferredReflection` instead. + The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined explicitly, in a way similar to that of the :class:`.DeferredReflection` class. Classes that extend from :class:`.AutomapBase` act like regular declarative diff --git a/lib/sqlalchemy/ext/declarative/extensions.py b/lib/sqlalchemy/ext/declarative/extensions.py index 22fa83c58f..097599a3bf 100644 --- a/lib/sqlalchemy/ext/declarative/extensions.py +++ b/lib/sqlalchemy/ext/declarative/extensions.py @@ -371,6 +371,11 @@ class DeferredReflection: ReflectedOne.prepare(engine_one) ReflectedTwo.prepare(engine_two) + .. seealso:: + + :ref:`orm_declarative_reflected_deferred_reflection` - in the + :ref:`orm_declarative_table_config_toplevel` section. + """ @classmethod diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 2a227f9daa..0241a31232 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -289,10 +289,29 @@ class Mapper( :param column_prefix: A string which will be prepended to the mapped attribute name when :class:`_schema.Column` objects are automatically assigned as attributes to the - mapped class. Does not affect explicitly specified - column-based properties. - - See the section :ref:`column_prefix` for an example. + mapped class. Does not affect :class:`.Column` objects that + are mapped explicitly in the :paramref:`.Mapper.properties` + dictionary. + + This parameter is typically useful with imperative mappings + that keep the :class:`.Table` object separate. Below, assuming + the ``user_table`` :class:`.Table` object has columns named + ``user_id``, ``user_name``, and ``password``:: + + class User(Base): + __table__ = user_table + __mapper_args__ = {'column_prefix':'_'} + + The above mapping will assign the ``user_id``, ``user_name``, and + ``password`` columns to attributes named ``_user_id``, + ``_user_name``, and ``_password`` on the mapped ``User`` class. + + The :paramref:`.Mapper.column_prefix` parameter is uncommon in + modern use. For dealing with reflected tables, a more flexible + approach to automating a naming scheme is to intercept the + :class:`.Column` objects as they are reflected; see the section + :ref:`mapper_automated_reflection_schemes` for notes on this usage + pattern. :param concrete: If True, indicates this mapper should use concrete table inheritance with its parent mapper. @@ -580,12 +599,21 @@ class Mapper( based on all those :class:`.MapperProperty` instances declared in the declared class body. + .. seealso:: + + :ref:`orm_mapping_properties` - in the + :ref:`orm_mapping_classes_toplevel` + :param primary_key: A list of :class:`_schema.Column` objects which define the primary key to be used against this mapper's selectable unit. This is normally simply the primary key of the ``local_table``, but can be overridden here. + .. seealso:: + + :ref:`mapper_primary_key` - background and example use + :param version_id_col: A :class:`_schema.Column` that will be used to keep a running version id of rows in the table. This is used to detect concurrent updates or diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index af9f487066..db4263f03e 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -118,10 +118,12 @@ class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]): >>> from sqlalchemy import inspect >>> insp = inspect(some_mapped_object) + >>> insp.attrs.nickname.history + History(added=['new nickname'], unchanged=(), deleted=['nickname']) .. seealso:: - :ref:`core_inspection_toplevel` + :ref:`orm_mapper_inspection_instancestate` """ -- 2.47.2