]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
re-establish section on why __init__ not called on load
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 26 Jan 2024 14:17:31 +0000 (09:17 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 26 Jan 2024 14:19:18 +0000 (09:19 -0500)
this section got lost, leaving the FAQ to point to an
empty document.  Rewrite a new section introducing that
__init__ is not called on load, illustrate strategies.

I am not that happy with *where* this doc is, as this is
supposed to be "mapping styles" high level introductory
type stuff, but there's nowhere else for it.

References: https://github.com/sqlalchemy/sqlalchemy/discussions/10923
Change-Id: Ie9260e4076bc82da0ef6dc11349a85beb0223a33

doc/build/faq/sessions.rst
doc/build/orm/mapping_styles.rst
lib/sqlalchemy/orm/events.py

index a2c61c0a41da4d840bcb56d2743bbf9a079e6cba..a95580ef51420e9816d5a4158c512ad1ff925621 100644 (file)
@@ -370,7 +370,7 @@ See :ref:`session_deleting_from_collections` for a description of this behavior.
 why isn't my ``__init__()`` called when I load objects?
 -------------------------------------------------------
 
-See :ref:`mapping_constructors` for a description of this behavior.
+See :ref:`mapped_class_load_events` for a description of this behavior.
 
 how do I use ON DELETE CASCADE with SA's ORM?
 ---------------------------------------------
index fbe4267be7807978c123679d72b1e0f534593446..4e3e3183797167c81c64c268c55b8ba0ce8ac0d9 100644 (file)
@@ -370,6 +370,13 @@ An object of type ``User`` above will have a constructor which allows
     Python dataclasses, and allows for a highly configurable constructor
     form.
 
+.. warning::
+
+    The ``__init__()`` method of the class is called only when the object is
+    constructed in Python code, and **not when an object is loaded or refreshed
+    from the database**.  See the next section :ref:`mapped_class_load_events`
+    for a primer on how to invoke special logic when objects are loaded.
+
 A class that includes an explicit ``__init__()`` method will maintain
 that method, and no default constructor will be applied.
 
@@ -404,6 +411,99 @@ will also feature the default constructor associated with the :class:`_orm.regis
    constructor when they are mapped via the :meth:`_orm.registry.map_imperatively`
    method.
 
+.. _mapped_class_load_events:
+
+Maintaining Non-Mapped State Across Loads
+------------------------------------------
+
+The ``__init__()`` method of the mapped class is invoked when the object
+is constructed directly in Python code::
+
+    u1 = User(name="some name", fullname="some fullname")
+
+However, when an object is loaded using the ORM :class:`_orm.Session`,
+the ``__init__()`` method is **not** called::
+
+    u1 = session.scalars(select(User).where(User.name == "some name")).first()
+
+The reason for this is that when loaded from the database, the operation
+used to construct the object, in the above example the ``User``, is more
+analogous to **deserialization**, such as unpickling, rather than initial
+construction.  The majority of the object's important state is not being
+assembled for the first time, it's being re-loaded from database rows.
+
+Therefore to maintain state within the object that is not part of the data
+that's stored to the database, such that this state is present when objects
+are loaded as well as constructed, there are two general approaches detailed
+below.
+
+1. Use Python descriptors like ``@property``, rather than state, to dynamically
+   compute attributes as needed.
+
+   For simple attributes, this is the simplest approach and the least error prone.
+   For example if an object ``Point`` with ``Point.x`` and ``Point.y`` wanted
+   an attribute with the sum of these attributes::
+
+      class Point(Base):
+          __tablename__ = "point"
+          id: Mapped[int] = mapped_column(primary_key=True)
+          x: Mapped[int]
+          y: Mapped[int]
+
+          @property
+          def x_plus_y(self):
+              return self.x + self.y
+
+   An advantage of using dynamic descriptors is that the value is computed
+   every time, meaning it maintains the correct value as the underlying
+   attributes (``x`` and ``y`` in this case) might change.
+
+   Other forms of the above pattern include Python standard library
+   :ref:`cached_property <https://docs.python.org/3/library/functools.html#functools.cached_property>`
+   decorator (which is cached, and not re-computed each time), as well as SQLAlchemy's :class:`.hybrid_property` decorator which
+   allows for attributes that can work for SQL querying as well.
+
+
+2. Establish state on-load using :meth:`.InstanceEvents.load`, and optionally
+   supplemental methods :meth:`.InstanceEvents.refresh` and :meth:`.InstanceEvents.refresh_flush`.
+
+   These are event hooks that are invoked whenever the object is loaded
+   from the database, or when it is refreshed after being expired.   Typically
+   only the :meth:`.InstanceEvents.load` is needed, since non-mapped local object
+   state is not affected by expiration operations.   To revise the ``Point``
+   example above looks like::
+
+      from sqlalchemy import event
+
+
+      class Point(Base):
+          __tablename__ = "point"
+          id: Mapped[int] = mapped_column(primary_key=True)
+          x: Mapped[int]
+          y: Mapped[int]
+
+          def __init__(self, x, y, **kw):
+              super().__init__(x=x, y=y, **kw)
+              self.x_plus_y = x + y
+
+
+      @event.listens_for(Point, "load")
+      def receive_load(target, context):
+          target.x_plus_y = target.x + target.y
+
+   If using the refresh events as well, the event hooks can be stacked on
+   top of one callable if needed, as::
+
+      @event.listens_for(Point, "load")
+      @event.listens_for(Point, "refresh")
+      @event.listens_for(Point, "refresh_flush")
+      def receive_load(target, context, attrs=None):
+          target.x_plus_y = target.x + target.y
+
+   Above, the ``attrs`` attribute will be present for the ``refresh`` and
+   ``refresh_flush`` events and indicate a list of attribute names that are
+   being refreshed.
+
 .. _orm_mapper_inspection:
 
 Runtime Introspection of Mapped classes, Instances and Mappers
index 185c0eaf6555bbdf1ae71803990dbaa00c23292f..828dad2b6fd5165562d9040d916e0398a5501470 100644 (file)
@@ -494,14 +494,14 @@ class InstanceEvents(event.Events[ClassManager[Any]]):
 
         .. seealso::
 
+            :ref:`mapped_class_load_events`
+
             :meth:`.InstanceEvents.init`
 
             :meth:`.InstanceEvents.refresh`
 
             :meth:`.SessionEvents.loaded_as_persistent`
 
-            :ref:`mapping_constructors`
-
         """
 
     def refresh(
@@ -534,6 +534,8 @@ class InstanceEvents(event.Events[ClassManager[Any]]):
 
         .. seealso::
 
+            :ref:`mapped_class_load_events`
+
             :meth:`.InstanceEvents.load`
 
         """
@@ -577,6 +579,8 @@ class InstanceEvents(event.Events[ClassManager[Any]]):
 
         .. seealso::
 
+            :ref:`mapped_class_load_events`
+
             :ref:`orm_server_defaults`
 
             :ref:`metadata_defaults_toplevel`