]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
ORM quickstart
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 11 Mar 2022 15:08:36 +0000 (10:08 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 11 Mar 2022 16:28:26 +0000 (11:28 -0500)
This is done in 1.4 style so it can be backported to 1.4.
Will put this up as is, we can work on it.   For 2.0, the
ORM mapping will be updated to mapped_column() style when
we do the full pass.

Change-Id: Icfdf81449973844dac244b3a107ce955a7d3b16c

doc/build/index.rst
doc/build/orm/index.rst
doc/build/orm/quickstart.rst [new file with mode: 0644]
doc/build/orm/session_basics.rst
doc/build/tutorial/data_select.rst
doc/build/tutorial/dbapi_transactions.rst
test/base/test_tutorials.py

index 3be6b4726a9e6360eea1690860082ff3496d353b..f27202d334d87ee5e3c3d3fb7e4c0737bb19eb19 100644 (file)
@@ -37,7 +37,9 @@ SQLAlchemy Documentation
     is recommended for all new users as well as veterans of older SQLAlchemy
     versions alike.
 
-    * **For all users:** :doc:`/tutorial/index` - SQLAlchemy 2.0's main tutorial
+    * **For a quick glance:** :doc:`/orm/quickstart` - a glimpse at what working with the ORM looks like
+
+    * **For all users:** :doc:`/tutorial/index` - SQLAlchemy 2.0's main tutorial, for complete learning
 
 
 .. container:: left_right_container
index c3ab4558dc5ebfddef1ce08524b59448b11d85f9..2d5beca6a3ed7f0e0b475cb9538c29188e9bc958 100644 (file)
@@ -11,6 +11,7 @@ tutorial.
 .. toctree::
     :maxdepth: 2
 
+    quickstart
     mapper_config
     relationships
     loading_objects
diff --git a/doc/build/orm/quickstart.rst b/doc/build/orm/quickstart.rst
new file mode 100644 (file)
index 0000000..d670fad
--- /dev/null
@@ -0,0 +1,415 @@
+.. _orm_quickstart:
+
+
+ORM Quick Start
+===============
+
+For new users who want to quickly see what basic ORM use looks like, here's an
+abbreviated form of the mappings and examples used in the
+:ref:`unified_tutorial`. The code here is fully runnable from a clean command
+line.
+
+As the descriptions in this section are intentionally **very short**, please
+proceed to the full :ref:`unified_tutorial` for a much more in-depth
+description of each of the concepts being illustrated here.
+
+
+Step One - Declare Models
+==========================
+
+Here, we define module-level constructs that will form the structures
+which we will be querying from the database.  This structure, known as a
+:ref:`Declarative Mapping <orm_declarative_mapping>`, defines at once both a
+Python object model, as well as
+:term:`database metadata` that describes
+real SQL tables that exist, or will exist, in a particular database::
+
+    >>> from sqlalchemy import Column
+    >>> from sqlalchemy import ForeignKey
+    >>> from sqlalchemy import Integer
+    >>> from sqlalchemy import String
+    >>> from sqlalchemy.orm import declarative_base
+    >>> from sqlalchemy.orm import relationship
+
+    >>> Base = declarative_base()
+
+    >>> class User(Base):
+    ...     __tablename__ = "user_account"
+    ...
+    ...     id = Column(Integer, primary_key=True)
+    ...     name = Column(String(30))
+    ...     fullname = Column(String)
+    ...
+    ...     addresses = relationship(
+    ...         "Address", back_populates="user", cascade="all, delete-orphan"
+    ...     )
+    ...
+    ...     def __repr__(self):
+    ...         return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
+
+    >>> class Address(Base):
+    ...     __tablename__ = "address"
+    ...
+    ...     id = Column(Integer, primary_key=True)
+    ...     email_address = Column(String, nullable=False)
+    ...     user_id = Column(Integer, ForeignKey("user_account.id"), nullable=False)
+    ...
+    ...     user = relationship("User", back_populates="addresses")
+    ...
+    ...     def __repr__(self):
+    ...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"
+
+Above, the declarative mapping makes use of :class:`_schema.Column` objects
+to define the basic units of data storage that will be in the database.
+The :func:`_orm.relationship` construct defines linkages between two
+:term:`mapped` classes, ``User`` and ``Address`` above.
+
+The schema contains necessary elements such as primary key constraints set up
+by the :paramref:`_schema.Column.primary_key` parameter, a
+:term:`foreign key constraint` configured using :class:`_schema.ForeignKey`
+(which is used by :func:`_orm.relationship` as well), and datatypes for columns
+including :class:`_types.Integer` and :class:`_types.String`.
+
+More on table metadata and an intro to ORM declared mapping is in the
+Tutorial at :ref:`tutorial_working_with_metadata`.
+
+Step Two - Create an Engine
+============================
+
+The :class:`_engine.Engine` is a **factory** that can create new
+database connections for us, which also holds onto connections inside
+of a :ref:`Connection Pool <pooling_toplevel>` for fast reuse.  For learning
+purposes, we normally use a :ref:`SQLite <sqlite_toplevel>` memory-only database
+for convenience::
+
+    >>> from sqlalchemy import create_engine
+    >>> engine = create_engine("sqlite://", echo=True, future=True)
+
+.. tip::
+
+    The ``echo=True`` parameter indicates that SQL emitted by connections will
+    be logged to standard out.  ``future=True`` is to ensure we are using
+    the latest SQLAlchemy :term:`2.0-style` APIs.
+
+A full intro to the :class:`_engine.Engine` starts at :ref:`tutorial_engine`.
+
+Step Three - Emit CREATE TABLE DDL
+==================================
+
+Using our table metadata and our engine, we can generate our schema at once
+in our target SQLite database, using a method called :meth:`_schema.MetaData.create_all`:
+
+.. sourcecode:: pycon+sql
+
+    >>> Base.metadata.create_all(engine)
+    {opensql}BEGIN (implicit)
+    PRAGMA main.table_...info("user_account")
+    ...
+    PRAGMA main.table_...info("address")
+    ...
+    CREATE TABLE user_account (
+        id INTEGER NOT NULL,
+        name VARCHAR(30),
+        fullname VARCHAR,
+        PRIMARY KEY (id)
+    )
+    ...
+    CREATE TABLE address (
+        id INTEGER NOT NULL,
+        email_address VARCHAR NOT NULL,
+        user_id INTEGER NOT NULL,
+        PRIMARY KEY (id),
+        FOREIGN KEY(user_id) REFERENCES user_account (id)
+    )
+    ...
+    COMMIT
+
+A lot just happened from that bit of Python code we wrote.  For a complete
+overview of what's going on on with Table metadata, proceed in the
+Tutorial at :ref:`tutorial_working_with_metadata`.
+
+Step Four - Create Objects and Persist
+======================================
+
+We are now ready to insert data in the database.  We accomplish this by
+creating instances of ``User`` and ``Address`` objects, which have
+an ``__init__()`` method already as established automatically by the
+declarative mapping process.  We then pass them
+to the database using an object called a :ref:`Session <tutorial_executing_orm_session>`,
+which makes use of the :class:`_engine.Engine` to interact with the
+database.  The :meth:`_orm.Session.add_all` method is used here to add
+multiple objects at once, and the :meth:`_orm.Session.commit` method
+will be used to :ref:`flush <session_flushing>` any pending changes to the
+database and then :ref:`commit <session_committing>` the current database
+transaction, which is always in progress whenever the :class:`_orm.Session`
+is used:
+
+.. sourcecode:: pycon+sql
+
+    >>> from sqlalchemy.orm import Session
+
+    >>> with Session(engine) as session:
+    ...
+    ...     spongebob = User(
+    ...         name="spongebob",
+    ...         fullname="Spongebob Squarepants",
+    ...         addresses=[Address(email_address="spongebob@sqlalchemy.org")],
+    ...     )
+    ...     sandy = User(
+    ...         name="sandy",
+    ...         fullname="Sandy Cheeks",
+    ...         addresses=[
+    ...             Address(email_address="sandy@sqlalchemy.org"),
+    ...             Address(email_address="sandy@squirrelpower.org"),
+    ...         ],
+    ...     )
+    ...     patrick = User(name="patrick", fullname="Patrick Star")
+    ...
+    ...     session.add_all([spongebob, sandy, patrick])
+    ...
+    ...     session.commit()
+    {opensql}BEGIN (implicit)
+    INSERT INTO user_account (name, fullname) VALUES (?, ?)
+    [...] ('spongebob', 'Spongebob Squarepants')
+    INSERT INTO user_account (name, fullname) VALUES (?, ?)
+    [...] ('sandy', 'Sandy Cheeks')
+    INSERT INTO user_account (name, fullname) VALUES (?, ?)
+    [...] ('patrick', 'Patrick Star')
+    INSERT INTO address (email_address, user_id) VALUES (?, ?)
+    [...] ('spongebob@sqlalchemy.org', 1)
+    INSERT INTO address (email_address, user_id) VALUES (?, ?)
+    [...] ('sandy@sqlalchemy.org', 2)
+    INSERT INTO address (email_address, user_id) VALUES (?, ?)
+    [...] ('sandy@squirrelpower.org', 2)
+    COMMIT
+
+
+.. tip::
+
+    It's recommended that the :class:`_orm.Session` be used in context
+    manager style as above, that is, using the Python ``with:`` statement.
+    The :class:`_orm.Session` object represents active database resources
+    so it's good to make sure it's closed out when a series of operations
+    are completed.  In the next section, we'll keep a :class:`_orm.Session`
+    opened just for illustration purposes.
+
+Basics on creating a :class:`_orm.Session` are at
+:ref:`tutorial_executing_orm_session` and more at :ref:`session_basics`.
+
+Then, some varieties of basic persistence operations are introduced
+at :ref:`tutorial_inserting_orm`.
+
+Step Five - Simple SELECT
+==========================
+
+With some rows in the database, here's the simplest form of emitting a SELECT
+statement to load some objects. To create SELECT statements, we use the
+:func:`_sql.select` function to create a new :class:`_sql.Select` object, which
+we then invoke using a :class:`_orm.Session`. The method that is often useful
+when querying for ORM objects is the :meth:`_orm.Session.scalars` method, which
+will return a :class:`_result.ScalarResult` object that will iterate through
+the ORM objects we've selected:
+
+.. sourcecode:: pycon+sql
+
+    >>> from sqlalchemy import select
+
+    >>> session = Session(engine)
+
+    >>> stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))
+
+    >>> for user in session.scalars(stmt):
+    ...     print(user)
+    {opensql}BEGIN (implicit)
+    SELECT user_account.id, user_account.name, user_account.fullname
+    FROM user_account
+    WHERE user_account.name IN (?, ?)
+    [...] ('spongebob', 'sandy'){stop}
+    User(id=1, name='spongebob', fullname='Spongebob Squarepants')
+    User(id=2, name='sandy', fullname='Sandy Cheeks')
+
+
+The above query also made use of the :meth:`_sql.Select.where` method
+to add WHERE criteria, and also used the :meth:`_sql.ColumnOperators.in_`
+method that's part of all SQLAlchemy column-like constructs to use the
+SQL IN operator.
+
+More detail on how to select objects and individual columns is at
+:ref:`tutorial_selecting_orm_entities`.
+
+Step Six - SELECT with JOIN
+===========================
+
+It's very common to query amongst multiple tables at once, and in SQL
+the JOIN keyword is the primary way this happens.   The :class:`_sql.Select`
+construct creates joins using the :meth:`_sql.Select.join` method:
+
+.. sourcecode:: pycon+sql
+
+    >>> stmt = (
+    ...  select(Address)
+    ...  .join(Address.user)
+    ...  .where(User.name == "sandy")
+    ...  .where(Address.email_address == "sandy@sqlalchemy.org")
+    ... )
+    >>> sandy_address = session.scalars(stmt).one()
+    {opensql}SELECT address.id, address.email_address, address.user_id
+    FROM address JOIN user_account ON user_account.id = address.user_id
+    WHERE user_account.name = ? AND address.email_address = ?
+    [...] ('sandy', 'sandy@sqlalchemy.org')
+    {stop}
+    >>> sandy_address
+    Address(id=2, email_address='sandy@sqlalchemy.org')
+
+The above query illustrates multiple WHERE criteria which are automatically
+chained together using AND, as well as how to use SQLAlchemy column-like
+objects to create "equality" comparisons, which uses the overridden Python
+method :meth:`_sql.ColumnOperators.__eq__` to produce a SQL criteria object.
+
+Some more background on the concepts above are at
+:ref:`tutorial_select_where_clause` and :ref:`tutorial_select_join`.
+
+Step Seven - Make Changes
+=========================
+
+The :class:`_orm.Session` object, in conjunction with our ORM-mapped classes
+``User`` and ``Address``, automatically track changes to the objects as they
+are made, which result in SQL statements that will be emitted the next
+time the :class:`_orm.Session` flushes.   Below, we change one email
+address associated with "sandy", and also add a new email address to
+"patrick", after emitting a SELECT to retrieve the row for "patrick":
+
+.. sourcecode:: pycon+sql
+
+    >>> stmt = select(User).where(User.name == "patrick")
+    >>> patrick = session.scalars(stmt).one()
+    {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+    FROM user_account
+    WHERE user_account.name = ?
+    [...] ('patrick',)
+    {stop}
+
+    >>> patrick.addresses.append(
+    ...     Address(email_address="patrickstar@sqlalchemy.org")
+    ... )
+    {opensql}SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
+    FROM address
+    WHERE ? = address.user_id
+    [...] (3,){stop}
+
+    >>> sandy_address.email_address = "sandy_cheeks@sqlalchemy.org"
+
+    >>> session.commit()
+    {opensql}UPDATE address SET email_address=? WHERE address.id = ?
+    [...] ('sandy_cheeks@sqlalchemy.org', 2)
+    INSERT INTO address (email_address, user_id) VALUES (?, ?)
+    [...] ('patrickstar@sqlalchemy.org', 3)
+    COMMIT
+    {stop}
+
+Notice when we accessed ``patrick.addresses``, a SELECT was emitted.  This is
+called a :term:`lazy load`.   Background on different ways to access related
+items using more or less SQL is introduced at :ref:`tutorial_orm_loader_strategies`.
+
+A detailed walkthrough on ORM data manipulation starts at
+:ref:`tutorial_orm_data_manipulation`.
+
+Step Eight - Some Deletes
+=========================
+
+All things must come to an end, as is the case for some of our database
+rows - here's a quick demonstration of two different forms of deletion, both
+of which are important based on the specific use case.
+
+First we will remove one of the ``Address`` objects from the "sandy" user.
+When the :class:`_orm.Session` next flushes, this will result in the
+row being deleted.   This behavior is something that we configured in our
+mapping called the :ref:`delete cascade <cascade_delete>`.  We can get a handle to the ``sandy``
+object by primary key using :meth:`_orm.Session.get`, then work with the object:
+
+.. sourcecode:: pycon+sql
+
+    >>> sandy = session.get(User, 2)
+    {opensql}BEGIN (implicit)
+    SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
+    FROM user_account
+    WHERE user_account.id = ?
+    [...] (2,){stop}
+
+    >>> sandy.addresses.remove(sandy_address)
+    {opensql}SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
+    FROM address
+    WHERE ? = address.user_id
+    [...] (2,)
+
+The last SELECT above was the :term:`lazy load` operation proceeding so that
+the ``sandy.addresses`` collection could be loaded, so that we could remove the
+``sandy_address`` member.  There are other ways to go about this series
+of operations that won't emit as much SQL.
+
+We can choose to emit the DELETE SQL for what's set to be changed so far, without
+committing the transaction, using the
+:meth:`_orm.Session.flush` method:
+
+.. sourcecode:: pycon+sql
+
+    >>> session.flush()
+    {opensql}DELETE FROM address WHERE address.id = ?
+    [...] (2,)
+
+Next, we will delete the "patrick" user entirely.  For a top-level delete of
+an object by itself, we use the :meth:`_orm.Session.delete` method; this
+method doesn't actually perform the deletion, but sets up the object
+to be deleted on the next flush.  The
+operation will also :term:`cascade` to related objects based on the cascade
+options that we configured, in this case, onto the related ``Address`` objects:
+
+.. sourcecode:: pycon+sql
+
+    >>> session.delete(patrick)
+    {opensql}SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
+    FROM user_account
+    WHERE user_account.id = ?
+    [...] (3,)
+    SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
+    FROM address
+    WHERE ? = address.user_id
+    [...] (3,)
+
+The :meth:`_orm.Session.delete` method in this particular case emitted two
+SELECT statements, even though it didn't emit a DELETE, which might seem surprising.
+This is because when the method went to inspect the object, it turns out the
+``patrick`` object was :term:`expired`, which happened when we last called upon
+:meth:`_orm.Session.commit`, and the SQL emitted was to re-load the rows
+from the new transaction.   This expiration is optional, and in normal
+use we will often be turning it off for situations where it doesn't apply well.
+
+To illustrate the rows being deleted, here's the commit:
+
+.. sourcecode:: pycon+sql
+
+    >>> session.commit()
+    {opensql}DELETE FROM address WHERE address.id = ?
+    [...] (4,)
+    DELETE FROM user_account WHERE user_account.id = ?
+    [...] (3,)
+    COMMIT
+    {stop}
+
+The Tutorial discusses ORM deletion at :ref:`tutorial_orm_deleting`.
+Background on object expiration is at :ref:`session_expiring`; cascades
+are discussed in depth at :ref:`unitofwork_cascades`.
+
+Step Nine - Learn the above concepts in depth
+=============================================
+
+For a new user, the above sections were likely a whirlwind tour.   There's a
+lot of important concepts in each step above that weren't covered.   With a
+quick overview of what things look like, it's recommended to work through
+the :ref:`unified_tutorial` to gain a solid working knowledge of what's
+really going on above.  Good luck!
+
+
+
+
+
index 9734b6eccb3afc1202596f28a58d5fcc8f3813ca..3047fdc4fdeb1ddbef97c481a5af371f830d5835 100644 (file)
@@ -40,6 +40,7 @@ caveats.  It's intended that usually, you'd re-associate detached objects with
 another :class:`.Session` when you want to work with them again, so that they
 can resume their normal task of representing database state.
 
+.. _session_basics:
 
 Basics of Using a Session
 =========================
@@ -464,6 +465,7 @@ so-called "subtransactions" is consistently maintained.
     :ref:`faq_session_rollback` - further background on why
     :meth:`_orm.Session.rollback` must be called when a flush fails.
 
+.. _session_expiring:
 
 Expiring / Refreshing
 ---------------------
index 9f7aafc1b2e5449e91a63ccb44ed3b76375c0dff..c8fac288e629319f24307d7f958b0f485c61f787 100644 (file)
@@ -156,6 +156,20 @@ The above :class:`_engine.Row` has just one element, representing the ``User`` e
     >>> row[0]
     User(id=1, name='spongebob', fullname='Spongebob Squarepants')
 
+A highly recommended convenience method of achieving the same result as above
+is to use the :meth:`_orm.Session.scalars` method to execute the statement
+directly; this method will return a :class:`_result.ScalarResult` object
+that delivers the first "column" of each row at once, in this case,
+instances of the ``User`` class::
+
+    >>> user = session.scalars(select(User)).first()
+    {opensql}SELECT user_account.id, user_account.name, user_account.fullname
+    FROM user_account
+    [...] (){stop}
+    >>> user
+    User(id=1, name='spongebob', fullname='Spongebob Squarepants')
+
+
 Alternatively, we can select individual columns of an ORM entity as distinct
 elements within result rows, by using the class-bound attributes; when these
 are passed to a construct such as :func:`_sql.select`, they are resolved into
index f4d2ad8e07efa404a2552f13b2a89c68ccd08359..da07de87cbafe86994d312e9ef8ac7a4c1936fc9 100644 (file)
@@ -525,6 +525,11 @@ than that, however understanding that it has a :meth:`_orm.Session.execute`
 method that's used the same way as :meth:`_future.Connection.execute` will
 get us started with the examples that follow later.
 
+.. seealso::
+
+    :ref:`session_basics` - presents basic creational and usage patterns with
+    the :class:`_orm.Session` object.
+
 
 
 
index bf4503fe65632123c8bd49336dc87b4ec269b016..5a0d8c8ac814563074376dcb4db959ae40a8a14f 100644 (file)
@@ -103,6 +103,9 @@ class DocTest(fixtures.TestBase):
     def test_orm_queryguide(self):
         self._run_doctest("orm/queryguide.rst")
 
+    def test_orm_quickstart(self):
+        self._run_doctest("orm/quickstart.rst")
+
 
 # unicode checker courtesy pytest