From 0ccce7191c629d11b8afda9eab911c1a56bf6a72 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 30 Jan 2022 14:25:36 -0500 Subject: [PATCH] split out declarative varieties into the declarative mapping section specific declarative styles like those for dataclasses and attrs should be in the more reference-oriented "declarative_mapping" section rather than the more introduction-oriented mapping_styles. this will also make it easier for us to add still more ways of mapping declaratively for 2.0. Change-Id: I2700c2c2b34db9680f9cbe6ed6197add773a6a5d --- doc/build/orm/declarative_mapping.rst | 1 + doc/build/orm/declarative_styles.rst | 482 ++++++++++++++++++++++++++ doc/build/orm/mapping_styles.rst | 425 +---------------------- 3 files changed, 493 insertions(+), 415 deletions(-) create mode 100644 doc/build/orm/declarative_styles.rst diff --git a/doc/build/orm/declarative_mapping.rst b/doc/build/orm/declarative_mapping.rst index 9d2f3af40a..1bb07e6af4 100644 --- a/doc/build/orm/declarative_mapping.rst +++ b/doc/build/orm/declarative_mapping.rst @@ -12,6 +12,7 @@ top level introduction. .. toctree:: :maxdepth: 3 + declarative_styles declarative_tables declarative_config declarative_mixins diff --git a/doc/build/orm/declarative_styles.rst b/doc/build/orm/declarative_styles.rst new file mode 100644 index 0000000000..08c713a233 --- /dev/null +++ b/doc/build/orm/declarative_styles.rst @@ -0,0 +1,482 @@ +.. _orm_declarative_styles_toplevel: + +========================== +Declarative Mapping Styles +========================== + +As introduced at :ref:`orm_declarative_mapping`, the **Declarative Mapping** is +the typical way that mappings are constructed in modern SQLAlchemy. This +section will provide an overview of forms that may be used for Declarative +mapper configuration. + + +.. _orm_declarative_generated_base_class: + +Using a Generated Base Class +---------------------------- + +The most common approach is to generate a "base" class using the +:func:`_orm.declarative_base` function:: + + from sqlalchemy.orm import declarative_base + + # declarative base class + Base = declarative_base() + + +The declarative base class may also be created from an existing +:class:`_orm.registry`, by using the :meth:`_orm.registry.generate_base` +method:: + + from sqlalchemy.orm import registry + + reg = registry() + + # declarative base class + Base = reg.generate_base() + +With the declarative base class, new mapped classes are declared as subclasses +of the base:: + + from sqlalchemy import Column, Integer, String, ForeignKey + from sqlalchemy.orm import declarative_base + + # declarative base class + Base = declarative_base() + + # an example mapping using the base + class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + name = Column(String) + fullname = Column(String) + nickname = Column(String) + +Above, the :func:`_orm.declarative_base` function returns a new base class from +which new classes to be mapped may inherit from, as above a new mapped +class ``User`` is constructed. + +For each subclass constructed, the body of the class then follows the +declarative mapping approach which defines both a :class:`_schema.Table` +as well as a :class:`_orm.Mapper` object behind the scenes which comprise +a full mapping. + +.. seealso:: + + :ref:`orm_declarative_table_config_toplevel` + + :ref:`orm_declarative_mapper_config_toplevel` + + +.. _orm_explicit_declarative_base: + +Creating an Explicit Base Non-Dynamically (for use with mypy, similar) +---------------------------------------------------------------------- + +SQLAlchemy includes a :ref:`Mypy plugin ` that automatically +accommodates for the dynamically generated ``Base`` class +delivered by SQLAlchemy functions like :func:`_orm.declarative_base`. + +When this plugin is not in use, or when using other :pep:`484` tools which +may not know how to interpret this class, the declarative base class may +be produced in a fully explicit fashion using the +:class:`_orm.DeclarativeMeta` directly as follows:: + + from sqlalchemy.orm import registry + from sqlalchemy.orm.decl_api import DeclarativeMeta + + mapper_registry = registry() + + class Base(metaclass=DeclarativeMeta): + __abstract__ = True + + registry = mapper_registry + metadata = mapper_registry.metadata + + __init__ = mapper_registry.constructor + +The above ``Base`` is equivalent to one created using the +:meth:`_orm.registry.generate_base` method and will be fully understood by +type analysis tools without the use of plugins. + +.. seealso:: + + :ref:`mypy_toplevel` - background on the Mypy plugin which applies the + above structure automatically when running Mypy. + + +.. _orm_declarative_decorator: + +Declarative Mapping using a Decorator (no declarative base) +------------------------------------------------------------ + +As an alternative to using the "declarative base" class is to apply +declarative mapping to a class explicitly, using either an imperative technique +similar to that of a "classical" mapping, or more succinctly by using +a decorator. The :meth:`_orm.registry.mapped` function is a class decorator +that can be applied to any Python class with no hierarchy in place. The +Python class otherwise is configured in declarative style normally:: + + from sqlalchemy import Column, Integer, String, Text, ForeignKey + + from sqlalchemy.orm import registry + from sqlalchemy.orm import relationship + + mapper_registry = registry() + + @mapper_registry.mapped + class User: + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + name = Column(String) + + addresses = relationship("Address", back_populates="user") + + @mapper_registry.mapped + class Address: + __tablename__ = 'address' + + id = Column(Integer, primary_key=True) + user_id = Column(ForeignKey("user.id")) + email_address = Column(String) + + user = relationship("User", back_populates="addresses") + +Above, the same :class:`_orm.registry` that we'd use to generate a declarative +base class via its :meth:`_orm.registry.generate_base` method may also apply +a declarative-style mapping to a class without using a base. When using +the above style, the mapping of a particular class will **only** proceed +if the decorator is applied to that class directly. For inheritance +mappings, the decorator should be applied to each subclass:: + + from sqlalchemy.orm import registry + mapper_registry = registry() + + @mapper_registry.mapped + class Person: + __tablename__ = "person" + + person_id = Column(Integer, primary_key=True) + type = Column(String, nullable=False) + + __mapper_args__ = { + + "polymorphic_on": type, + "polymorphic_identity": "person" + } + + + @mapper_registry.mapped + class Employee(Person): + __tablename__ = "employee" + + person_id = Column(ForeignKey("person.person_id"), primary_key=True) + + __mapper_args__ = { + "polymorphic_identity": "employee" + } + +Both the "declarative table" and "imperative table" styles of declarative +mapping may be used with the above mapping style. + +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_. 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 ``@attr.s`` 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 ``@attr.s`` 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. In contrast, the ``@attr.s`` decorator actually removes its +own class-bound attributes after the decorator runs, so that SQLAlchemy's +mapping process takes over these attributes without any issue. + +.. 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 + from dataclasses import field + from typing import List + from typing import Optional + + from sqlalchemy import Column + from sqlalchemy import ForeignKey + from sqlalchemy import Integer + from sqlalchemy import String + from sqlalchemy import Table + from sqlalchemy.orm import registry + from sqlalchemy.orm import 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 + from dataclasses import field + from typing import List + + from sqlalchemy import Column + from sqlalchemy import ForeignKey + from sqlalchemy import Integer + from sqlalchemy import String + from sqlalchemy.orm import registry + from sqlalchemy.orm import 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 ``@attr.s``, in conjunction with imperative table:: + + import attr + + # other imports + + from sqlalchemy.orm import registry + + mapper_registry = registry() + + + @mapper_registry.mapped + @attr.s + 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 = attr.ib() + name = attr.ib() + fullname = attr.ib() + nickname = attr.ib() + addresses = attr.ib() + + # 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. + +.. _dataclasses: https://docs.python.org/3/library/dataclasses.html +.. _attrs: https://pypi.org/project/attrs/ diff --git a/doc/build/orm/mapping_styles.rst b/doc/build/orm/mapping_styles.rst index f273196b4a..41afa768b7 100644 --- a/doc/build/orm/mapping_styles.rst +++ b/doc/build/orm/mapping_styles.rst @@ -88,7 +88,12 @@ constructor, and then generating a base class using the 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: +of mapping styles to suit different use cases. The primary mapping styles +offered by :class:`_orm.registry` are further detailed in the following +sections: + +* :ref:`orm_declarative_generated_base_class` - declarative mapping using a + base class generated by the :class:`_orm.registry` object. * :ref:`orm_declarative_decorator` - declarative mapping using a decorator, rather than a base class. @@ -100,421 +105,12 @@ Documentation for Declarative mapping continues at :ref:`declarative_config_topl .. seealso:: - :ref:`declarative_config_toplevel` - -.. _orm_explicit_declarative_base: - -Creating an Explicit Base Non-Dynamically (for use with mypy, similar) ----------------------------------------------------------------------- - -TODO: update for 2.0 - code here may not be accurate - -SQLAlchemy includes a :ref:`Mypy plugin ` that automatically -accommodates for the dynamically generated ``Base`` class -delivered by SQLAlchemy functions like :func:`_orm.declarative_base`. - -When this plugin is not in use, or when using other :pep:`484` tools which -may not know how to interpret this class, the declarative base class may -be produced in a fully explicit fashion using the -:class:`_orm.DeclarativeMeta` directly as follows:: - - from sqlalchemy.orm import registry - from sqlalchemy.orm.decl_api import DeclarativeMeta - - mapper_registry = registry() - - class Base(metaclass=DeclarativeMeta): - __abstract__ = True - - registry = mapper_registry - metadata = mapper_registry.metadata - - __init__ = mapper_registry.constructor - -The above ``Base`` is equivalent to one created using the -:meth:`_orm.registry.generate_base` method and will be fully understood by -type analysis tools without the use of plugins. - -.. seealso:: - - :ref:`mypy_toplevel` - background on the Mypy plugin which applies the - above structure automatically when running Mypy. - - -.. _orm_declarative_decorator: - -Declarative Mapping using a Decorator (no declarative base) ------------------------------------------------------------- - -As an alternative to using the "declarative base" class is to apply -declarative mapping to a class explicitly, using either an imperative technique -similar to that of a "classical" mapping, or more succinctly by using -a decorator. The :meth:`_orm.registry.mapped` function is a class decorator -that can be applied to any Python class with no hierarchy in place. The -Python class otherwise is configured in declarative style normally:: - - from sqlalchemy import Column, Integer, String, Text, ForeignKey - - from sqlalchemy.orm import registry - from sqlalchemy.orm import relationship - - mapper_registry = registry() - - @mapper_registry.mapped - class User: - __tablename__ = 'user' - - id = Column(Integer, primary_key=True) - name = Column(String) - - addresses = relationship("Address", back_populates="user") - - @mapper_registry.mapped - class Address: - __tablename__ = 'address' - - id = Column(Integer, primary_key=True) - user_id = Column(ForeignKey("user.id")) - email_address = Column(String) - - user = relationship("User", back_populates="addresses") - -Above, the same :class:`_orm.registry` that we'd use to generate a declarative -base class via its :meth:`_orm.registry.generate_base` method may also apply -a declarative-style mapping to a class without using a base. When using -the above style, the mapping of a particular class will **only** proceed -if the decorator is applied to that class directly. For inheritance -mappings, the decorator should be applied to each subclass:: - - from sqlalchemy.orm import registry - mapper_registry = registry() - - @mapper_registry.mapped - class Person: - __tablename__ = "person" - - person_id = Column(Integer, primary_key=True) - type = Column(String, nullable=False) - - __mapper_args__ = { - - "polymorphic_on": type, - "polymorphic_identity": "person" - } - - - @mapper_registry.mapped - class Employee(Person): - __tablename__ = "employee" - - person_id = Column(ForeignKey("person.person_id"), primary_key=True) - - __mapper_args__ = { - "polymorphic_identity": "employee" - } - -Both the "declarative table" and "imperative table" styles of declarative -mapping may be used with the above mapping style. - -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_. 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 ``@attr.s`` 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 ``@attr.s`` 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. In contrast, the ``@attr.s`` decorator actually removes its -own class-bound attributes after the decorator runs, so that SQLAlchemy's -mapping process takes over these attributes without any issue. - -.. 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 - from dataclasses import field - from typing import List - from typing import Optional - - from sqlalchemy import Column - from sqlalchemy import ForeignKey - from sqlalchemy import Integer - from sqlalchemy import String - from sqlalchemy import Table - from sqlalchemy.orm import registry - from sqlalchemy.orm import relationship - - mapper_registry = registry() + * :ref:`declarative_config_toplevel` + * :ref:`orm_declarative_styles_toplevel` + * :ref:`orm_declarative_table_config_toplevel` + * :ref:`orm_declarative_mapper_config_toplevel` - @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 - from dataclasses import field - from typing import List - - from sqlalchemy import Column - from sqlalchemy import ForeignKey - from sqlalchemy import Integer - from sqlalchemy import String - from sqlalchemy.orm import registry - from sqlalchemy.orm import 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 ``@attr.s``, in conjunction with imperative table:: - - import attr - - # other imports - - from sqlalchemy.orm import registry - - mapper_registry = registry() - - - @mapper_registry.mapped - @attr.s - 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 = attr.ib() - name = attr.ib() - fullname = attr.ib() - nickname = attr.ib() - addresses = attr.ib() - - # 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. - -.. _dataclasses: https://docs.python.org/3/library/dataclasses.html -.. _attrs: https://pypi.org/project/attrs/ .. _orm_imperative_mapping: @@ -555,7 +151,6 @@ the :meth:`_orm.registry.map_imperatively` method:: mapper_registry.map_imperatively(User, user_table) - Information about mapped attributes, such as relationships to other classes, are provided via the ``properties`` dictionary. The example below illustrates a second :class:`_schema.Table` object, mapped to a class called ``Address``, then linked to ``User`` via :func:`_orm.relationship`:: -- 2.47.2