From 0b70dd51e6bd69863cb25138844f2fcb39ac84e3 Mon Sep 17 00:00:00 2001 From: Seongeun Yu <6926298+s3ich4n@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:21:45 +0900 Subject: [PATCH] =?UTF-8?q?[tutorial]=20=EC=98=81=EB=AC=B8=20=EB=B2=88?= =?UTF-8?q?=EC=97=AD=EC=9D=84=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4=20(#3?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hyeon Co-authored-by: heumsi --- src/.vuepress/config.js | 123 +- src/en/index.md | 8 + src/en/tutorial/1. Tutorial Overview.md | 42 + src/en/tutorial/2. Setting Up a Connection.md | 27 + .../3. Executing Transactions and Queries.md | 196 +++ .../4. Working with Database Metadata.md | 251 ++++ .../5.1. Querying Rows Using Core and ORM.md | 1081 +++++++++++++++++ .../5.2. Inserting Rows Using Core.md | 140 +++ ... Modifying and Deleting Rows Using Core.md | 194 +++ .../6. Manipulating Data Using ORM.md | 342 ++++++ ... Working with Related Objects Using ORM.md | 776 ++++++++++++ src/en/tutorial/README.md | 19 + src/en/tutorial/config.js | 120 ++ 13 files changed, 3279 insertions(+), 40 deletions(-) create mode 100644 src/en/index.md create mode 100644 src/en/tutorial/1. Tutorial Overview.md create mode 100644 src/en/tutorial/2. Setting Up a Connection.md create mode 100644 src/en/tutorial/3. Executing Transactions and Queries.md create mode 100644 src/en/tutorial/4. Working with Database Metadata.md create mode 100644 src/en/tutorial/5.1. Querying Rows Using Core and ORM.md create mode 100644 src/en/tutorial/5.2. Inserting Rows Using Core.md create mode 100644 src/en/tutorial/5.3. Modifying and Deleting Rows Using Core.md create mode 100644 src/en/tutorial/6. Manipulating Data Using ORM.md create mode 100644 src/en/tutorial/7. Working with Related Objects Using ORM.md create mode 100644 src/en/tutorial/README.md create mode 100644 src/en/tutorial/config.js diff --git a/src/.vuepress/config.js b/src/.vuepress/config.js index 98f8208..b3cdcb7 100755 --- a/src/.vuepress/config.js +++ b/src/.vuepress/config.js @@ -1,17 +1,22 @@ const { description } = require('../../package') module.exports = { - /** - * Ref:https://v1.vuepress.vuejs.org/config/#title - */ - title: '파이썬 개발자를 위한 SQLAlchemy', - /** - * Ref:https://v1.vuepress.vuejs.org/config/#description - */ - description: description, - base: "/sqlalchemy-for-pythonist/", + // Path + locales: { + '/': { + lang: 'ko', + title: '파이썬 개발자를 위한 SQLAlchemy', + description: description, + }, + '/en/': { + lang: 'en-US', + title: 'SQLAlchemy for Python Developers', + description: 'This is a document that simplifies SQLAlchemy for easy understanding.', + } + }, + /** * Extra tags to be injected to the page HTML `` * @@ -38,38 +43,76 @@ module.exports = { * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html */ themeConfig: { - repo: '', - editLinks: true, - docsDir: '', - editLinkText: '', - lastUpdated: true, - smoothScroll: true, - nav: [ - { - text: 'GitHub', - link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' + locales: { + '/': { + repo: '', + editLinks: true, + docsDir: '', + editLinkText: '', + lastUpdated: true, + smoothScroll: true, + nav: [ + { + text: 'GitHub', + link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' + }, + ], + sidebar: { + '/tutorial/': [ + { + title: 'Tutorial', + path: '/tutorial/', + collapsable: false, + children: [ + '1. 튜토리얼 개요', + '2. 연결 설정하기', + '3. 트랜잭션과 쿼리 실행하기', + '4. 데이터베이스 메타데이터로 작업하기', + '5.1. Core와 ORM 방식으로 행 조회하기', + '5.2. Core 방식으로 행 삽입하기', + '5.3. Core 방식으로 행 수정 및 삭제하기', + '6. ORM 방식으로 데이터 조작하기', + '7. ORM 방식으로 관련 개체 작업하기', + ] + }, + ] + } }, - ], - sidebar: { - '/tutorial/': [ - { - title: 'Tutorial', - path: '/tutorial/', - collapsable: false, - children: [ - '1. 튜토리얼 개요', - '2. 연결 설정하기', - '3. 트랜잭션과 쿼리 실행하기', - '4. 데이터베이스 메타데이터로 작업하기', - '5.1. Core와 ORM 방식으로 행 조회하기', - '5.2. Core 방식으로 행 삽입하기', - '5.3. Core 방식으로 행 수정 및 삭제하기', - '6. ORM 방식으로 데이터 조작하기', - '7. ORM 방식으로 관련 개체 작업하기', + '/en/': { + repo: '', + editLinks: true, + docsDir: '/en/', + editLinkText: '/en/', + lastUpdated: true, + smoothScroll: true, + nav: [ + { + text: 'GitHub', + link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' + }, + ], + sidebar: { + '/en/tutorial/': [ + { + title: 'Tutorial', + path: '/en/tutorial/', + collapsable: false, + children: [ + '1. Tutorial Overview', + '2. Setting Up a Connection', + '3. Executing Transactions and Queries', + '4. Working with Database Metadata', + '5.1. Querying Rows Using Core and ORM', + '5.2. Inserting Rows Using Core', + '5.3. Modifying and Deleting Rows Using Core', + '6. Manipulating Data Using ORM', + '7. Working with Related Objects Using ORM', + ] + }, ] - }, - ] - } + } + }, + }, }, /** @@ -80,5 +123,5 @@ module.exports = { '@vuepress/plugin-medium-zoom', ["sitemap", { hostname: "https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/" }], ["@vuepress/last-updated"], - ] + ], } diff --git a/src/en/index.md b/src/en/index.md new file mode 100644 index 0000000..12af522 --- /dev/null +++ b/src/en/index.md @@ -0,0 +1,8 @@ +--- +home: true +heroImage: https://media.vlpt.us/images/zamonia500/post/6dd8b08b-a089-49db-a2f1-921ad6a9649e/connect-a-flask-app-to-a-mysql-database-with-sqlalchemy-and-pymysql.jpg +tagline: This is a document that simplifies SQLAlchemy for easy understanding. +actionText: Tutorial → +actionLink: /en/tutorial/ +footer: Made by soogoonsoogoon pythonists ❤️ +--- diff --git a/src/en/tutorial/1. Tutorial Overview.md b/src/en/tutorial/1. Tutorial Overview.md new file mode 100644 index 0000000..f57a512 --- /dev/null +++ b/src/en/tutorial/1. Tutorial Overview.md @@ -0,0 +1,42 @@ +# Tutorial Overview + +
+ +## Overview + +SQLAlchemy is a library in Python that facilitates the connection to databases and the use of ORM (Object-Relational Mapping). +For instance, you can execute specific queries in your code and perform a series of operations in the database through ORM objects. + +
+ +## Installation + +SQLAlchemy can be installed as follows: + +```bash +$ pip install sqlalchemy +``` + +The version being used is as follows: + +```python +>>> import sqlalchemy +>>> sqlalchemy.__version__ +1.4.20 +``` + +
+ +## Offerings + +SQLAlchemy is offered in the following two ways: + +- **Core** + - This is the database toolkit and the foundational architecture of SQLAlchemy. + - It manages connections to databases, interacts with database queries and results, and provides tools to programmatically compose SQL statements. +- **ORM** + - Built on top of Core, it provides optional **ORM** (Object-Relational Mapping) features. + +It is generally recommended to understand Core first before using ORM. +This tutorial will start by explaining Core. + diff --git a/src/en/tutorial/2. Setting Up a Connection.md b/src/en/tutorial/2. Setting Up a Connection.md new file mode 100644 index 0000000..976c2d0 --- /dev/null +++ b/src/en/tutorial/2. Setting Up a Connection.md @@ -0,0 +1,27 @@ +# Setting up a Connection + +
+ +## Connecting to a Database + +Let's try connecting to SQLite, a relatively lightweight database. +You can do it as follows: + +```python +>>> from sqlalchemy import create_engine +>>> engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True) +``` + +- Use the `sqlalchemy.create_engine` function to create an **'engine'** that establishes a connection to the database. +- The first argument is a **`string URL`**. + - Typically, the `string URL` is structured as `dialect+driver://username:password@host:port/database`. + - If you don't specify a `driver`, SQLAlchemy's default settings will be used. + - Here, `sqlite+pysqlite:///test.db` is the `string URL`. + - For `sqlite`, the format follows `sqlite:///`. +- From the string URL `sqlite:///test.db`, we can understand the following information: + - **Which database** to use (`dialect`, in this case, `sqlite`) + - **Which database API** (the driver interacting with the database) to use (in this case, `pysqlite`) + - **How to find** the database (in this case, it uses the in-memory feature provided by `sqlite`) +- Setting the `echo` parameter to `True` prints all executed SQL. + +Creating an engine doesn't yet attempt an actual connection. The real connection occurs only when a request to perform an operation on the database is received for the first time. \ No newline at end of file diff --git a/src/en/tutorial/3. Executing Transactions and Queries.md b/src/en/tutorial/3. Executing Transactions and Queries.md new file mode 100644 index 0000000..9e8bcfa --- /dev/null +++ b/src/en/tutorial/3. Executing Transactions and Queries.md @@ -0,0 +1,196 @@ +# Executing Transactions and Queries + +
+ +## Obtaining connection + +You can connect to the database and execute a query as follows. + +```python +>>> from sqlalchemy import text + +>>> with engine.connect() as conn: +... result = conn.execute(text("select 'hello world'")) +... print(result.all()) + +[('hello world',)] +``` + +- Obtain a [`Connection`](https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Connection) object through `engine.connect()` and store it in `conn`. + - This `Connection` object allows you to interact with the database. + - The `with` statement becomes a single transaction unit. +- **Transactions are not committed automatically.** + - You have to invoke the `Connection.commit()` to commit changes. + +
+ +## Committing Changes + +Obtaining a connection, initiating a transaction, and interacting with the database **do not automatically commit** changes. + +To commit the change, you need to call `Connection.commit()` as follows. + +```python +>>> with engine.connect() as conn: +... # DDL - Creating the table +... conn.execute(text("CREATE TABLE some_table (x int, y int)")) +... # DML - Inserting data into the table +... conn.execute( +... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), +... [{"x": 1, "y": 1}, {"x": 2, "y": 4}] +... ) +... # TCL - Commiting changes. +... conn.commit() +``` + +When you run the code above, you'll see the following result below. + +```sql +BEGIN (implicit) +CREATE TABLE some_table (x int, y int) +[...] () + +INSERT INTO some_table (x, y) VALUES (?, ?) +[...] ((1, 1), (2, 4)) + +COMMIT +``` + +You can also automatically commit at the end of a transaction using **`Engine.begin()`** and `with` statement. + +```python +>>> with engine.begin() as conn: +... conn.execute( +... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), +... [{"x": 6, "y": 8}, {"x": 9, "y": 10}] +... ) +... # Transaction commits automatically when the execution is done. +``` + +Executing the code above will yield the following results. + +```sql +BEGIN (implicit) +INSERT INTO some_table (x, y) VALUES (?, ?) +[...] ((6, 8), (9, 10)) + +COMMIT +``` + +
+ +## Command Line Execution Basics + +You can execute queries and retrieve results as follows. + +```python +>>> with engine.connect() as conn: +... # conn.execute() initializes the result in an object named `result`. +... result = conn.execute(text("SELECT x, y FROM some_table")) +... for row in result: +... print(f"x: {row.x} y: {row.y}") + +x: 1 y: 1 +x: 2 y: 4 +x: 6 y: 8 +x: 9 y: 10 +``` + +- The [`Result`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Result) object is the object that **holds the "query result"** returned by `conn.execute()`. + - You can see what features it provides by clicking on the link. + - For instance, you can receive _a list_ of Row objects using `Result.all()`. + +> cf. Both `Result` and `Row` are objects provided by SQLAlchemy. + +You can access each row using the `Result` object as follows. + +```python +result = conn.execute(text("select x, y from some_table")) + +# Accessing the tuple. +for x, y in result: + # ... + +# Accessing the value by using integer index. +for row in result: + x = row[0] + +# Accessing the value by using a name of the property. +for row in result: + y = row.y + +# Accessing the value by using a mapping access. +for dict_row in result.mappings(): + x = dict_row['x'] + y = dict_row['y'] +``` + +
+ +## Passing parameters to your query + +You can pass a parameter to a query as follows. + +```python +>>> with engine.connect() as conn: +... result = conn.execute( +... text("SELECT x, y FROM some_table WHERE y > :y"), # Receive in colon format (`:`). +... {"y": 2} # Pass by `dict`. +... ) +... for row in result: +... print(f"x: {row.x} y: {row.y}") + +x: 2 y: 4 +x: 6 y: 8 +x: 9 y: 10 +``` + +You can also send multiple parameters like this. + +```python +>>> with engine.connect() as conn: +... conn.execute( +... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), +... [{"x": 11, "y": 12}, {"x": 13, "y": 14}] # Pass by `List[dict]`` +... ) +... conn.commit() +``` + +The above code executes the following query. + +```sql +INSERT INTO some_table (x, y) VALUES (?, ?) [...] ((11, 12), (13, 14)) +``` + +
+ +## Executing ORM by using `Session` + +From now on, let's execute the query using `Session` provided by the `ORM`, instead of the `Connection` object. +You can do it as follows: + +```python +>>> from sqlalchemy.orm import Session + +>>> stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6) + +>>> # Pass an instance of the Engine object to the Session object +>>> # to get an instance that can interact with the database. +>>> with Session(engine) as session: +... # Executing the query using Session.execute(). +... result = session.execute(stmt) +... for row in result: +... print(f"x: {row.x} y: {row.y}") +``` + +Like `Connection`, `Session` also **does not automatically commit** upon closing. To commit, you need to _explicitly call_ `Session.commit()` as follows: + +```python +>>> with Session(engine) as session: +... result = session.execute( +... text("UPDATE some_table SET y=:y WHERE x=:x"), +... [{"x": 9, "y":11}, {"x": 13, "y": 15}] +... ) +... # You have to call `commit()` explicitly. +... session.commit() +``` diff --git a/src/en/tutorial/4. Working with Database Metadata.md b/src/en/tutorial/4. Working with Database Metadata.md new file mode 100644 index 0000000..489b71b --- /dev/null +++ b/src/en/tutorial/4. Working with Database Metadata.md @@ -0,0 +1,251 @@ +# Working with Database Metadata + +
+ +SQLAlchemy Core and ORM were created to allow Python objects to be used like tables and columns in a database. These can be used as _database metadata_. + +> Metadata describes data that describes other data. Here, **metadata refers to configured tables, columns, constraints, and other object information**. + +
+ +## Creating a table object and add it to your metadata + +In relational databases, tables are created via queries, but in SQLAlchemy, tables can be created through Python objects. +To start with SQLAlchemy Expression Language, you need to create a `Table` object for the database table you want to use. + +```python +>>> from sqlalchemy import MetaData +>>> # An object that will hold the meta information for the tables. +>>> metadata = MetaData() +>>> +>>> from sqlalchemy import Table, Column, Integer, String +>>> user_table = Table( +... # The name of the table that will be stored in the database. +... 'user_account', +... metadata, +... +... # The columns that will go into this table. +... Column('id', Integer, primary_key=True), +... Column('name', String(30)), +... Column('fullname', String), +... ) +``` + +- You can create database tables using the `Table` object. +- Columns of the table are implemented using `Column`. + - By default, it defines like `Column(column name, data type)`. + +- After creating a `Table` instance, you can know the created column information as follows: + +```Python +>>> user_table.c.name +Column('name', String(length=30), table=) + +>>> user_table.c.keys() +['id', 'name', 'fullname'] +``` + +
+ +## Declaring Simple Constraints + +We saw the `Column('id', Integer, primary_key=True)` statement in the code that creates the user table above. This declares the id column as the primary key. + +The primary key is implicitly declared as a structure in the `PrimaryKeyConstraint` object. This can be confirmed as follows. + +```python +>>> user_table.primary_key +PrimaryKeyConstraint(Column('id', Integer(), table=, primary_key=True, nullable=False)) +``` + +Along with the primary key, foreign keys can also be declared as follows. + +```python +>>> from sqlalchemy import ForeignKey +>>> address_table = Table( +... "address", +... metadata, +... Column('id', Integer, primary_key=True), +... # Declaring Foreign Key as `ForeignKey` object. +... Column('user_id', ForeignKey('user_account.id'), nullable=False), +... Column('email_address', String, nullable=False) +... ) +``` + +- You can declare a foreign key column in the form of `ForeignKey('table_name.foreign_key')`. + - In this case, you can omit the data type of the `Column` object. The data type is automatically inferred by locating the column corresponding to the foreign key. +- You can also declare a `NOT NULL` constraint on a column by passing the `nullable=False` parameter and value. + +
+ +## Applying to your database + +We have declared database tables using SQLAlchemy so far. Now, let's make these declared tables actually get created in the database. + +Execute `metadata.create_all()` as follows. + +```python +>>> metadata.create_all(engine) + +# The above code creates all tables recorded in the metadata instance. +# As a result, it executes the following queries. + +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, + user_id INTEGER NOT NULL, + email_address VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(user_id) REFERENCES user_account (id) +) +... +COMMIT +``` + +
+ +## Defining table metadata the ORM way + +We will create the same database structure and use the same constraints as above, but this time we will proceed using the ORM approach. + +
+ +### Setting the `registry` object. + +First of all, create a `registry` object as follows. + +```python +>>> from sqlalchemy.orm import registry +>>> mapper_registry = registry() +``` + +The `registry` object contains a `MetaData` object. + +```python +>>> mapper_registry.metadata +MetaData() +``` + +Now we can execute the following code. + +```python +>>> Base = mapper_registry.generate_base() +``` + +> The above process can be simplified using `declarative_base` as follows. +> +> ```python +> >>> from sqlalchemy.orm import declarative_base +> >>> Base = declarative_base() +> ``` + +
+ +### Declaring the ORM object + +By defining a subclass that inherits from the `Base` object, you can declare tables in the database using the ORM approach. + +```python +>>> from sqlalchemy.orm import relationship +>>> class User(Base): +... # A name of the table to be used in the database. +... __tablename__ = 'user_account' +... +... id = Column(Integer, primary_key=True) +... name = Column(String(30)) +... fullname = Column(String) +... +... addresses = relationship("Address", back_populates="user") +... +... 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')) +... +... user = relationship("User", back_populates="addresses") +... +... def __repr__(self): +... return f"Address(id={self.id!r}, email_address={self.email_address!r})" +``` + +The `User` and `Address` objects include a `Table` object. + +You can check this through the `__table__` attribute as follows. + +```python +>>> User.__table__ +Table('user_account', MetaData(), + Column('id', Integer(), table=, primary_key=True, nullable=False), + Column('name', String(length=30), table=), + Column('fullname', String(), table=), schema=None) +``` + +
+ +### Creating an ORM object + +After defining the table, you can create an ORM object as follows. + +```python +>>> sandy = User(name="sandy", fullname="Sandy Cheeks") +>>> sandy +User(id=None, name='sandy', fullname='Sandy Cheeks') +``` + +
+ +### Applying to your database + +Now, you can apply the tables declared with ORM to the actual database as follows. + +```python +>>> mapper_registry.metadata.create_all(engine) +>>> Base.metadata.create_all(engine) +``` + +
+ +## Importing tables from an existing database into an ORM object + +Aside from the above methods, there is a way to retrieve tables from the database without declaring them directly. + +```python +>>> some_table = Table("some_table", metadata, autoload_with=engine) + +BEGIN (implicit) +PRAGMA main.table_...info("some_table") +[raw sql] () +SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = ? AND type = 'table' +[raw sql] ('some_table',) +PRAGMA main.foreign_key_list("some_table") +... +PRAGMA main.index_list("some_table") +... +ROLLBACK +``` + +Now it can be used as follows: + +```python +>>> some_table +Table('some_table', MetaData(), + Column('x', INTEGER(), table=), + Column('y', INTEGER(), table=), + schema=None) +``` diff --git a/src/en/tutorial/5.1. Querying Rows Using Core and ORM.md b/src/en/tutorial/5.1. Querying Rows Using Core and ORM.md new file mode 100644 index 0000000..a63505e --- /dev/null +++ b/src/en/tutorial/5.1. Querying Rows Using Core and ORM.md @@ -0,0 +1,1081 @@ +# Querying Rows Using `Core` and `ORM` + +
+ +This chapter covers the most frequently used `Select` in SQLAlchemy. + +
+ +## Constructing SQL Expressions with `select()` + +The `select()` constructor allows you to create query statements in the same way as the `insert()` constructor. + +```python +>>> from sqlalchemy import select +>>> stmt = select(user_table).where(user_table.c.name == 'spongebob') +>>> print(stmt) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.name = :name_1 +""" +``` + +Similarly, you can put a query in the `Connection.execute()` method to execute a query statement, just like any SQL constructor at the same level (`select`, `insert`, `update`,`create` and etc.). + +```python +>>> with engine.connect() as conn: +... for row in conn.execute(stmt): +... print(row) +(1, 'spongebob', 'Spongebob Squarepants') +``` + +On the other hand, if you want to use the ORM to execute a `select` query statement, you should use `Session.exeuct()`. + +The result returns a `Row` object, just like in the example just now. This object contains the `User` object that we defined in the [previous tutorial](https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/en/tutorial/4.%20Working%20with%20Database%20Metadata.html#create-a-table-object-and-add-it-to-your-metadata). + +```python +>>> stmt = select(User).where(User.name == 'spongebob') +>>> with Session(engine) as session: +... for row in session.execute(stmt): +... # Print each row in an instance of the User object +... print(row) +(User(id=1, name='spongebob', fullname='Spongebob Squarepants'),) +``` + +
+ +## Setting up the `FROM` clause and columns + +The `select()` function can take a variety of objects as positional arguments, including `Column` and `Table`. + +These argument values can be represented as the return value of the `select()` function, i.e., as an SQL query statement, and can also set the `FROM` clause. + + +```python +>>> print(select(user_table)) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +""" +``` + +To retrieve each column using the `Core`, you can access the `Column` object through the `Table.c` accessor. + +```python +>>> print(select(user_table.c.name, user_table.c.fullname)) +""" +SELECT user_account.name, user_account.fullname +FROM user_account +""" +``` + +
+ + +### ORM entity and column lookups + +When implementing SQL queries in SQLAlchemy, you can use ORM entities like the `User` object or attributes that map to columns, such as `User.name`, to represent tables or columns. The example below queries the `User` entity, but in fact, the result is the same as when using `user_table`. + +The example below looks up the `User` entity, but the results are the same as when using `user_table`. + +```python +>>> print(select(User)) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +""" +``` + +In the above example, the query can be executed in the same way using ORM's `Session.execute()`. + +However, there is a difference between querying the `User` entity and querying `user_info`. Whether you query `user_info` or the `User` entity, in both cases a Row object is returned. + +But, when querying the `User` entity, the returned `Row` object includes a `User` instance. + + +> Tips: +> +> The `user_table` and `User` were created in the [previous chapter](https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/en/tutorial/4.%20Working%20with%20Database%20Metadata.html), +> where `user_table` is a `Table` object, and +> `User` is an entity that inherits from the `Base` object and includes a `Table` object." + +```python +>>> with Session(engine) as session: +... row = session.execute(select(User)).first() +... print(row) +(User(id=1, name='spongebob',fullname='Spongebob Squarepants'),) +``` + +Alternatively, you can query the desired columns using object attributes(class-bound attributes). + +```python +>>> print(select(User.name, User.fullname)) +""" +SELECT user_account.name, user_account.fullname +FROM user_account +""" +``` + +When querying object attributes using `Session.execute()`, the values of the object attributes sent as arguments (column values) are returned as follows. + +```python +>>> with Session(engine) as session: +... row = session.execute(select(User.name, User.fullname)).first() +... print(row) +('spongebob', 'Spongebob Squarepants') +``` + +These methods can also be mixed and used together, as shown in the following example + +```python +>>> session.execute( +... select(User.name, Address). +... where(User.id==Address.user_id). +... order_by(Address.id) +... ).all() +[('spongebob', Address(id=1, email_address='spongebob@sqlalchemy.org')), +('sandy', Address(id=2, email_address='sandy@sqlalchemy.org')), +('sandy', Address(id=3, email_address='sandy@squirrelpower.org'))] +``` + +
+ +### Querying Labeled SQL Expressions + +When you execute a query like SELECT name AS username FROM user_account, you can get the following results: + +|username| +|------| +|patrick| +|sandy| +|spongebob| + +Here, we've labeled the `name` column as `username`, which is why `username` appears as the column header. This functionality can be implemented in SQLAlchemy using the `ColumnElement.label()` function, as shown below: + +```python +>>> from sqlalchemy import func, cast +>>> stmt = ( +... select( +... # Labeling is done like this. +... ("Username: " + user_table.c.name).label("username"), +... ).order_by(user_table.c.name) +... ) +>>> with engine.connect() as conn: +... for row in conn.execute(stmt): +... # The labeled part can be accessed like this. +... print(f"{row.username}") +Username: patrick +Username: sandy +Username: spongebob +``` +
+ +### Querying String Columns + +Usually, columns are queried using the `Select` object or the `select()` constructor, but sometimes you need to query a column along with an arbitrary string. This section covers how to query such string data. + +The `text()` constructor was introduced in a [previous chapter 3](https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/en/tutorial/3.%20Executing%20Transactions%20and%20Queries.html). Transactions and Database API Operations. It allows you to directly use a `SELECT` statement within it. + + +Let's consider a scenario where we want to execute a query like `SELECT 'some_phrase', name FROM user_account`. In this case, since some_phrase is a string, it must be enclosed in either single or double quotes. Consequently, the output will inevitably have single quotes around the string. + +```python +>>> from sqlalchemy import text +>>> stmt = ( +... select( +... text("'some phrase'"), user_table.c.name +... ).order_by(user_table.c.name) +... ) +>>> with engine.connect() as conn: +... print(conn.execute(stmt).all()) +[('some phrase', 'patrick'), ('some phrase', 'sandy'), ('some phrase', 'spongebob')] +``` +Therefore, instead of `text()`, it is common to use `literal_column()` to solve the problem of having single quotes attached to the output. `text()` and `literal_column()` are almost similar, but `literal_column()` explicitly signifies a column and can be labeled for use in subqueries and other SQL expressions." + +```python +>>> from sqlalchemy import literal_column +>>> stmt = ( +... select( +... literal_column("'some phrase'").label("p"), user_table.c.name +... ).order_by(user_table.c.name) +... ) +>>> with engine.connect() as conn: +... for row in conn.execute(stmt): +... print(f"{row.p}, {row.name}") +some phrase, patrick +some phrase, sandy +some phrase, spongebob + +``` + +
+ +## `WHERE` Clauses + +Using SQLAlchemy, you can easily write queries to output data where conditions like `name = 'thead'` or `user_id > 10 ` are met using Python operators. + +```python +>>> print(user_table.c.name == 'squidward') +user_account.name = :name_1 + +>>> print(address_table.c.user_id > 10) +address.user_id > :user_id_1 +``` + +To create a `WHERE` clause, you can pass arguments to the `Select.where()` method. + +```python +>>> print(select(user_table).where(user_table.c.name == 'squidward')) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.name = :name_1 +""" +``` + + +When implementing a `JOIN` with a `WHERE` clause, it can be written as follows. + +```python +>>> print( +... select(address_table.c.email_address). +... where(user_table.c.name == 'squidward'). +... where(address_table.c.user_id == user_table.c.id) +... ) +""" +SELECT address.email_address +FROM address, user_account +WHERE user_account.name = :name_1 AND address.user_id = user_account.id +""" + +# The same expression can be used, but you can also pass parameters to the where() method. +>>> print( + select(address_table.c.email_address). +... where( +... user_table.c.name == 'squidward', +... address_table.c.user_id == user_table.c.id +... ) +... ) +""" +SELECT address.email_address +FROM address, user_account +WHERE user_account.name = :name_1 AND address.user_id = user_account.id +""" +``` + +It's also possible to use conjunctions such as `and_()` and `or_()`. + +```python +>>> from sqlalchemy import and_, or_ +>>> print( +... select(Address.email_address). +... where( +... and_( +... or_(User.name == 'squidward', User.name == 'sandy'), +... Address.user_id == User.id +... ) +... ) +... ) +""" +SELECT address.email_address +FROM address, user_account +WHERE (user_account.name = :name_1 OR user_account.name = :name_2) +AND address.user_id = user_account.id +""" +``` + +For simple equality or inequality comparisons, `Select.filter_by()` is often used. +```python +>>> print( +... select(User).filter_by(name='spongebob', fullname='Spongebob Squarepants') +... ) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.name = :name_1 AND user_account.fullname = :fullname_1 +""" +``` + +
+ +## Specifying the `FROM` Clause and `JOIN`s + +As mentioned before, the `FROM` clause is automatically set based on the columns included as arguments in the `select()` method, without the need for explicit specification. +```python +# Even without specifying the FROM clause explicitly, it is set and displayed in the output. +>>> print(select(user_table.c.name)) +""" +SELECT user_account.name +FROM user_account +""" +``` + +If you want to reference columns from two different tables in the positional arguments of `select()`, you can separate them with a comma (`,`). +```python +>>> print(select(user_table.c.name, address_table.c.email_address)) +""" +SELECT user_account.name, address.email_address +FROM user_account, address +""" +``` + +If you want to join two different tables, there are two methods you can use: + +One is the `Select.join()` method, which allows you to explicitly specify the left and right tables for the `JOIN`. +```python +>>> print( +... select(user_table.c.name, address_table.c.email_address). +... join_from(user_table, address_table) +... ) +""" +SELECT user_account.name, address.email_address +FROM user_account JOIN address ON user_account.id = address.user_id +""" +``` + +The other is to explicitly specify only the right table in the `Select.join()` method, and let the other table be implicitly referenced when selecting columns. +```python +# This expression is the same, but the left table to join (user_table) is expressed implicitly. +>>> print( +... select(user_table.c.name, address_table.c.email_address). +... join(address_table) +... ) +""" +SELECT user_account.name, address.email_address +FROM user_account JOIN address ON user_account.id = address.user_id +""" +``` + +Alternatively, if you want to write the two JOINing tables more explicitly, or if you want to provide explicit additional options in the `FROM` clause, you can write it as follows. +```python +>>> print( +... select(address_table.c.email_address). +... select_from(user_table).join(address_table) +... ) +""" +SELECT address.email_address +FROM user_account JOIN address ON user_account.id = address.user_id +""" +``` + +Another case for using `Select.select_from()` is when we cannot implicitly set the `FROM` clause through the columns we want to query. + +For example, to query `count(*)` in a typical SQL query, you would need to use `sqlalchemy.sql.expression.func` from SQLAlchemy. + + +```python +>>> from sqlalchemy import func +>>> print(select(func.count('*')).select_from(user_table)) +""" +SELECT count(:count_2) AS count_1 +FROM user_account +""" +``` + +
+ +### Setting the `ON` Clause + +But there was something unusual, wasn't there? +In fact, in the previous example, when joining two tables using `Select.select_from()` or `select.join()`, the `ON` clause was implicitly set. + +This automatic setting of the `ON` clause happened because the `user_table` and `address_table` objects have a ForeignKeyConstraint, i.e., a foreign key constraint, which led to the automatic setting. + +If the two tables targeted for a Join lack such constraint keys, you must explicitly specify the `ON` clause. This functionality can be explicitly set by passing parameters to the `Select.join()` or `Select.join_from()` methods for the `ON` clause. + +```python +>>> print( +... select(address_table.c.email_address). +... select_from(user_table). +... join(address_table, user_table.c.id == address_table.c.user_id) +... ) +""" +SELECT address.email_address +FROM user_account JOIN address ON user_account.id = address.user_id +""" +``` +
+ + +### OUTER, FULL Join + +To implement `LEFT OUTER JOIN` or `FULL OUTER JOIN` In SQLAlchemy, you can use the keyword arguments `Select.join.isouter` and `Select.join.full` in the `Select.join()` and `Select.join_from()` methods. + +An examples of implementing the `LEFT OUTER JOIN`: +```python +>>> print( +... select(user_table).join(address_table, isouter=True) +... ) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account LEFT OUTER JOIN address ON user_account.id = address.user_id +""" +``` + +An examples of implementing the `FULL OUTER JOIN`: +```python +>>> print( +... select(user_table).join(address_table, full=True) +... ) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account FULL OUTER JOIN address ON user_account.id = address.user_id +""" +``` + + +
+ + +## ORDER BY, GROUP BY, HAVING + +- The `ORDER BY` clause allows you to set the order of the rows retrieved by the `SELECT` clause. +- The `GROUP BY` clause creates groups based on a specific column for rows aggregated by group functions. +- `HAVING` applies conditions to groups created by the `GROUP BY` clause. + +
+ +### ORDER BY + +You can implement the ORDER BY feature using `Select.order_by()`. This method accepts Column objects or similar objects as positional arguments. + +```python +>>> print(select(user_table).order_by(user_table.c.name)) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account ORDER BY user_account.name +""" +``` +Ascending and descending order can be implemented using the `ColumnElement.asc()` and `ColumnElement.desc()` modifiers, respectively. + +The following example orders by the `user_account.fullname` column in descending order. +```python +>>> print(select(User).order_by(User.fullname.desc())) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account ORDER BY user_account.fullname DESC +""" +``` +
+ +### Aggregations: GROUP BY, HAVING + +In SQL, aggregate functions can also be used to combine multiple rows into a single row. Examples of aggregate functions include `COUNT()`, `SUM()`, and `AVG()`. + +SQLAlchemy provides SQL functions using the func namespace, where `func` creates a `Function` instance when given the name of an SQL function. + +In the example below, the `count()` function is called to render the `user_account.id` column with the SQL `COUNT()` function. + +```python +>>> from sqlalchemy import func +>>> count_fn = func.count(user_table.c.id) +>>> print(count_fn) +""" +count(user_account.id) +""" +``` + +More details about SQL functions are explained in [Handling SQL Functions](). + +To summarize: + +`GROUP BY` is a function needed to divide the retrieved rows into specific groups. In SQL, if a few columns are queried in the `SELECT` clause, these columns are directly or indirectly dependent on the primary key in the `GROUP BY`. + +`HAVING` is necessary to apply conditions to groups created by `GROUP BY` (similar to the `WHERE` clause because it places conditions on groups). + +In SQLAlchemy, `GROUP BY` and `HAVING` can be implemented using `Select.group_by()` and `Select.having()`. + +```python +>>> with engine.connect() as conn: +... result = conn.execute( +... select(User.name, func.count(Address.id).label("count")). +... join(Address). +... group_by(User.name). +... having(func.count(Address.id) > 1) +... ) +... print(result.all()) +""" The syntax above represents the SQL statement below. +SELECT user_account.name, count(address.id) AS count +FROM user_account JOIN address ON user_account.id = address.user_id GROUP BY user_account.name +HAVING count(address.id) > ? +[...] (1,) +""" + +[('sandy', 2)] +``` +
+ + +### Grouping or ordering by alias + +In some database backends, when using aggregate functions to query tables, it is important not to **restate** already specified aggregate functions in the `ORDER BY` or `GROUP BY` clauses. + +```sql +# NOT GOOD +SELECT id, COUNT(id) FROM user_account GROUP BY id ORDER BY count(id) + +# CORRECT +SELECT id, COUNT(id) as cnt_id FROM user_account GROUP BY id ORDER BY cnt_id +``` + +Therefore, to implement `ORDER BY` or `GROUP BY` using aliases, you just need to insert the alias you want to use as an argument in the `Select.order_by()` or `Select.group_by()` methods. + +The alias used here is not rendered first; instead, the alias used in the column clause is rendered first. If the rendered alias does not match anything in the rest of the query, an error occurs. + + +```python +>>> from sqlalchemy import func, desc +>>> # The alias 'num_addresses' is used in both the column and in the order_by clause. +>>> stmt = select( +... Address.user_id, +... func.count(Address.id).label('num_addresses')).\ +... group_by("user_id").order_by("user_id", desc("num_addresses")) +>>> print(stmt) +""" +SELECT address.user_id, count(address.id) AS num_addresses +FROM address GROUP BY address.user_id ORDER BY address.user_id, num_addresses DESC +""" +``` +
+ + +## Using aliases + +When using `JOIN` to query multiple tables, it's often necessary to repeatedly write the table names in the query. + +In SQL, this issue can be addressed by giving _aliases_ to table names or subqueries, reducing repetition. + +In SQLAlchemy, such aliases can be implemented using the Core's `FromClause.alias()` function. + +Within the `Table` object namespace, there are `Column` objects, allowing access to column names via `Table.c`. + +```python +print(select(user_table.c.name, user_table.c.fullname)) +""" +SELECT user_account.name, user_account.fullname +FROM user_account +""" +``` + +Similarly, in the `Alias` object namespace, there are `Column` objects, making it possible to access columns via `Alias.c`. + +```python +>>> # Both user_alias_1 and user_alias_2 are Alias objects. +>>> user_alias_1 = user_table.alias(‘table1’) +>>> user_alias_2 = user_table.alias(‘table2’) +>>> # To access columns using the newly created table aliases, +>>> # you should use Alias.c.column_name +>>> print( +... select(user_alias_1.c.name, user_alias_2.c.name). +... join_from(user_alias_1, user_alias_2, user_alias_1.c.id > user_alias_2.c.id) +... ) + +""" +SELECT table1.name, table2.name AS name_1 +FROM user_account AS table1 JOIN user_account AS table2 ON table1.id > table2.id +""" +``` + +
+ +### ORM Entity Aliases + +The ORM in SQLAlchemy also has a function similar to the `FromClause.alias()` method, known as `aliased()` + +This ORM `aliased()` function internally creates an `Alias` object for the originally mapped `Table` object, while maintaining ORM functionalities. + +> Tips: +> +> The `user_table` and `User` were created in the [previous chapter](https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/en/tutorial/4.%20Working%20with%20Database%20Metadata.html), +> where `user_table` is a `Table` object, and +> `User` is an entity that inherits from the `Base` object and includes a `Table` object." + + +```python +>>> user_alias_1 = user_table.alias() +>>> user_alias_2 = user_table.alias() +>>> # In the examples, it is applied to the User or Address entities. +>>> print( +... select(User). +... join_from(User, address_alias_1). +... where(address_alias_1.email_address == 'patrick@aol.com'). +... join_from(User, address_alias_2). +... where(address_alias_2.email_address == 'patrick@gmail.com') +... ) +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account JOIN address AS address_1 ON user_account.id = address_1.user_id JOIN address AS address_2 ON user +""" +``` + +
+ +## Subqueries and CTE(Common Table Expression)s + +This section explains subqueries typically found in the `FROM` clause of a `SELECT` statement. It also covers CTEs (Common Table Expressions), which are used in a similar way to subqueries but with additional functionalities. + +> More about CTE +> +> A CTE is a temporary result set within a query that can be referenced multiple times within the same query. +> You can check the official links to describe CTEs in RDBMS below. +> +> - [PostgreSQL: WITH Queries (Common Table Expressions)](https://www.postgresql.org/docs/current/queries-with.html) +> - [MySQL: WITH (Common Table Expressions)](https://dev.mysql.com/doc/refman/8.0/en/with.html) +> - [MariaDB: WITH](https://mariadb.com/kb/en/with/) + + +SQLAlchemy represents subqueries using the `Subquery` object created by `Select.subquery()`, and CTEs are represented using `Select.cte()`. + +```python +>>> subq = select( +... func.count(address_table.c.id).label("count"), +... address_table.c.user_id +... ).group_by(address_table.c.user_id).subquery() +>>> print(subq) +""" +SELECT count(address.id) AS count, address.user_id +FROM address GROUP BY address.user_id +""" + +>>> # The ON clause automatically binds two tables +>>> # that are already constrained by a foreign key. +>>> stmt = select( +... user_table.c.name, +... user_table.c.fullname, +... subq.c.count +... ).join_from(user_table, subq) +>>> print(stmt) +""" +SELECT user_account.name, user_account.fullname, anon_1.count +FROM user_account JOIN (SELECT count(address.id) AS count, address.user_id AS user_id +FROM address GROUP BY address.user_id) AS anon_1 ON user_account.id = anon_1.user_id +""" +``` +
+ +### Hierarchy Query + +The method of using CTE syntax in SQLAlchemy is almost identical to how subquery syntax is used. Instead of calling the `Select.subquery()` method, you use `Select.cte()`, allowing the resulting object to be used as a FROM element. + +```python +>>> subq = select( +... func.count(address_table.c.id).label("count"), +... address_table.c.user_id +... ).group_by(address_table.c.user_id).cte() + +>>> stmt = select( +... user_table.c.name, +... user_table.c.fullname, +... subq.c.count +... ).join_from(user_table, subq) + +>>> print(stmt) +""" +WITH anon_1 AS +(SELECT count(address.id) AS count, address.user_id AS user_id +FROM address GROUP BY address.user_id) + SELECT user_account.name, user_account.fullname, anon_1.count +FROM user_account JOIN anon_1 ON user_account.id = anon_1.user_id +""" +``` + +
+ + +### ORM Entity Subqueries, CTE + +You can see that `aliased()` performs the same operation for `Subquery` and `CTE` subqueries. + +```python +>>> subq = select(Address).where(~Address.email_address.like('%@aol.com')).subquery() +>>> address_subq = aliased(Address, subq) +>>> stmt = select(User, address_subq).join_from(User, address_subq).order_by(User.id, address_subq.id) +>>> with Session(engine) as session: +... for user, address in session.execute(stmt): +... print(f"{user} {address}") + +""" The above syntax represents the following query: + +SELECT user_account.id, user_account.name, user_account.fullname, +anon_1.id AS id_1, anon_1.email_address, anon_1.user_id +FROM user_account JOIN +(SELECT address.id AS id, address.email_address AS email_address, address.user_id AS user_id +FROM address +WHERE address.email_address NOT LIKE ?) AS anon_1 ON user_account.id = anon_1.user_id +ORDER BY user_account.id, anon_1.id +[...] ('%@aol.com',) +""" + +User(id=1, name='spongebob', fullname='Spongebob Squarepants') Address(id=1, email_address='spongebob@sqlalchemy.org') +User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=2, email_address='sandy@sqlalchemy.org') +User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='sandy@squirrelpower.org') +``` + +Below is an example of querying the same result using the CTE constructor: + +```python +>>> cte = select(Address).where(~Address.email_address.like('%@aol.com')).cte() +>>> address_cte = aliased(Address, cte) +>>> stmt = select(User, address_cte).join_from(User, address_cte).order_by(User.id, address_cte.id) +>>> with Session(engine) as session: +... for user, address in session.execute(stmt): +... print(f"{user} {address}") + +User(id=1, name='spongebob', fullname='Spongebob Squarepants') Address(id=1, email_address='spongebob@sqlalchemy.org') +User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=2, email_address='sandy@sqlalchemy.org') +User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='sandy@squirrelpower.org') +``` +
+ + + +## Scalar Subqueries and Correlated Queries + +Before explaining scalar subqueries, let's briefly discuss subqueries in SQL. [출처:바이헨 블로그](https://rinuas.tistory.com/entry/%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%ACSub-Query) + +A "_subquery_" is a `SELECT` statement within another SQL statement, and the outer SQL statement is referred to as the "_main query_". + +The types of subqueries are determined based on whether they reference columns of the main query, where they are declared, and the number of rows they return. + +- Classification based on reference to main query columns: + - **Correlated Subqueries**: The subquery references columns of the main query. + - **Non-correlated Subqueries**: The subquery does not reference the main query's columns and operates independently, used to convey information to the main query. +- Classification based on declaration position: + - **Scalar Subqueries**: Subqueries that appear in the column position of a SELECT statement (correlated). + - **Inline Views**: Subqueries in the FROM clause (correlated). + - **Nested Subqueries**: Subqueries in the WHERE clause (non-correlated). +- Classification based on the number of rows returned: + - **Single-row Subqueries** (return one row) + - **Multi-row Subqueries** (return more than one row): Used with IN, ANY, ALL, EXISTS. + +
+ +In SQLAlchemy, scalar subqueries use `ScalarSelect`, which is part of the `ColumnElement` object, while general subqueries use `Subquery`, which is in the `FromClause` object. + +Scalar subqueries are often used as described earlier in [Aggregations](#aggregations-group-by-having). + +```python +# Implementing Scalar subquery using Select.scalar_subquery() +>>> subq = select(func.count(address_table.c.id)). +... where(user_table.c.id == address_table.c.user_id). +... scalar_subquery() +>>> print(subq) # ... is equal to "ScalarSelect" type +""" +(SELECT count(address.id) AS count_1 +FROM address, user_account +WHERE user_account.id = address.user_id) +""" +``` +Scalar subqueries implemented using `Select.scalar_subquery()` render the `user_account` and `address` in the FROM clause, but since the `user_account` table is already present in the main query, it is not rendered again in the scalar subquery. + +```python +>>> stmt = select(user_table.c.name, subq.label("address_count")) +>>> print(stmt) +""" +SELECT user_account.name, (SELECT count(address.id) AS count_1 +FROM address +WHERE user_account.id = address.user_id) AS address_count +FROM user_account +""" +``` +Meanwhile, when writing correlated queries, the connections between tables can become ambiguous. + +> I did not understand the correlated query example in the tutorial. +> If someone understands it well, please contribute to this document. + +
+ + +## UNION, UNION ALL operators + +In SQL, terms like `UNION` and `UNION ALL` are used to combine two `SELECT` statements. +Queries can be executed as shown below. +```sql +SELECT id FROM user_account +union +SELECT email_address FROM address +``` + +Additionally, SQL supports set operations like `INTERSECT` (intersection) and `EXCEPT` (difference). +In SQLAlchemy, for Select objects, functions such as `union()`, `intersect()`, `except_()`, `union_all()`, `intersect_all()`, and `except_all()` are available. + +The return value of these functions is a `CompoundSelect`, which is an object that can be used similarly to `Select` but has fewer methods. +The `CompoundSelect` object returned by `union_all()` can be executed with `Connection.execute()`. + +```python +>>> from sqlalchemy import union_all +>>> stmt1 = select(user_table).where(user_table.c.name == 'sandy') +>>> stmt2 = select(user_table).where(user_table.c.name == 'spongebob') +>>> u = union_all(stmt1, stmt2) # A value u is a CompoundSelect type. +>>> with engine.connect() as conn: +... result = conn.execute(u) +... print(result.all()) + +[(2, 'sandy', 'Sandy Cheeks'), (1, 'spongebob', 'Spongebob Squarepants')] +``` + +Just as `Select` provides the `SelectBase.subquery()` method to create `Subquery` objects, `CompoundSelect` objects can similarly be used as subqueries. + +```python +>>> u_subq = u.subquery() +>>> stmt = ( +... select(u_subq.c.name, address_table.c.email_address). +... join_from(address_table, u_subq). +... order_by(u_subq.c.name, address_table.c.email_address) +... ) +>>> with engine.connect() as conn: +... result = conn.execute(stmt) +... print(result.all()) + +[('sandy', 'sandy@sqlalchemy.org'), ('sandy','sandy@squirrelpower.org'), +('spongebob', 'spongebob@sqlalchemy.org')] +``` + +
+ +## EXISTS Subqueries + +SQLAlchemy creates an `Exists` object through the `SelectBase.exists()` method to implement the `EXISTS` clause. + +```python +>>> # subq is a Exists type +>>> subq = ( +... select(func.count(address_table.c.id)). +... where(user_table.c.id == address_table.c.user_id). +... group_by(address_table.c.user_id). +... having(func.count(address_table.c.id) > 1) +... ).exists() +>>> print(subq) +""" +EXISTS (SELECT count(address.id) AS count_1 +FROM address, user_account +WHERE user_account.id = address.user_id GROUP BY address.user_id +HAVING count(address.id) > :count_2) +""" +>>> with engine.connect() as conn: +... result = conn.execute( +... select(user_table.c.name).where(subq) +... ) +... print(result.all()) + +[('sandy',)] +``` + +The `EXISTS` clause is more often used in a non-negated form by the way. + +```python +# This is a query to select usernames that do not have an email address. +# Take a look at the part where the '~' operator is used." +>>> subq = ( +... select(address_table.c.id). +... where(user_table.c.id == address_table.c.user_id) +... ).exists() +>>> stmt = select(user_table.c.name).where(~subq) +>>> print(stmt) +""" +SELECT user_account.id +FROM user_account +WHERE NOT (EXISTS (SELECT count(address.id) AS count_1 +FROM address +WHERE user_account.id = address.user_id GROUP BY address.user_id +HAVING count(address.id) > :count_2)) +""" +>>> with engine.connect() as conn: +... result = conn.execute(stmt) +... print(result.all()) + +[('patrick',)] +``` + +
+ + +## Dealing with SQL functions. + +In the earlier section [Aggregations: GROUP BY, HAVING](#aggregations-group-by-having), the `func` object, which acts as a factory for creating new `Function` objects, was introduced. When using syntax like `select()`, you can pass SQL functions created by the `func` object as arguments. + +- `count()` : Aggregate functions are used to print the number of rows. + ```python + >>> # cnt is a type of . + >>> cnt = func.count() + >>> print(select(cnt).select_from(user_table)) + """ + SELECT count(*) AS count_1FROM user_account + """ + ``` +- `lower()` : String functions are used to convert strings to lowercase. + ```python + >>> print(select(func.lower("A String With Much UPPERCASE"))) + """ + SELECT lower(:lower_2) AS lower_1 + """ + ``` +- `now()` : There is also a function that returns the current time and date. This function is commonly used, so SQLAlchemy helps in easily rendering it across different backends. + ```` python + >>> stmt = select(func.now()) + >>> with engine.connect() as conn: + ... result = conn.execute(stmt) + ... print(result.all()) + + [(datetime.datetime(...),)] + ```` + +Different database backends have SQL functions with different names. Therefore, `func` allows access to any name in its namespace, automatically interpreting that name as an SQL function and rendering it. + +```python +>>> # A data type of crazy_function is Function. +>>> crazy_function = func.some_crazy_function(user_table.c.fullname, 17) +>>> print(select(crazy_function)) +""" +SELECT some_crazy_function(user_account.name, :some_crazy_function_2) AS some_crazy_function_1 +FROM user_account +""" +``` + +Meanwhile, SQLAlchemy provides appropriate data types for commonly used SQL functions like `count`, `now`, `max`, `concat`, etc., specific to each backend. + +```python +>>> from sqlalchemy.dialects import postgresql +>>> print(select(func.now()).compile(dialect=postgresql.dialect())) +""" +SELECT now() AS now_1 +""" + +>>> from sqlalchemy.dialects import oracle +>>> print(select(func.now()).compile(dialect=oracle.dialect())) +""" +SELECT CURRENT_TIMESTAMP AS now_1 FROM DUAL +""" + ``` + +
+ +### Functions Have Return Types +> I did not understand the part about 'Functions Have Return Types' in the original text. +> If anyone understands this, please contribute to this section. Thank you. + +
+ +### Built-in Functions Have Pre-Configured Return Types +> I did not understand the part about 'Built-in Functions Have Pre-Configured Return Types' in the original text. +> If anyone understands this, please contribute to this section. Thank you. + +
+ +### WINDOW Functions + +Window functions are similar to `GROUP BY`, created to easily define relationships between rows. + +In SQLAlchemy, among all SQL functions created by the `func` namespace, there is the `FunctionElement.over()` method, which implements the `OVER` clause. + +One of the window functions is `row_number()`, which counts the number of rows. You can group each row by username and then number the email addresses within each group. + +```python +# The FunctionElement.over.partition_by parameter is used +# to render the PARTITION BY clause in the OVER clause. +>>> stmt = select( +... func.row_number().over(partition_by=user_table.c.name), +... user_table.c.name, +... address_table.c.email_address +... ).select_from(user_table).join(address_table) +>>> with engine.connect() as conn: +... result = conn.execute(stmt) +... print(result.all()) +[(1, 'sandy', 'sandy@sqlalchemy.org'), + (2, 'sandy', 'sandy@squirrelpower.org'), + (1, 'spongebob', 'spongebob@sqlalchemy.org')] +``` + +`FunctionElement.over.order_by` can be used to apply an `ORDER BY` clause. + +```python +>>> stmt = select( +... func.count().over(order_by=user_table.c.name), +... user_table.c.name, +... address_table.c.email_address).select_from(user_table).join(address_table) +>>> with engine.connect() as conn: +... result = conn.execute(stmt) +... print(result.all()) + +[(2, 'sandy', 'sandy@sqlalchemy.org'), + (2, 'sandy', 'sandy@squirrelpower.org'), + (3, 'spongebob', 'spongebob@sqlalchemy.org')] +``` +
+ +### Special Modifiers like WITHIN GROUP, FILTER + +The SQL clause `WITHIN GROUP` is used with ordered sets or hypothetical sets along with aggregate functions. + +Common ordered set functions include `percentile_cont()` and `rank()`. + +In SQLAlchemy, functions such as `rank`, `dense_rank`, `percentile_count`, and `percentile_disc` are implemented, each with the `FunctionElement`.`within_group()` method. + +```python +>>> print( +... func.unnest( +... func.percentile_disc([0.25,0.5,0.75,1]).within_group(user_table.c.name) +... ) +... ) +""" +unnest(percentile_disc(:percentile_disc_1) WITHIN GROUP (ORDER BY user_account.name)) +""" +``` + +Some backends support the "FILTER" modifier, which can be utilized through the `FunctionElement.filter()` method in SQLAlchemy. + +```python +>>> stmt = select( +... func.count(address_table.c.email_address).filter(user_table.c.name == 'sandy'), +... func.count(address_table.c.email_address).filter(user_table.c.name == 'spongebob') +... ).select_from(user_table).join(address_table) +>>> with engine.connect() as conn: +... result = conn.execute(stmt) +... print(result.all()) + +""" +SELECT count(address.email_address) FILTER (WHERE user_account.name = ?) AS anon_1, +count(address.email_address) FILTER (WHERE user_account.name = ?) AS anon_2 +FROM user_account JOIN address ON user_account.id = address.user_id +""" + +('sandy', 'spongebob') +[(2, 1)] +``` + +
+ +### Table-Valued Functions +> I did not understand the part about 'Table-Valued Functions' in the original text. +> If anyone understands this, please contribute to this section. Thank you. + +
+ +### Column Value Functions or Scalar Column (Table Valued Functions) + +One of the special syntaxes supported by Oracle and PostgreSQL is functions set in the FROM clause. Examples in PostgreSQL include `json_array_elements()`, `json_object_keys()`, `json_each_text()`, and `json_each()`. + +SQLAlchemy refers to these functions as column values and applies them using the `FunctionElement.column_valued()` specifier on a `Function` object. + +```python +>>> from sqlalchemy import select, func +>>> stmt = select(func.json_array_elements('["one", "two"]').column_valued("x")) +>>> print(stmt) +""" +SELECT x +FROM json_array_elements(:json_array_elements_1) AS x +""" +``` + +Column value functions can also be used in Oracle as custom SQL functions, as shown below. + +```python +>>> from sqlalchemy.dialects import oracle +>>> stmt = select(func.scalar_strings(5).column_valued("s")) +>>> print(stmt.compile(dialect=oracle.dialect())) +""" +SELECT COLUMN_VALUE s +FROM TABLE (scalar_strings(:scalar_strings_1)) s +""" +``` diff --git a/src/en/tutorial/5.2. Inserting Rows Using Core.md b/src/en/tutorial/5.2. Inserting Rows Using Core.md new file mode 100644 index 0000000..772299d --- /dev/null +++ b/src/en/tutorial/5.2. Inserting Rows Using Core.md @@ -0,0 +1,140 @@ +# Inserting Rows Using Core + +In this chapter, we learn how to INSERT data using the SQLAlchemy Core approach. + +
+ +## Constructing SQL Expressions with `insert()` + +First, you can create an INSERT statement like this: + +```python +>>> from sqlalchemy import insert + +# stmt is an instance of the Insert object. +>>> stmt = insert(user_table).values(name='spongebob', fullname="Spongebob Squarepants") +>>> print(stmt) +'INSERT INTO user_account (name, fullname) VALUES (:name, :fullname)' +``` + +> Here, user_table is the Table object we created in the previous chapter. We created it as follows. +> +> ```python +> from sqlalchemy import MetaData +> from sqlalchemy import Table, Column, Integer, String +> +> metadata = MetaData() +> user_table = Table( +> 'user_account', +> metadata, +> Column('id', Integer, primary_key=True), +> Column('name', String(30)), +> Column('fullname', String), +> ) +> ``` + +Looking at `stmt`, you'll notice that the parameters have not yet been mapped. +This can be checked after `compile()` it, as shown next. + +```python +>>> compiled = stmt.compile() +>>> print(compiled.params) +{'name': 'spongebob', 'fullname': 'Spongebob Squarepants'} +``` + +
+ +## Executing the Statement + +Now, let's execute the INSERT statement we created above using the Core approach. + +```python +>>> with engine.connect() as conn: +... result = conn.execute(stmt) +... conn.commit() + +# The above code executes the following query. + +BEGIN (implicit) +INSERT INTO user_account (name, fullname) VALUES (?, ?) +[...] ('spongebob', 'Spongebob Squarepants') +COMMIT +``` + +What information does the result contain, which is obtained from the return value of `conn.execute(stmt)`? +result is a [`CursorResult`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.CursorResult) object. +It holds various information about the execution results, particularly the [`Row`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Row) objects that contain data rows. + +Since we have just inserted data, we can check the primary key value of the inserted data as follows. + +```python +>>> result.inserted_primary_key # This is also a Row object. +(1, ) # As the primary key can be composed of multiple columns, it is represented as a tuple. +``` + +
+ +## Passing INSERT Parameters to `Connection.execute()` + +Above, we created a statement that included `values` along with `insert`. + +```python +>>> stmt = insert(user_table).values(name='spongebob', fullname="Spongebob Squarepants") +``` + +However, besides this method, you can also execute an INSERT statement by passing parameters to the `Connection.execute()` method. The official documentation suggests this as a more common approach. + +```python +>>> with engine.connect() as conn: +... result = conn.execute( +... insert(user_table), +... [ +... {"name": "sandy", "fullname": "Sandy Cheeks"}, +... {"name": "patrick", "fullname": "Patrick Star"} +... ] +... ) +... conn.commit() +``` + +> The official documentation also explains how to execute statements including subqueries in a separate section. However, it has been deemed not entirely suitable for the tutorial content and is not included in this text. +> For those interested in this topic, please refer to the [original documentation](https://docs.sqlalchemy.org/en/20/tutorial/data_insert.html#insert-usually-generates-the-values-clause-automatically). + +
+ +## `Insert.from_select()` + +Sometimes you need a query to INSERT rows that are received from a SELECT statement, as in the following example. + +Such cases can be written as shown in the following code. + +```python +>>> select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com") +>>> insert_stmt = insert(address_table).from_select( +... ["user_id", "email_address"], select_stmt +... ) +>>> print(insert_stmt) +""" +INSERT INTO address (user_id, email_address) +SELECT user_account.id, user_account.name || :name_1 AS anon_1 +FROM user_account +""" +``` + +
+ +## `Insert.returning()` + +There are situations where you need to receive the value of the processed rows from the database after query processing. This is known as the RETURNING syntax. +For an introduction to this, it would be good to read [this wiki](https://wiki.postgresql.org/wiki/UPSERT). + +In SQLAlchemy Core, this `RETURNING` syntax can be written as follows. + +```python +>>> insert_stmt = insert(address_table).returning(address_table.c.id, address_table.c.email_address) +>>> print(insert_stmt) +""" +INSERT INTO address (id, user_id, email_address) +VALUES (:id, :user_id, :email_address) +RETURNING address.id, address.email_address +""" +``` diff --git a/src/en/tutorial/5.3. Modifying and Deleting Rows Using Core.md b/src/en/tutorial/5.3. Modifying and Deleting Rows Using Core.md new file mode 100644 index 0000000..d2d65ba --- /dev/null +++ b/src/en/tutorial/5.3. Modifying and Deleting Rows Using Core.md @@ -0,0 +1,194 @@ +# Modifying and Deleting Rows Using Core + +In this chapter, we explain the Update and Delete statements used for modifying and deleting existing rows using the Core approach in SQLAlchemy. + +
+ +## Constructing SQL Expressions with `update()` + +You can write an UPDATE statement as follows. + +```python +>>> from sqlalchemy import update +>>> stmt = ( +... update(user_table).where(user_table.c.name == 'patrick'). +... values(fullname='Patrick the Star') +... ) +>>> print(stmt) +'UPDATE user_account SET fullname=:fullname WHERE user_account.name = :name_1' +``` + +```python +>>> stmt = ( +... update(user_table). +... values(fullname="Username: " + user_table.c.name) +... ) +>>> print(stmt) +'UPDATE user_account SET fullname=(:name_1 || user_account.name)' +``` + +> The original text discusses `bindparam()`, but since I haven't seen many use cases for it, it is omitted in this text. If you're curious, please refer to [the original content](https://docs.sqlalchemy.org/en/20/tutorial/data_update.html). + +
+ +### Correlated Update + +Using a [Correlated Subquery](https://docs.sqlalchemy.org/en/20/tutorial/data_select.html#tutorial-scalar-subquery), you can utilize rows from another table as follows. + +```python +>>> scalar_subq = ( +... select(address_table.c.email_address). +... where(address_table.c.user_id == user_table.c.id). +... order_by(address_table.c.id). +... limit(1). +... scalar_subquery() +... ) +>>> update_stmt = update(user_table).values(fullname=scalar_subq) +>>> print(update_stmt) +""" +UPDATE user_account SET fullname=(SELECT address.email_address +FROM address +WHERE address.user_id = user_account.id ORDER BY address.id +LIMIT :param_1) +""" +``` + +
+ +### Updating with Conditions Related to Another Table + +When updating a table, there are times when you need to set conditions in relation to information from another table. +In such cases, you can use it as shown in the example below. + +```python +>>> update_stmt = ( +... update(user_table). +... where(user_table.c.id == address_table.c.user_id). +... where(address_table.c.email_address == 'patrick@aol.com'). +... values(fullname='Pat') +... ) +>>> print(update_stmt) +""" +UPDATE user_account SET fullname=:fullname FROM address +WHERE user_account.id = address.user_id AND address.email_address = :email_address_1 +""" +``` + +
+ +### Updating Multiple Tables Simultaneously + +You can simultaneously update specific values in multiple tables that meet certain conditions, as shown in the following example. + +```python +>>> update_stmt = ( +... update(user_table). +... where(user_table.c.id == address_table.c.user_id). +... where(address_table.c.email_address == 'patrick@aol.com'). +... values( +... { +... user_table.c.fullname: "Pat", +... address_table.c.email_address: "pat@aol.com" +... } +... ) +... ) +>>> from sqlalchemy.dialects import mysql +>>> print(update_stmt.compile(dialect=mysql.dialect())) +""" +UPDATE user_account, address +SET address.email_address=%s, user_account.fullname=%s +WHERE user_account.id = address.user_id AND address.email_address = %s +""" +``` + +> I did not include a summary of the '[Parameter Ordered Updates](https://docs.sqlalchemy.org/en/20/tutorial/data_update.html#parameter-ordered-updates)' section from the original text because I did not understand it. +> If someone understands this part well, it would be appreciated if you could contribute to this document. + +
+ +## Constructing SQL Expressions with `delete()` + +You can write a DELETE statement as follows. + +```python +>>> from sqlalchemy import delete +>>> stmt = delete(user_table).where(user_table.c.name == 'patrick') +>>> print(stmt) +""" +DELETE FROM user_account WHERE user_account.name = :name_1 +""" +``` + +
+ +### Deleting with a JOIN to Another Table + +There are cases where you need to delete data that meets specific conditions after joining with another table. (If this is unclear, refer to [this article](https://stackoverflow.com/questions/11366006/mysql-join-on-vs-using) for clarification.) +In such cases, you can use it as shown in the example below. + +```python +>>> delete_stmt = ( +... delete(user_table). +... where(user_table.c.id == address_table.c.user_id). +... where(address_table.c.email_address == 'patrick@aol.com') +... ) +>>> from sqlalchemy.dialects import mysql +>>> print(delete_stmt.compile(dialect=mysql.dialect())) +""" +DELETE FROM user_account USING user_account, address +WHERE user_account.id = address.user_id AND address.email_address = %s +""" +``` + +
+ +## Getting the Number of Rows Affected in UPDATE, DELETE + +You can obtain the number of rows processed by a query using the ['Result.rowcount'](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.CursorResult.rowcount) property, as shown next. + +```python +>>> with engine.begin() as conn: +... result = conn.execute( +... update(user_table). +... values(fullname="Patrick McStar"). +... where(user_table.c.name == 'patrick') +... ) +... print(result.rowcount) # You can use the rowcount property of the Result object. + +1 # The number of rows processed by the query (the same as the number of rows matching the conditions). +``` + +
+ +## Using RETURNING with UPDATE, DELETE + +You can use the RETURNING syntax as follows. + +For more on the RETURNING syntax, please see [this article](https://www.postgresql.org/docs/current/dml-returning.html). + +```python +>>> update_stmt = ( +... update(user_table).where(user_table.c.name == 'patrick'). +... values(fullname='Patrick the Star'). +... returning(user_table.c.id, user_table.c.name) +... ) +>>> print(update_stmt) +""" +UPDATE user_account SET fullname=:fullname +WHERE user_account.name = :name_1 +RETURNING user_account.id, user_account.name +""" +``` + +```python +>>> delete_stmt = ( +... delete(user_table).where(user_table.c.name == 'patrick'). +... returning(user_table.c.id, user_table.c.name) +... ) +>>> print(delete_stmt) +""" +DELETE FROM user_account +WHERE user_account.name = :name_1 +RETURNING user_account.id, user_account.name +""" +``` \ No newline at end of file diff --git a/src/en/tutorial/6. Manipulating Data Using ORM.md b/src/en/tutorial/6. Manipulating Data Using ORM.md new file mode 100644 index 0000000..12d1611 --- /dev/null +++ b/src/en/tutorial/6. Manipulating Data Using ORM.md @@ -0,0 +1,342 @@ +# Manipulating Data Using ORM + +Until the previous chapter, we focused on utilizing queries from the `CORE` perspective. + +In this chapter, we explain the components, lifecycle, and interaction methods of the `Session` used in the ORM approach. + +
+ +## Inserting rows with ORM + +The `Session` object, when using ORM, creates Insert objects and emits them in transactions. `Session` adds object entries to perform these processes. Then, through a process called `flush`, it records the new items in the database. + +### Instances of Objects Representing Rows + +In the previous process, we executed INSERT using a Python Dictionary. + +In ORM, we directly use user-defined Python objects defined in the table metadata definition. + +```python +>>> squidward = User(name="squidward", fullname="Squidward Tentacles") +>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs") +``` + +We create two `User` objects that represent potential database rows to be INSERTed. Because of `__init__()` constructor automatically created by ORM mapping, we can create each object using the constructor's column names as keys. + +```python +>>> squidward +User(id=None, name='squidward', fullname='Squidward Tentacles') +``` + +Similar to Core's `Insert`, ORM integrates it even without including a primary key. The `None` value for `id` is provided by SQLAlchemy to indicate that the attribute does not have a value yet. + +Currently, the two objects (`squiward` and `krabs`) are in a state called `transient`. The `transient` state means they are not yet connected to any database and not yet associated with a `Session` object that can generate an `INSERT` statement. + +### Adding Objects to the `Session` + +```python +>>> session = Session(engine) # It is essential to close after use. +>>> session.add(squidward) # Insert an object into session via Session.add() method. +>>> session.add(krabs) +``` + +When an object is added to the `Session` through `Session.add()`, it is called being in the `pending` state. +The pending state means the object has not yet been added to the database. + +```python +>>> session.new # You can check the objects in the pending state through session.new. Objects are added to the Session using the Session.add() method. +IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')]) +``` + +- `IdentitySet` is a Python set that hashes object IDs in all cases. +- That is, it uses the `id()` method, not the `hash()` function of Python's built-in functions." + +### Flushing + +The `Session` object uses the [unit of work pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html). This means that it accumulates changes but does not actually communicate with the database until necessary. +This behavior allows objects in the previously mentioned `pending` state to be used more efficiently in SQL DML. +The process of actually sending the current changes to the Database via SQL is called flushing. + +```python +>>> session.flush() +""" +INSERT INTO user_account (name, fullname) VALUES (?, ?) +[...] ('squidward', 'Squidward Tentacles') +INSERT INTO user_account (name, fullname) VALUES (?, ?) +[...] ('ehkrabs', 'Eugene H. Krabs') +""" +``` + +Now, the transaction remains open until one of `Session.commit()`, `Session.rollback()`, or `Session.close()` is invoked. + +While you can use `Session.flush()` directly to push the current pending contents, `Session` typically features `autoflush`, so this is usually not necessary. `Session.commit()` flushes changes every time it is called. + +### Automatically Generated Primary Key Properties + +When a row is inserted, the Python object we created becomes `persistent`. +The `persistent` state is associated with the loaded `Session` object. + +During `INSERT`, the ORM retrieves the primary key identifier for each new object. +This uses the same `CursorResult.inserted_primary_key` accessor introduced earlier. + +```python +>>> squidward.id +4 +>>> krabs.id +5 +``` + +> When ORM is flushed, instead of `executemany`, two separate INSERT statements are used because of this `CursorResult.inserted_primary_key`. +> In SQLite, for instance, you need to `INSERT` one column at a time to use the auto-increment feature (other various databases like PostgreSQL's IDENTITY or SERIAL function similarly). +> If a database connection like `psycopg2`, which can provide primary key information for many rows at once, is used, the ORM optimizes this to `INSERT` many rows at once." + +### Identity Map + +`Identity Map` (`ID Map`) is an in-memory storage that links all objects currently loaded in memory to their primary key IDs. You can retrieve one of these objects through `Session.get()`. This method searches for the object in the `ID Map` if it's in memory, or through a `SELECT` statement if it's not. + +```python +>>> some_squidward = session.get(User, 4) +>>> some_squidward +User(id=4, name='squidward', fullname='Squidward Tentacles') +``` + +An important point is that the `ID Map` maintains unique objects among Python objects. + +```python +>>> some_squidward is squidward +True +``` + +The `ID Map` is a crucial feature that allows manipulation of complex object sets within a transaction in an unsynchronized state. + +### Committing + +We now `commit` the current changes to the transaction. + +```python +>>> session.commit() +COMMIT +``` + +
+ +## How to UPDATE ORM objects + +There are two ways to perform an `UPDATE` through ORM: + +1. Using the `unit of work` pattern employed by `Session`. `UPDATE` operations for each primary key with changes are sent out in sequence. +2. Known as "ORM usage update", where you can explicitly use the `Update` construct with Session." + +### Updating changes automatically + +```python +>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one() +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.name = ? +[...] ('sandy',) +""" +``` + +This 'Sandy' user object acts as a _proxy_ for a row in the database, more specifically, for the row with primary key `2` from the transaction's perspective. + +```python +>>> sandy +User(id=2, name='sandy', fullname='Sandy Cheeks') +>>> sandy.fullname = "Sandy Squirrel" # When an object's attribute is changed, the Session records this change. +>>> sandy in session.dirty # Such changed objects are referred to as 'dirty' and can be checked in session.dirty. +True +``` + +When the `Session` executes `flush`, an `UPDATE` is executed in the database, actually updating the values in the database. If a `SELECT` statement is executed afterwards, a `flush` is automatically executed, allowing you to immediately retrieve the updated name value of Sandy through `SELECT`. + +```python +>>> sandy_fullname = session.execute( +... select(User.fullname).where(User.id == 2) +... ).scalar_one() +""" +UPDATE user_account SET fullname=? WHERE user_account.id = ? +[...] ('Sandy Squirrel', 2) +SELECT user_account.fullname +FROM user_account +WHERE user_account.id = ? +[...] (2,) +""" +>>> print(sandy_fullname) +Sandy Squirrel +# Using the flush, Sandy's changes are actually reflected in the database, +# causing the object to lose its 'dirty' status. +>>> sandy in session.dirty +False +``` + +### ORM usage update + +The last method to perform an `UPDATE` through ORM is to explicitly use 'ORM usage update'. This allows you to use a general SQL `UPDATE` statement that can affect many rows at once. + + +```python +>>> session.execute( +... update(User). +... where(User.name == "sandy"). +... values(fullname="Sandy Squirrel Extraordinaire") +... ) +""" +UPDATE user_account SET fullname=? WHERE user_account.name = ? +[...] ('Sandy Squirrel Extraordinaire', 'sandy') +""" + +``` +If there are objects in the current `Session` that match the given conditions, the corresponding `update` will also be reflected in these objects. + +```python +>>> sandy.fullname +'Sandy Squirrel Extraordinaire' +``` + +
+ +## How to Delete ORM objects + +You can mark individual ORM objects for deletion using the `Session.delete()` method. Once `delete` is executed, objects in that `Session` become expired. + +```python +>>> patrick = session.get(User, 3) +""" +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,) +""" +>>> session.delete(patrick) # Indicate that patrick will be deleted +>>> session.execute( +... select(User) +... .where(User.name == "patrick") +... ).first() # Execute flush at this point +""" +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,) +DELETE FROM user_account WHERE user_account.id = ? +[...] (3,) +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.name = ? +[...] ('patrick',) +""" +>>> squidward in session # Once expired in the Session, the object is removed from the session. +False +``` + +Like the 'Sandy' used in the above `UPDATE`, these actions are only within the ongoing transaction and can be undone at any time unless _committed_. + +### ORM usage delete + +Like `UPDATE`, there is also 'ORM usage delete'. + +```python +# This is just an example, not a necessary operation for delete. +>>> squidward = session.get(User, 4) +""" +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 = ? +[...] (4,) +""" + +>>> session.execute(delete(User).where(User.name == "squidward")) +""" +DELETE FROM user_account WHERE user_account.name = ? +[...] ('squidward',) + +""" +``` + +
+ +## Rolling Back + +`Session` has a `Session.rollback()` method to roll back the current operations. This method affects Python objects like the aforementioned `sandy`. +Calling `Session.rollback()` not only rolls back the transaction but also turns all objects associated with this `Session` into `expired` status. This state change triggers a self-refresh the next time the object is accessed, a process known as _lazy loading_. + +```python +>>> session.rollback() +ROLLBACK +``` + +Looking closely at `sandy`, which is in the `expired` state, you can see that no other information remains except for special SQLAlchemy-related status objects. + +```python +>>> sandy.__dict__ +{'_sa_instance_state': } +>>> sandy.fullname # Since the session is expired, accessing the object properties will trigger a new transaction. +""" +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,) +""" +'Sandy Cheeks' +>>> sandy.__dict__ # Now you can see that the database row is also filled in the sandy object. +{'_sa_instance_state': , + 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'} +``` + +For the deleted objects, you can see that they are restored in the `Session` and appear again in the database. + +```python +>>> patrick in session +True +>>> session.execute(select(User).where(User.name == 'patrick')).scalar_one() is patrick +""" +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.name = ? +[...] ('patrick',) +""" +True +``` + +
+ +## Closing the `Session` + +We have handled the `Session` outside of the context structure, and in such cases, it is good practice to _explicitly_ close the `Session` as follows: + +```python +>>> session.close() +ROLLBACK +``` + +Similarly, when a `Session` created through a context statement is closed within the context statement, the following actions are performed. + +- Cancel all ongoing transactions (e.g., rollbacks) to release all connection resources to the connection pool. + - This means you don't need to explicitly call `Session.rollback()` to check if the transaction was rolled back when closing the `Session` after performing some read-only operations with it. The connection pool handles this. +- Remove all objects from the `Session`. + - This means that all Python objects loaded for this Session, such as `sandy`, `patrick`, and `squidward`, are now in a `detached` state. For instance, an object that was in the `expired` state is no longer associated with a database transaction to refresh data due to a `Session.commit()` call, and it does not contain the current row's state. + - ```python + >>> squidward.name + Traceback (most recent call last): + ... + sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed + ``` + - Detached objects can be reassociated with the same or a new `Session` using the `Session.add()` method, re-establishing the relationship with a specific database row. + - ```python + >>> session.add(squidward) # Reconnect to the session + >>> squidward.name # Retrieve the information through the transaction again. + """ + 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 = ? + [...] (4,) + """ + 'squidward' + ``` + +> Objects in the `detached` state should ideally be avoided. When a `Session` is closed, it cleans up references to all previously connected objects. Typically, the need for `detached` objects arises in web applications when an object has just been committed and the `Session` is closed before it is rendered in a view. In this case, set the `Session.expire_on_commit` flag to `False`. diff --git a/src/en/tutorial/7. Working with Related Objects Using ORM.md b/src/en/tutorial/7. Working with Related Objects Using ORM.md new file mode 100644 index 0000000..f7ce0cd --- /dev/null +++ b/src/en/tutorial/7. Working with Related Objects Using ORM.md @@ -0,0 +1,776 @@ +# Working with Related Objects Using ORM + +
+ +In this chapter, we will cover another essential ORM concept, which is the interaction with mapped objects that reference other objects. + +`relationship()` defines the relationship between two mapped objects and is also known as **self-referencing**. + +For simplicity, we will omit `Column` mappings and other directives, and explain `relationship()` in a shortened form. + +
+ +```python +from sqlalchemy.orm import relationship + + +class User(Base): + __tablename__ = 'user_account' + + # ... Column mappings + + addresses = relationship("Address", back_populates="user") + + +class Address(Base): + __tablename__ = 'address' + + # ... Column mappings + + user = relationship("User", back_populates="addresses") +``` + +
+ +In the structure shown, the `User` object has a variable `addresses`, and the `Address` object has a variable `user`. + +Both are created as relationship objects, but these aren't __actual database columns__ but are set up to allow __easy access__ in the code. + +In other words, it facilitates easy navigation from a `User` object to an `Address` object. + +Additionally, the `back_populates` parameter in the `relationship` declaration allows for the reverse situation, i.e., navigating from an `Address` object to a `User` object. + +> In relational Database terms, it naturally sets a 1 : N relationship as an N : 1 relationship. + +In the next section, we will see what role the `relationship()` object's instances play and how they function. + +
+ +## Using Related Objects + +
+ +When a new `User` object is created, the `.addresses` collection appears as a `List` object. + +```python +>>> u1 = User(name='pkrabs', fullname='Pearl Krabs') +>>> u1.addresses +[] +``` + +You can add an `Address` object using `list.append()`. + +```python +>>> a1 = Address(email_address="pear1.krabs@gmail.com") +>>> u1.addresses.append(a1) + +# The u1.addresses collection now includes the new Address object. +>>> u1.addresses +[Address(id=None, email_address='pearl.krabs@gmail.com')] +``` + +If an `Address` object is associated with the `User.addresses` collection, another action occurs in the variable `u1`. The User.addresses and Address.user relationship is synchronized, allowing you to move: + - From a `User` object to an `Address`, and + - Back from an `Address` object to a `User`. + +```python +>>> a1.user +User(id=None, name='pkrabs', fullname='Pearl Krabs') +``` + +This is the result of synchronization using `relationship.back_populates` between the two `relationship()` objects. + +The `relationship()` parameter can be complementarily assigned/list modified to another variable. Creating another `Address` object and assigning it to the `Address.user` property makes it part of the `User.addresses` collection. + +```python +>>> a2 = Address(email_address="pearl@aol.com", user=u1) +>>> u1.addresses +[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')] +``` + +
+ +We actually used the variable `u1` as a keyword argument for `user` as if it were a property declared in the object (`Address`). It's equivalent to assigning the property afterward. + +```python +# equivalent effect as a2 = Address(user=u1) +>>> a2.user = u1 +``` + +
+ +## Cascading Objects in the `Session` + +
+ +We now have two related `User` and `Address` objects in a bidirectional structure in memory, but as mentioned earlier in [Inserting Rows with ORM] , these objects are in a [transient] state in the `Session` until they are associated with it. + +We need to see when using `Session.add()`, and applying the method to the `User` object, that the related `Address` objects are also added. + +```python +>>> session.add(u1) +>>> u1 in session +True +>>> a1 in session +True +>>> a2 in session +True +``` + +The three objects are now in a [pending] state, which means no `INSERT` operations have been executed yet. The three objects have not been assigned primary keys, and the `a1` and `a2` objects have a column (`user_id`) reference property. This is because the objects are not yet actually connected to a real database. + +```python +>>> print(u1.id) +None +>>> print(a1.user_id) +None +``` + +Let's save it to the database. + +```python +>>> session.commit() +``` + +If we translate the implemented code into SQL queries, it would look like this. + +```sql +INSERT INTO user_account (name, fullname) VALUES (?, ?) +[...] ('pkrabs', 'Pearl Krabs') +INSERT INTO address (email_address, user_id) VALUES (?, ?) +[...] ('pearl.krabs@gmail.com', 6) +INSERT INTO address (email_address, user_id) VALUES (?, ?) +[...] ('pearl@aol.com', 6) +COMMIT +``` + +Using session, you can automate SQL `INSERT`, `UPDATE`, `DELETE` statements. + +Finally, executing `Session.commit()` ensures all steps are called in the correct order, and the primary key of `address.user_id` is applied in the `user_account`. + +
+ +## Loading Relationships + +
+ +After calling `Session.commit()`, you can see the primary key created for the `u1` object. + +```python +>>> u1.id +6 +``` + +> The above code is equivalent to executing the following query. + +```sql +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 = ? +[...] (6,) +``` + +You can also see that `id`s are now present in the objects linked to `u1.addresses`. + +To retrieve these objects, we can observe the **lazy load** approach. + +> lazy loading : This is a method where a SELECT statement is executed to fetch information only when someone tries to access that information. In other words, it retrieves the necessary information as needed. + +```python +>>> u1.addresses +[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] +``` + +```sql +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 +[...] (6,) +``` + +SQLAlchemy ORM’s default for collections and related properties is **lazy loading**. This means once a collection has been *relationshipped*, as long as the data exists in memory, it remains accessible. + +```python +>>> u1.addresses +[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] +``` +Although lazy loading can be costly without explicit steps for optimization, it is optimized at least not to perform redundant operations. + +You can also see the `a1` and `a2` objects in the `u1.addresses` collection. + +```python +>>> a1 +Address(id=4, email_address='pearl.krabs@gmail.com') +>>> a2 +Address(id=5, email_address='pearl@aol.com') +``` + +We will provide a further introduction to the concept of `relationship` in the latter part of this section. + +
+ +## Using `relationship` in Queries + +
+ +This section introduces several ways in which `relationship()` helps automate SQL query construction. + +
+ +### JOIN tables using `relationship()` + +In [Specifying the FROM and JOIN Clauses] and [WHERE Clauses] sections, we used `Select.join()` and `Select.join_from()` methods to construct SQL JOINs. These methods infer the ON clause based on whether there's a `ForeignKeyConstraint` object linking the two tables or provide specific SQL Expression syntax representing the `ON` clause. + +`relationship()` objects can be used to set the `ON` clause for joins. +A `relationship()` corresponding object can be passed as a **single argument** to `Select.join()`, serving as both the right join and the ON clause. + +```python +>>> print( +... select(Address.email_address). +... select_from(User). +... join(User.addresses) +... ) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.email_address +FROM user_account JOIN address ON user_account.id = address.user_id +``` + +If `relationship()` is not specified in `Select.join()` or `Select.join_from()`, **no ON clause is used**. This means it functions due to the `ForeignKeyConstraint` between the two mapped table objects, not because of the `relationship()` object of `User` and `Address`. + +```python +>>> print( +... select(Address.email_address). +... join_from(User, Address) +... ) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.email_address +FROM user_account JOIN address ON user_account.id = address.user_id +``` + +
+ +### Joining Using Aliases(`aliased`) + +When configuring SQL JOINs using `relationship()`, it's suitable to use [PropComparator.of_type()] with `aliased()` cases. However, `relationship()` is used to configure the same joins as described in [`ORM Entity Aliases`]. + +You can directly use `aliased()` in a join with `relationship()`. + +```python +>>> from sqlalchemy.orm import aliased +>>> address_alias_1 = aliased(Address) +>>> address_alias_2 = aliased(Address) +>>> print( +... select(User). +... join_from(User, address_alias_1). +... where(address_alias_1.email_address == 'patrick@aol.com'). +... join_from(User, address_alias_2). +... where(address_alias_2.email_address == 'patrick@gmail.com') +... ) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +JOIN address AS address_1 ON user_account.id = address_1.user_id +JOIN address AS address_2 ON user_account.id = address_2.user_id +WHERE address_1.email_address = :email_address_1 +AND address_2.email_address = :email_address_2 +``` + +You can use join clause in `aliased()` object using `relationship()`. + +```python +>>> user_alias_1 = aliased(User) +>>> print( +... select(user_alias_1.name). +... join(user_alias_1.addresses) +... ) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account_1.name +FROM user_account AS user_account_1 +JOIN address ON user_account_1.id = address.user_id +``` + +
+ +### Expanding ON Conditions + +You can add conditions to the ON clause created by `relation()`. This feature is useful not only for quickly limiting the scope of a specific join for a related path but also for use cases like loader strategy configuration introduced in the last section. +[`PropComparator.and_()`] method allows a series of SQL expressions to be positionally combined in the JOIN's `ON` clause via `AND`. +For example, to limit the ON criteria to specific email addresses using `User` and `Address`, you would do this. + +```python +>>> stmt = ( +... select(User.fullname). +... join(User.addresses.and_(Address.email_address == 'pearl.krabs@gmail.com')) +... ) + +>>> session.execute(stmt).all() +[('Pearl Krabs',)] +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.fullname +FROM user_account +JOIN address ON user_account.id = address.user_id AND address.email_address = ? +[...] ('pearl.krabs@gmail.com',) +``` + +
+ +### EXISTS `has()` , `and()` + +In the [EXISTS Subqueries] section, the SQL EXISTS keyword was introduced along with the [Scalar Subqueries, Correlated Queries] section. +`relationship()` provides some help in commonly creating subqueries for relationships. + +
+ +For a 1:N (one-to-many) relationship like `User.addresses`, you can use `PropComparator.any()` to create a subquery for the address table rejoining the `user_account` table. This method allows optional WHERE criteria to limit the rows matching the subquery. + +```python +>>> stmt = ( +... select(User.fullname). +... where(User.addresses.any(Address.email_address == 'pearl.krabs@gmail.com')) +... ) + +>>> session.execute(stmt).all() +[('Pearl Krabs',)] +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.fullname +FROM user_account +WHERE EXISTS (SELECT 1 +FROM address +WHERE user_account.id = address.user_id AND address.email_address = ?) +[...] ('pearl.krabs@gmail.com',) +``` + +Conversely, to find objects without related data, use `~User.addresses.any()` to search for `User` objects. + +```python +>>> stmt = ( +... select(User.fullname). +... where(~User.addresses.any()) +... ) + +>>> session.execute(stmt).all() +[('Patrick McStar',), ('Squidward Tentacles',), ('Eugene H. Krabs',)] +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.fullname +FROM user_account +WHERE NOT (EXISTS (SELECT 1 +FROM address +WHERE user_account.id = address.user_id)) +[...] () + +``` + +`PropComparator.has()` works similarly to `PropComparator.any()` but is used for N:1 (Many-to-one) relationships. +For instance, to find all `Address` objects belonging to "pearl", you would use this method. + +```python +>>> stmt = ( +... select(Address.email_address). +... where(Address.user.has(User.name=="pkrabs")) +... ) + +>>> session.execute(stmt).all() +[('pearl.krabs@gmail.com',), ('pearl@aol.com',)] +``` +> The above code is equivalent to executing the following query. + +```sql +SELECT address.email_address +FROM address +WHERE EXISTS (SELECT 1 +FROM user_account +WHERE user_account.id = address.user_id AND user_account.name = ?) +[...] ('pkrabs',) +``` + +
+ +### Relationship Operators + +Several types of SQL creation helpers come with `relationship()`: + +- N : 1 (Many-to-one) comparison +You can select rows where the foreign key of the target entity matches the primary key value of a specified object instance in an N:1 relationship. + +```python +>>> print(select(Address).where(Address.user == u1)) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.id, address.email_address, address.user_id +FROM address +WHERE :param_1 = address.user_id +``` + +- NOT N : 1 (Many-to-one) comparison +You can use the not equal (!=) operator. + +```python +>>> print(select(Address).where(Address.user != u1)) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.id, address.email_address, address.user_id +FROM address +WHERE address.user_id != :user_id_1 OR address.user_id IS NULL +``` + +- You can check if an object is included in a 1:N (one-to-many) collection. +```python +>>> print(select(User).where(User.addresses.contains(a1))) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account +WHERE user_account.id = :param_1 +``` + +- You can check if an object in a 1:N relationship is part of a specific parent item. `with_parent()` creates a comparison that returns rows referencing the given parent item, equivalent to using the `==` operator." + + +```python +>>> from sqlalchemy.orm import with_parent +>>> print(select(Address).where(with_parent(u1, User.addresses))) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.id, address.email_address, address.user_id +FROM address +WHERE :param_1 = address.user_id +``` + +
+ +## Types of Relationship Loading + +
+ +In the [Loading Relationships](#loading-relationships) section, we introduced the concept that when working with mapped object instances and accessing mapped attributes using `relationship()`, objects that should be in this collection are loaded, and if the collection is not filled, _lazy load_ occurs. + +Lazy loading is one of the most famous ORM patterns and also the most controversial. If dozens of ORM objects in memory each refer to a few unloaded properties, the routine manipulation of objects can implicitly release many problems ([`N+1 Problem`]), which can accumulate. Such implicit queries may not work at all when attempting database transformations that are no longer viable or when using alternative concurrency patterns like asynchronous. + +> What is a [`N + 1 Problem`]? +> It's a problem where you fetch N records with one query, but to get the desired data, you end up performing a secondary query for each of these N records. + +Lazy loading is a very popular and useful pattern when it is compatible with the concurrency approach in use and does not cause other problems. For this reason, SQLAlchemy's ORM focuses on features that allow you to permit and optimize these load behaviors. + +Above all, the first step to effectively using ORM's lazy loading is to **test the Application and check the SQL**. +If inappropriate loads occur for objects detached from the `Session`, the use of **[`Types of Relationship Loading`](#types-of-relationship-loading)** should be reviewed. + +You can mark objects to be associated with a SELECT statement using the `Select.options()` method. + +```python +for user_obj in session.execute( + select(User).options(selectinload(User.addresses)) +).scalars(): + user_obj.addresses # access addresses collection already loaded +``` + +You can also configure it as a default for `relationship()` using `relationship.lazy`. + +```sql +from sqlalchemy.orm import relationship +class User(Base): + __tablename__ = 'user_account' + + addresses = relationship("Address", back_populates="user", lazy="selectin") +``` + +> +> cf. **Two Techniques of Relationship Loading** +> +> - [`Configuring Loader Strategies at Mapping Time`] +> - Details about `relationship()` configuration +> - [`Relationship Loading with Loader Options`] +> - Details about the loader + +
+ +### Select IN loading Method + +The most useful loading option in recent SQLAlchemy versions is `selectinload()`. This option solves the most common form of the "N+1 Problem" problem, which is an issue with sets of objects referencing related collections. It typically uses a SELECT form that can be sent out for the related table without introducing JOINs or subqueries and only queries for parent objects whose collections are not loaded. + +The following example shows the Address objects related to a `User` object being loaded with `selectinload()`. During the `Session.execute()` call, two SELECT statements are generated in the database, with the second fetching the related `Address` objects. + +```sql +>>> from sqlalchemy.orm import selectinload +>>> stmt = ( +... select(User).options(selectinload(User.addresses)).order_by(User.id) +... ) +>>> for row in session.execute(stmt): +... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})") +spongebob (spongebob@sqlalchemy.org) +sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org) +patrick () +squidward () +ehkrabs () +pkrabs (pearl.krabs@gmail.com, pearl@aol.com) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account ORDER BY user_account.id +[...] () +SELECT address.user_id AS address_user_id, address.id AS address_id, +address.email_address AS address_email_address +FROM address +WHERE address.user_id IN (?, ?, ?, ?, ?, ?) +[...] (1, 2, 3, 4, 5, 6) +``` + +
+ +### Joined Loading Method + +_Joined Loading_, the oldest in SQLAlchemy, is a type of eager loading, also known as `joined eager loading`. It is best suited for loading objects in "N:1 relationships", as it performs a SELECT JOIN of the tables specified in `relationship()`, fetching all table data at once. + +For example, where an `Address` object has a connected user, an INNER JOIN can be used rather than an OUTER JOIN. + +```python +>>> from sqlalchemy.orm import joinedload +>>> stmt = ( +... select(Address).options(joinedload(Address.user, innerjoin=True)).order_by(Address.id) +... ) +>>> for row in session.execute(stmt): +... print(f"{row.Address.email_address} {row.Address.user.name}") + +spongebob@sqlalchemy.org spongebob +sandy@sqlalchemy.org sandy +sandy@squirrelpower.org sandy +pearl.krabs@gmail.com pkrabs +pearl@aol.com pkrabs +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1, +user_account_1.name, user_account_1.fullname +FROM address +JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id +ORDER BY address.id +[...] () +``` + +`joinedload()` is also used for 1: N collections but should be evaluated case-by-case compared to other options like `selectinload()` due to its nested collections and larger collections. + +It's important to note that the WHERE and ORDER BY criteria of the SELECT query **do not target the table rendered by `joinload()`**. In the SQL query above, you can see an **anonymous alias** applied to the `user_account` table, which cannot directly address. This concept is further explained in the [Zen of joined Eager Loading] section. + +The ON clause by `joinedload()` can be directly influenced using the method described previously in [`Expanding ON Conditions`](#expanding-on-conditions). + +> cf. +> +> In general cases, "N+1 problem" is much less prevalent, so many-to-one eager loading is often unnecessary. +> +> When many objects all reference the same related object (e.g., many `Address` objects referencing the same `User`), a single SQL for the `User` object is emitted using ordinary lazy loading. +> +> The lazy loading routine queries the related object by the current primary key without emitting SQL if possible. + +
+ +### Explicit Join + Eager Load Method + +A common use case uses the `contains_eager()` option, which is very similar to `joinedload()` except it assumes you have set up the JOIN directly and instead marks additional columns in the COLUMNS clause that should be loaded into each object's related properties. + +```python +>>> from sqlalchemy.orm import contains_eager + +>>> stmt = ( +... select(Address). +... join(Address.user). +... where(User.name == 'pkrabs'). +... options(contains_eager(Address.user)).order_by(Address.id) +... ) + +>>> for row in session.execute(stmt): +... print(f"{row.Address.email_address} {row.Address.user.name}") + +pearl.krabs@gmail.com pkrabs +pearl@aol.com pkrabs +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.id, user_account.name, user_account.fullname, +address.id AS id_1, address.email_address, address.user_id +FROM address JOIN user_account ON user_account.id = address.user_id +WHERE user_account.name = ? ORDER BY address.id +[...] ('pkrabs',) +``` + +For instance, we filtered `user_account.name` and loaded it into the returned `Address.user` property. A separate application of `joinedload()` would have unnecessarily created a twice-joined SQL query. + +```python +>>> stmt = ( +... select(Address). +... join(Address.user). +... where(User.name == 'pkrabs'). +... options(joinedload(Address.user)).order_by(Address.id) +... ) +>>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily +``` +> The above code is equivalent to executing the following query. +```sql +SELECT address.id, address.email_address, address.user_id, +user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname +FROM address JOIN user_account ON user_account.id = address.user_id +LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id +WHERE user_account.name = :name_1 ORDER BY address.id +``` + +
+ +> cf. +> +> **Two Techniques of Relationship Loading** +> [`Zen of joined Eager Loading`] +> - Details about this loading method +> [`Routing Explicit Joins/Statements into Eagerly Loaded Collections`] +> - using `contains_eager()` + +
+ +### Setting Loader Paths + +The `PropComparator.and_()` method is actually generally usable for most loader options. + +For example, if you want to reload usernames and email addresses from the `sqlalchemy.org` domain, you can limit the conditions with `PropComparator.and_()` applied to the arguments passed to `selectinload()`. + +```python +>>> from sqlalchemy.orm import selectinload +>>> stmt = ( +... select(User). +... options( +... selectinload( +... User.addresses.and_( +... ~Address.email_address.endswith("sqlalchemy.org") +... ) +... ) +... ). +... order_by(User.id). +... execution_options(populate_existing=True) +... ) + +>>> for row in session.execute(stmt): +... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})") + +spongebob () +sandy (sandy@squirrelpower.org) +patrick () +squidward () +ehkrabs () +pkrabs (pearl.krabs@gmail.com, pearl@aol.com) +``` +> The above code is equivalent to executing the following query. +```sql +SELECT user_account.id, user_account.name, user_account.fullname +FROM user_account ORDER BY user_account.id +[...] () +SELECT address.user_id AS address_user_id, address.id AS address_id, +address.email_address AS address_email_address +FROM address +WHERE address.user_id IN (?, ?, ?, ?, ?, ?) +AND (address.email_address NOT LIKE '%' || ?) +[...] (1, 2, 3, 4, 5, 6, 'sqlalchemy.org') +``` + +It's crucial to note the addition of the `.execution_options(populate_existing=True)` **option** above. When fetching rows, this option indicates that loader options must replace the existing collections' contents in already loaded objects. + +Since we are iterating with a `Session` object, the objects being loaded here are the same Python instances as those initially maintained at the start of this tutorial's ORM section. + +
+ +### Raise Loading Method + +The `raiseload()` option is commonly used to completely block the occurrence of the "N+1 problem" by instead causing errors rather than slow loading. + +There are two variants: blocking all "load" operations that include works that need SQL (_lazy load_) and those that only reference the current `Session` (`raiseload.sql_only` **option**). + + +```python +class User(Base): + __tablename__ = 'user_account' + + # ... Column mappings + + addresses = relationship("Address", back_populates="user", lazy="raise_on_sql") + + +class Address(Base): + __tablename__ = 'address' + + # ... Column mappings + + user = relationship("User", back_populates="addresses", lazy="raise_on_sql") +``` + +Using such mappings blocks the application from 'lazy loading', requiring you to specify loader strategies for specific queries. + +```python +u1 = s.execute(select(User)).scalars().first() +u1.addresses +sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql' +``` + +The exception indicates that the collection must be loaded first. + +```python +u1 = s.execute(select(User).options(selectinload(User.addresses))).scalars().first() +``` + +The `lazy="raise_on_sql"` option is also wisely attempted for N:1 relationships. + +Above, although the `Address.user` property was not loaded into `Address`, "raiseload" does not cause an error because the corresponding `User` object is in the same `Session`. + +> cf. +> +> [Preventing unwanted lazy loading with `raiseload`] +> [Preventing lazy loading in `relationship`] + + + +[Inserting Rows with ORM]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/en/tutorial/6.%20Manipulating%20Data%20Using%20ORM.html#inserting-rows-with-orm) +[transient]: (https://docs.sqlalchemy.org/en/20/glossary.html#term-transient) +[pending]: (https://docs.sqlalchemy.org/en/20/glossary.html#term-pending) + +[Specifying the FROM and JOIN Clauses]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#from%E1%84%8C%E1%85%A5%E1%86%AF%E1%84%80%E1%85%AA-join-%E1%84%86%E1%85%A7%E1%86%BC%E1%84%89%E1%85%B5%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5) + +[WHERE Clauses]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#where%E1%84%8C%E1%85%A5%E1%86%AF) + + +[`PropComparator.and_()`]: (https://docs.sqlalchemy.org/en/14/orm/internals.html#sqlalchemy.orm.PropComparator.and_) + +[EXISTS subqueries]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#exists-%E1%84%89%E1%85%A5%E1%84%87%E1%85%B3%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5%E1%84%83%E1%85%B3%E1%86%AF) + +[Scalar Subqueries, Correlated Queries]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#%E1%84%89%E1%85%B3%E1%84%8F%E1%85%A1%E1%86%AF%E1%84%85%E1%85%A1-%E1%84%89%E1%85%A5%E1%84%87%E1%85%B3-%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5-%E1%84%89%E1%85%A1%E1%86%BC%E1%84%92%E1%85%A9%E1%84%8B%E1%85%A7%E1%86%AB%E1%84%80%E1%85%AA%E1%86%AB-%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5) + +[`N + 1 Problem`]: (https://blog.naver.com/yysdntjq/222405755893) + +[`N+1 Problem`]: (https://docs.sqlalchemy.org/en/20/glossary.html#term-N-plus-one-problem) + +[`Zen of joined Eager Loading`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#zen-of-eager-loading) + +[`Routing Explicit Joins/Statements into Eagerly Loaded Collections`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#contains-eager) + +[`Configuring Loader Strategies at Mapping Time`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#relationship-lazy-option) + +[`Relationship Loading with Loader Options`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#relationship-loader-options) + +[Preventing unwanted lazy loading with `raiseload`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#prevent-lazy-with-raiseload) + +[Preventing lazy loading in `relationship`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html) diff --git a/src/en/tutorial/README.md b/src/en/tutorial/README.md new file mode 100644 index 0000000..bcab295 --- /dev/null +++ b/src/en/tutorial/README.md @@ -0,0 +1,19 @@ +# Tutorial + +This document is a translated and organized version of the [SQLAlchemy 1.4/2.0 Tutorial](https://docs.sqlalchemy.org/en/14/tutorial/). + +The original official documentation is challenging to navigate and contains an overwhelming amount of information. Furthermore, it can be quite difficult for beginners to understand. +With this in mind, we thought of translating and organizing the SQLAlchemy Tutorial documents for easier comprehension and accessibility. The idea originated from [this post](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/issues/2), where people gathered to work together on this project. + +Over a little more than a month, we took turns each week to work on a chapter of the official Tutorial document. +We reviewed each other's work, refining the articles and asking questions about parts we didn't understand. +These documents are the results of such efforts and also represent the traces of our study. + +There are still many imperfections, and there might be errors. +If you find such issues, please feel free to contribute. Your participation is always welcome. + +The following individuals have contributed to this project: + + + + diff --git a/src/en/tutorial/config.js b/src/en/tutorial/config.js new file mode 100644 index 0000000..e3579d0 --- /dev/null +++ b/src/en/tutorial/config.js @@ -0,0 +1,120 @@ +const { description } = require('../../package') + +module.exports = { + base: "/sqlalchemy-for-pythonist/", + + // Path + locales: { + '/': { + lang: 'ko', + title: '파이썬 개발자를 위한 SQLAlchemy', + description: description, + }, + '/en/': { + lang: 'en-US', + title: 'SQLAlchemy for Python Developers', + description: 'This is a document that simplifies SQLAlchemy for easy understanding.', + } + }, + + /** + * Extra tags to be injected to the page HTML `` + * + * ref:https://v1.vuepress.vuejs.org/config/#head + */ + head: [ + ['meta', { name: 'theme-color', content: '#3eaf7c' }], + ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], + ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], + ['meta', { name: 'google-site-verification', content: 'wjX_mSoZBgO9SZMvjr96yOjo6n3_7pS8xNdmzDl1ESw' }], + [ + "script", + { + async: true, + src: "https://www.googletagmanager.com/gtag/js?id=G-SNPCYHY4R2", + }, + ], + ["script", {}, ["window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-SNPCYHY4R2');"]], + ], + + /** + * Theme configuration, here is the default theme configuration for VuePress. + * + * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html + */ + themeConfig: { + locales: { + '/': { + repo: '', + editLinks: true, + docsDir: '', + editLinkText: '', + lastUpdated: true, + smoothScroll: true, + nav: [ + { + text: 'GitHub', + link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' + }, + ], + sidebar: { + '/tutorial/': [ + { + title: 'Tutorial', + path: '/tutorial/', + collapsable: false, + children: [ + '1. 튜토리얼 개요', + '2. 연결 설정하기', + '3. 트랜잭션과 쿼리 실행하기', + '4. 데이터베이스 메타데이터로 작업하기', + '5.1. Core와 ORM 방식으로 행 조회하기', + '5.2. Core 방식으로 행 삽입하기', + '5.3. Core 방식으로 행 수정 및 삭제하기', + '6. ORM 방식으로 데이터 조작하기', + '7. ORM 방식으로 관련 개체 작업하기', + ] + }, + ] + } + }, + '/en/': { + repo: '', + editLinks: true, + docsDir: '/en.', + editLinkText: '/en/', + lastUpdated: true, + smoothScroll: true, + nav: [ + { + text: 'GitHub', + link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' + }, + ], + sidebar: { + '/en/tutorial/': [ + { + title: 'Tutorial', + path: '/en/tutorial/', + collapsable: false, + children: [ + '1. tutorial', + '2. connection setting', + ] + }, + ] + } + }, + }, + }, + + /** + * Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/ + */ + plugins: [ + '@vuepress/plugin-back-to-top', + '@vuepress/plugin-medium-zoom', + ["sitemap", { hostname: "https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/" }], + ["@vuepress/last-updated"], + ], +}