Python Advance 📂 Important concepts · 3 of 6 324 min read

Databases in Python — sqlite3 and SQLAlchemy with Diagrams

Master Python's two essential database tools from first principles. Start with sqlite3 — the zero-setup SQL engine that ships with Python — then graduate to SQLAlchemy for typed models, portable connection URLs, and object-relational mapping. Includes 4 visual diagrams (the connection pipeline, transaction lifecycle, ORM class↔table mapping, one-to-many relationships), parameterized queries to block SQL injection, transactions with commit/rollback, foreign keys and joins, and a full production-s

🌐
SectionSection第 01 节

The Story That Explains Databases 📚Databases समझाने वाली कहानी 📚一个讲清数据库的故事 📚

The Notebook vs the LibraryNotebook बनाम Library笔记本 vs 图书馆
Imagine you run a bookstore. You track customers in a notebook. On day one, easy. By day 500 you have three notebooks, hundreds of crossed-out edits, and no way to answer "who bought sci-fi books last month?" without flipping every page. You have hit the wall of flat storage: files are great until you need to ask questions.

Now imagine the same data in a library with a card catalogue. Every book has a shelf. Every card is sorted by author, title and genre. Ask "sci-fi bought in October" and the librarian hands you a stack in seconds. Nothing moved — just organised questions against organised data.

That is a database. Data lives in named tables. Each row is a record. You ask questions in SQL. Python has two main tools for this: sqlite3 (built in, zero setup) and SQLAlchemy (the popular toolkit that works with many databases). This tutorial covers both.
मान लीजिए आप एक किताबों की दुकान चलाते हैं। आप ग्राहकों का हिसाब एक notebook में रखते हैं। पहले दिन आसान है। 500वें दिन तक आपके पास तीन notebooks हैं, सैकड़ों कटे-फटे बदलाव, और "पिछले महीने sci-fi किताबें किसने ख़रीदीं?" का जवाब हर पन्ना पलटे बिना नहीं मिलता। आप flat storage की दीवार से टकरा गए: files तब तक बढ़िया हैं जब तक आपको सवाल पूछने न हों।

अब वही data एक library में सोचिए जिसमें card catalogue है। हर किताब की एक shelf है। हर card लेखक, शीर्षक और genre से क्रमित है। पूछिए "अक्टूबर में ख़रीदी गई sci-fi" और librarian सेकंडों में ढेर थमा देता है। कुछ नहीं हिला — बस व्यवस्थित data पर व्यवस्थित सवाल।

यही database है। Data नाम वाली tables में रहता है। हर row एक record है। आप SQL में सवाल पूछते हैं। Python में इसके दो मुख्य औज़ार हैं: sqlite3 (built-in, कोई setup नहीं) और SQLAlchemy (लोकप्रिय toolkit जो कई databases के साथ चलता है)। यह tutorial दोनों सिखाता है।
想象你经营一家书店,用一个 笔记本 记录顾客。第一天很轻松。到了第 500 天,你有了三个笔记本、几百处涂改,想回答"上个月谁买了科幻书?"就只能一页页翻。你撞上了 平面存储 的墙:文件很好用,直到你需要 提问。

现在想象同样的数据放在一个有卡片目录的 图书馆 里。每本书都有书架,每张卡片按作者、书名和类别排好。问一句"十月卖出的科幻书",图书管理员几秒钟就递给你一叠。什么都没搬动 — 只是对有序的数据提出 有序的问题。

这就是 数据库。数据存放在有名字的表中,每一行是一条记录。你用 SQL 提问。Python 为此有两个主要工具:sqlite3(内置,零配置)和 SQLAlchemy(流行的工具包,支持多种数据库)。本教程两者都讲。

A database is organised storage you can query. Python ships with sqlite3 — a full SQL engine stored in a single file. For bigger work — PostgreSQL, MySQL, an ORM, migrations — most Python developers reach for SQLAlchemy. Together they cover most of what everyday Python programs need.

Database व्यवस्थित storage है जिससे सवाल पूछे जा सकते हैं। Python के साथ sqlite3 आता है — एक file में रखा पूरा SQL engine। बड़े काम के लिए — PostgreSQL, MySQL, ORM, migrations — ज़्यादातर Python developers SQLAlchemy लेते हैं। दोनों मिलकर रोज़ के Python programs की ज़्यादातर ज़रूरतें पूरी करते हैं।

数据库就是 可以查询的有序存储。Python 自带 sqlite3 — 一个存放在单个文件里的完整 SQL 引擎。更大的工作 — PostgreSQL、MySQL、ORM、数据库迁移 — 大多数 Python 开发者会选择 SQLAlchemy。二者合起来能满足日常 Python 程序的大部分需求。

🧠
The Core Insightमूल बात核心要点

You do not have to master SQL before doing useful work. Four verbs — SELECT, INSERT, UPDATE, DELETE — plus filters (WHERE) and joins take you a very long way.

काम की चीज़ें करने से पहले SQL में माहिर होना ज़रूरी नहीं। चार क्रियाएँ — SELECT, INSERT, UPDATE, DELETE — साथ में filters (WHERE) और joins आपको बहुत दूर ले जाते हैं।

不必先精通 SQL 才能做有用的事。四个动词 — SELECT、INSERT、UPDATE、DELETE — 加上过滤(WHERE)和连接(JOIN),就能走得很远。

▶️
Six Simulators and a Quizछह Simulators और एक Quiz六个模拟器和一个小测验

Look for the interactive boxes: the SQL Injection Lab (Section 04), the Query Playground (05), the Transaction Lab (07), the JOIN Lab (08), the N+1 Query Counter (13) and the Bug Doctor (15). Every output on this page is from a real run on Python 3.13.16 with SQLite 3.45.1 and SQLAlchemy 2.1.5.

Interactive डिब्बे खोजें: SQL Injection Lab (Section 04), Query Playground (05), Transaction Lab (07), JOIN Lab (08), N+1 Query Counter (13) और Bug Doctor (15)। इस page का हर output Python 3.13.16, SQLite 3.45.1 और SQLAlchemy 2.1.5 पर असली run से है।

请留意这些互动框:SQL 注入实验室(第 04 节)、查询游乐场(05)、事务实验室(07)、JOIN 实验室(08)、N+1 查询计数器(13)和 Bug 医生(15)。本页每个输出都来自 Python 3.13.16、SQLite 3.45.1 和 SQLAlchemy 2.1.5 上的真实运行。


SectionSection第 02 节

Visual Diagram — How Your Program Reaches the Database 🔌चित्र — आपका Program Database तक कैसे पहुँचता है 🔌图解 — 程序如何到达数据库 🔌

Figure 1 — Program → Connection → Cursor → Databaseचित्र 1 — Program → Connection → Cursor → Database图 1 — 程序 → 连接 → 游标 → 数据库
Program to database pipeline"SEND MY SQL THROUGH THE PIPE""मेरा SQL PIPE से भेजो""把我的 SQL 送进管道"Your program never edits the raw file. Everything goes through the engine.आपका program raw file को कभी सीधे नहीं बदलता। सब कुछ engine से होकर जाता है।程序从不直接改动原始文件,一切都经由引擎。YOUR PYTHONआपका PYTHON你的 PYTHONprogramCONNECTIONCONNECTION连接conn = connect(...)CURSORCURSOR游标runs SQLDATABASEDATABASE数据库.db file / serverSELECT * FROM booksrows come back as tuples (or objects)rows tuples (या objects) बनकर लौटती हैं行以元组(或对象)形式返回Connection = your session · Cursor = runs one statement at a time · Database = stores the truthConnection = आपका session · Cursor = एक बार में एक statement · Database = असली data连接 = 你的会话 · 游标 = 一次执行一条语句 · 数据库 = 保存真实数据

SQL goes in; rows come back.

SQL अंदर जाता है; rows वापस आती हैं।

SQL 发出去,行返回来。


SectionSection第 03 节

The Three Building Blocks of Any Database 🧱हर Database की तीन ईंटें 🧱任何数据库的三块基石 🧱

📋
1 — Schema1 — Schema1 — 模式(Schema)
the shape of your dataआपके data का आकार数据的形状
A blueprint declared once with CREATE TABLE. It names each column, its type (INTEGER, TEXT, REAL) and its rules (PRIMARY KEY, NOT NULL).
CREATE TABLE से एक बार बताया गया नक़्शा। यह हर column का नाम, उसका type (INTEGER, TEXT, REAL) और नियम (PRIMARY KEY, NOT NULL) बताता है।
用 CREATE TABLE 一次性声明的蓝图。它规定每一列的名字、类型(INTEGER、TEXT、REAL)和约束(PRIMARY KEY、NOT NULL)。
📄
2 — Rows2 — Rows2 — 行
the actual dataअसली data实际数据
Each INSERT adds one row. Each row is a record — a customer, a book, an order — matching the columns of its table.
हर INSERT एक row जोड़ता है। हर row एक record है — एक ग्राहक, किताब, order — जो अपनी table के columns से मेल खाता है।
每次 INSERT 添加一行。每行是一条记录 — 一位顾客、一本书、一个订单 — 与所在表的列相对应。
🔍
3 — Queries3 — Queries3 — 查询
questions in SQLSQL में सवाल用 SQL 提问
SELECT … FROM … WHERE … — filter rows, join tables, add things up. Every answer you get from a database starts as a query.
SELECT … FROM … WHERE … — rows छाँटिए, tables जोड़िए, जोड़-घटाव कीजिए। Database से मिलने वाला हर जवाब एक query से शुरू होता है।
SELECT … FROM … WHERE … — 筛选行、连接表、做汇总。你从数据库得到的每个答案都始于一条查询。

Your First Database — Nothing to Installआपका पहला Database — कुछ install नहीं करना你的第一个数据库 — 无需安装

import sqlite3

# Creates the file if it doesn't exist; opens it if it does
conn = sqlite3.connect("bookstore.db")
cur  = conn.cursor()

# Schema - declared ONCE. IF NOT EXISTS makes it safe to re-run.
cur.execute("""
    CREATE TABLE IF NOT EXISTS books (
        id     INTEGER PRIMARY KEY,
        title  TEXT    NOT NULL,
        author TEXT    NOT NULL,
        price  REAL    NOT NULL,
        stock  INTEGER DEFAULT 0
    )
""")

conn.commit()      # save the schema
conn.close()       # always close when done
print("database ready")
OUTPUT
database ready $ ls -l *.db -rw-r--r-- 8192 bookstore.db
🔧
Fixed: AUTOINCREMENT Is Not Needed, and the Output Line Was Not Outputसुधार: AUTOINCREMENT की ज़रूरत नहीं, और Output वाली Line Output नहीं थी已修正:不需要 AUTOINCREMENT,原"输出"那行也不是输出

The original used INTEGER PRIMARY KEY AUTOINCREMENT. In SQLite, INTEGER PRIMARY KEY already gives each new row the next id; the SQLite docs advise against AUTOINCREMENT unless you must never reuse an id, because it adds extra work. The original output block also contained a comment ("a file … now exists") that Python never prints — above, a real ls shows the file.

मूल में INTEGER PRIMARY KEY AUTOINCREMENT था। SQLite में INTEGER PRIMARY KEY पहले से हर नई row को अगला id देता है; SQLite docs AUTOINCREMENT से बचने की सलाह देते हैं जब तक id को कभी दोबारा न लेने की ज़रूरत न हो, क्योंकि यह अतिरिक्त काम जोड़ता है। मूल output block में एक टिप्पणी ("a file … now exists") भी थी जिसे Python कभी print नहीं करता — ऊपर असली ls file दिखाता है।

原文使用了 INTEGER PRIMARY KEY AUTOINCREMENT。在 SQLite 中,INTEGER PRIMARY KEY 本身就会给每一新行分配下一个 id;SQLite 文档建议除非绝对不能复用 id,否则不要用 AUTOINCREMENT,因为它会带来额外开销。原输出框里还有一句 Python 根本不会打印的注释("a file … now exists")— 上面用真实的 ls 展示了这个文件。

💡
Why SQLite Is a SuperpowerSQLite एक Superpower क्यों है为什么 SQLite 是"超能力"

SQLite is one file. No server to run, no user accounts. Copy the .db file and you have copied the whole database. It is built into every Android and iPhone and into most web browsers, which makes it one of the most widely used databases in the world. Perfect for learning, prototypes, single-user tools and tests.

SQLite एक file है। चलाने को कोई server नहीं, कोई user accounts नहीं। .db file copy कीजिए और पूरा database copy हो गया। यह हर Android और iPhone और ज़्यादातर web browsers में बना हुआ है, इसलिए यह दुनिया के सबसे ज़्यादा इस्तेमाल होने वाले databases में से है। सीखने, prototypes, एक-user वाले tools और tests के लिए बढ़िया।

SQLite 就是 一个文件。不用运行服务器,不用用户账号。复制 .db 文件,就复制了整个数据库。它内置于每部 Android 手机和 iPhone 以及大多数浏览器中,是世界上使用最广泛的数据库之一。非常适合学习、原型、单用户工具和测试。

🔧
Fixed: "Every iPhone, Browser and Android App on Earth" Overstatedसुधार: "धरती का हर iPhone, Browser और Android App" बढ़ा-चढ़ाकर已修正:"地球上每个 iPhone、浏览器和 Android 应用"言过其实

SQLite ships inside every iPhone and Android phone and most browsers — but not every app uses it, and not every browser exposes it. The callout above keeps the true part.

SQLite हर iPhone और Android फ़ोन और ज़्यादातर browsers में आता है — पर हर app इसे इस्तेमाल नहीं करता, और हर browser इसे उपलब्ध नहीं कराता। ऊपर का callout सच्चा हिस्सा रखता है।

SQLite 内置于每部 iPhone 和 Android 手机以及大多数浏览器中 — 但并非每个应用都使用它,也不是每个浏览器都对外开放它。上面的提示保留了真实的部分。


SectionSection第 04 节

INSERT — Adding Rows ➕INSERT — Rows जोड़ना ➕INSERT — 添加行 ➕

Never put values into SQL with f-strings. That is the classic way SQL injection gets in. Use parameterized queries with ? placeholders: the values travel separately from the SQL text, so they can never be read as SQL.

f-strings से values SQL में कभी मत डालिए। SQL injection घुसने का यही पुराना रास्ता है। ? placeholders वाली parameterized queries लीजिए: values SQL text से अलग जाती हैं, इसलिए उन्हें कभी SQL की तरह पढ़ा नहीं जा सकता।

永远不要用 f-string 把值塞进 SQL。这正是 SQL 注入的经典入口。请使用带 ? 占位符的 参数化查询:值与 SQL 文本 分开 传送,因此永远不会被当作 SQL 来解读。

import sqlite3

with sqlite3.connect("bookstore.db") as conn:
    cur = conn.cursor()

    # Single insert - parameterized with ? placeholders
    cur.execute(
        "INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)",
        ("Dune", "Frank Herbert", 15.99, 12),
    )
    print("new id:", cur.lastrowid)

    # Bulk insert - one call for many rows
    rows = [
        ("1984",            "George Orwell",  9.99,  30),
        ("Brave New World", "Aldous Huxley",  11.50, 18),
        ("Foundation",      "Isaac Asimov",   13.75, 22),
        ("Neuromancer",     "William Gibson", 14.20, 8),
    ]
    cur.executemany(
        "INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)",
        rows,
    )
    print("executemany rowcount:", cur.rowcount)
    conn.commit()

    total = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0]
    print(f"books in the table: {total}")
OUTPUT
new id: 1 executemany rowcount: 4 books in the table: 5
🔧
Fixed: "cur.rowcount + 1" Only Worked by Accidentसुधार: "cur.rowcount + 1" संयोग से चला已修正:"cur.rowcount + 1" 只是碰巧正确

The original printed f"inserted {cur.rowcount + 1} books". After executemany(), rowcount is the rows of that call (4); the "+1" silently guessed the earlier single insert. Add one more single insert and the message is wrong. Count what you need directly, as above.

मूल ने f"inserted {cur.rowcount + 1} books" print किया। executemany() के बाद rowcount उसी call की rows (4) है; "+1" ने चुपचाप पहले वाले एक insert का अंदाज़ा लगाया। एक और insert जोड़िए और message ग़लत हो जाएगा। जो चाहिए उसे सीधे गिनिए, जैसा ऊपर।

原文打印 f"inserted {cur.rowcount + 1} books"。executemany() 之后,rowcount 只是 这一次 调用影响的行数(4);"+1"是在悄悄猜测前面那次单条插入。再加一条插入,消息就错了。请像上面那样直接统计你需要的数字。

⚠️
NEVER Use f-strings for SQL ValuesSQL Values के लिए f-strings कभी नहीं绝不要用 f-string 拼接 SQL 值

cur.execute(f"SELECT … WHERE name = '{name}'") is a SQL injection hole. A user who types x' OR '1'='1 as a name gets every row. Always use ? placeholders.

cur.execute(f"SELECT … WHERE name = '{name}'") एक SQL injection छेद है। नाम में x' OR '1'='1 लिखने वाले user को हर row मिल जाती है। हमेशा ? placeholders लीजिए।

cur.execute(f"SELECT … WHERE name = '{name}'") 是一个 SQL 注入漏洞。用户把名字填成 x' OR '1'='1 就能拿到所有行。始终使用 ? 占位符。

import sqlite3

conn = sqlite3.connect(":memory:")            # a throw-away database in RAM
conn.execute("CREATE TABLE users (name TEXT, secret TEXT)")
conn.executemany("INSERT INTO users VALUES (?, ?)",
                 [("Ada", "ada-pin-1111"), ("Grace", "grace-pin-2222")])

name = "x' OR '1'='1"                         # typed into a login form

# WRONG - the input becomes part of the SQL text
sql = f"SELECT name, secret FROM users WHERE name = '{name}'"
print("SQL sent :", sql)
print("f-string :", conn.execute(sql).fetchall())

# RIGHT - the input is sent separately, as a value
print("? param  :", conn.execute("SELECT name, secret FROM users WHERE name = ?",
                                 (name,)).fetchall())
OUTPUT
SQL sent : SELECT name, secret FROM users WHERE name = 'x' OR '1'='1' f-string : [('Ada', 'ada-pin-1111'), ('Grace', 'grace-pin-2222')] ? param : []

🛡️ Simulator: SQL Injection Lab🛡️ Simulator: SQL Injection Lab🛡️ 模拟器:SQL 注入实验室

Pick what a user types into a login box. You see the SQL an f-string builds (the user's text is in red) and what really came back — next to the safe ? version. Every result is a real run.

चुनिए user login box में क्या लिखता है। आप f-string से बना SQL देखते हैं (user का text लाल में) और असल में क्या लौटा — साथ में सुरक्षित ? version। हर result असली run है।

选择用户在登录框里输入的内容。你会看到 f-string 拼出的 SQL(用户的文字标红)以及真实返回的结果 — 旁边是安全的 ? 版本。每个结果都来自真实运行。

SQL Injection LabSQL Injection LabSQL 注入实验室5 inputs · real runs5 inputs · असली runs5 个输入 · 真实运行
name =
SQL built by the f-stringf-string से बना SQLf-string 拼出的 SQL
❌ f-string result❌ f-string result❌ f-string 结果
✅ ? placeholder result✅ ? placeholder result✅ ? 占位符结果

SectionSection第 05 节

SELECT — Asking Questions ❓SELECT — सवाल पूछना ❓SELECT — 提出问题 ❓

import sqlite3

with sqlite3.connect("bookstore.db") as conn:
    # Row factory: rows behave like dicts as well as tuples
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()

    # All books
    cur.execute("SELECT id, title, author, price FROM books")
    for row in cur.fetchall():
        print(f"#{row['id']}  {row['title']:20s} £{row['price']:.2f}")

    # With a WHERE filter - parameterized (safe against injection)
    cur.execute("SELECT * FROM books WHERE price < ? AND stock > ?", (14, 0))
    affordable = cur.fetchall()
    print("affordable:", [r["title"] for r in affordable])

    # Aggregate query
    cur.execute("SELECT COUNT(*), AVG(price), MAX(stock) FROM books")
    count, avg_price, max_stock = cur.fetchone()
    print(f"total={count}  avg=£{avg_price:.2f}  most-stocked={max_stock}")
OUTPUT
#1 Dune £15.99 #2 1984 £9.99 #3 Brave New World £11.50 #4 Foundation £13.75 #5 Neuromancer £14.20 affordable: ['1984', 'Brave New World', 'Foundation'] total=5 avg=£13.09 most-stocked=30
📌
fetchone vs fetchall vs Iterationfetchone बनाम fetchall बनाम Iterationfetchone vs fetchall vs 迭代

fetchone() returns the next row (or None). fetchall() loads every remaining row into memory — fine for small results, risky for huge ones. For big results, loop over the cursor: for row in cur: reads rows bit by bit.

fetchone() अगली row लौटाता है (या None)। fetchall() बची हर row memory में ले आता है — छोटे results के लिए ठीक, बहुत बड़े के लिए जोखिम। बड़े results के लिए cursor पर loop चलाइए: for row in cur: rows थोड़ा-थोड़ा पढ़ता है।

fetchone() 返回下一行(或 None)。fetchall() 把剩下的所有行都读进内存 — 结果少时没问题,结果巨大时有风险。结果很多时,直接遍历游标:for row in cur: 会一点点地读取行。

🔎 Simulator: Query Playground🔎 Simulator: Query Playground🔎 模拟器:查询游乐场

Click a query. Rows that match the WHERE stay bright in the table on the left; the result comes in on the right. All results came from running the query on the real bookstore.db built above.

एक query पर click कीजिए। WHERE से मेल खाने वाली rows बाईं table में चमकती रहती हैं; result दाईं ओर आता है। हर result ऊपर बने असली bookstore.db पर query चलाकर आया है।

点击一条查询。符合 WHERE 条件的行在左侧表中保持高亮;结果出现在右侧。所有结果都来自在上面建好的真实 bookstore.db 上运行查询。

Query PlaygroundQuery Playground查询游乐场8 real queries on bookstore.dbbookstore.db पर 8 असली queries在 bookstore.db 上的 8 条真实查询
table bookstable books表 books
resultresult结果

SectionSection第 06 节

UPDATE and DELETE — Changing Data ✏️UPDATE और DELETE — Data बदलना ✏️UPDATE 与 DELETE — 修改数据 ✏️

import sqlite3

with sqlite3.connect("bookstore.db") as conn:
    cur = conn.cursor()

    # Reduce stock when a book is sold - the WHERE clause is essential!
    cur.execute("UPDATE books SET stock = stock - 1 WHERE id = ?", (1,))
    print(f"rows updated: {cur.rowcount}")

    # Price rise on cheap books
    cur.execute("UPDATE books SET price = ROUND(price * 1.1, 2) WHERE price < ?", (12,))
    print(f"prices raised: {cur.rowcount}")

    # Delete out-of-stock, cheap books
    cur.execute("DELETE FROM books WHERE stock = 0 AND price < ?", (10,))
    print(f"rows deleted: {cur.rowcount}")

    conn.commit()
    for row in conn.execute("SELECT id, title, price, stock FROM books"):
        print(row)
OUTPUT
rows updated: 1 prices raised: 2 rows deleted: 0 (1, 'Dune', 15.99, 11) (2, '1984', 10.99, 30) (3, 'Brave New World', 12.65, 18) (4, 'Foundation', 13.75, 22) (5, 'Neuromancer', 14.2, 8)

cur.rowcount tells you how many rows each statement touched. Here the DELETE matched nothing: no book had stock = 0. Checking rowcount is a cheap way to catch a WHERE that matched less (or more) than you expected.

cur.rowcount बताता है कि हर statement ने कितनी rows छुईं। यहाँ DELETE किसी से मेल नहीं खाया: किसी किताब का stock = 0 नहीं था। rowcount जाँचना उस WHERE को पकड़ने का सस्ता तरीका है जो उम्मीद से कम (या ज़्यादा) rows से मेल खाए।

cur.rowcount 告诉你每条语句影响了多少行。这里的 DELETE 一行也没匹配到:没有哪本书 stock = 0。检查 rowcount 是发现 WHERE 匹配得比预期少(或多)的廉价办法。

🔧
Fixed: Rounding Added to the Price Riseसुधार: Price बढ़ाने में Rounding जोड़ी गई已修正:涨价时加上了四舍五入

The original did price = price * 1.1. With floating point, 9.99 × 1.1 is stored as 10.989000000000001 — prices with long tails. We used ROUND(price * 1.1, 2). For real money, store whole cents as INTEGER.

मूल में price = price * 1.1 था। Floating point में 9.99 × 1.1, 10.989000000000001 बनकर रखा जाता है — लंबी पूँछ वाले दाम। हमने ROUND(price * 1.1, 2) लिया। असली पैसों के लिए पूरे पैसे (cents) INTEGER में रखिए।

原文写的是 price = price * 1.1。由于浮点数,9.99 × 1.1 会被存成 10.989000000000001 — 价格带着长尾巴。我们改用 ROUND(price * 1.1, 2)。真正涉及金钱时,应把"分"作为 INTEGER 存储。

🔥
UPDATE / DELETE Without WHERE = DisasterWHERE के बिना UPDATE / DELETE = तबाही没有 WHERE 的 UPDATE / DELETE = 灾难

DELETE FROM books with no WHERE deletes every row. UPDATE books SET price = 0 makes every book free. Run a SELECT with the same filter first to see exactly what will change. (The Bug Doctor in Section 15 shows this with a real run.)

WHERE के बिना DELETE FROM books हर row मिटा देता है। UPDATE books SET price = 0 हर किताब मुफ़्त कर देता है। पहले उसी filter से SELECT चलाइए ताकि ठीक-ठीक दिखे क्या बदलेगा। (Section 15 का Bug Doctor इसे असली run से दिखाता है।)

不带 WHERE 的 DELETE FROM books 会删除 所有行。UPDATE books SET price = 0 会让每本书免费。先用相同的条件跑一次 SELECT,看清到底会改动哪些行。(第 15 节的 Bug 医生用真实运行展示了这一点。)

Does "with sqlite3.connect()" close the connection?क्या "with sqlite3.connect()" connection बंद करता है?"with sqlite3.connect()" 会关闭连接吗?

import sqlite3
from contextlib import closing

with sqlite3.connect("bookstore.db") as conn:
    conn.execute("UPDATE books SET stock = stock + 1 WHERE id = 1")
# the with-block COMMITTED... but did it close the connection?
print("still open:", conn.execute("SELECT COUNT(*) FROM books").fetchone())

# closing() really closes it; the inner "with conn" handles the transaction
with closing(sqlite3.connect("bookstore.db")) as conn:
    with conn:
        conn.execute("UPDATE books SET stock = stock + 1 WHERE id = 1")
try:
    conn.execute("SELECT 1")
except sqlite3.ProgrammingError as e:
    print("after closing():", e)
OUTPUT
still open: (5,) after closing(): Cannot operate on a closed database.
🔧
Fixed: The sqlite3 with-Block Does NOT Close the Connectionसुधार: sqlite3 का with-Block Connection बंद नहीं करता已修正:sqlite3 的 with 语句块不会关闭连接

The original pitfalls table and Rule 2 say with sqlite3.connect(...) closes the connection. It does not: for a sqlite3 connection, with only commits (or rolls back on an error). The run above shows the connection still working after the block. To close it as well, wrap it in contextlib.closing(), or call conn.close().

मूल pitfalls table और नियम 2 कहते हैं कि with sqlite3.connect(...) connection बंद करता है। ऐसा नहीं है: sqlite3 connection के लिए with सिर्फ़ commit करता है (या error पर rollback)। ऊपर का run दिखाता है कि block के बाद भी connection चल रहा है। उसे बंद भी करना हो तो contextlib.closing() में लपेटिए, या conn.close() बुलाइए।

原陷阱表和规则 2 说 with sqlite3.connect(...) 会关闭连接。其实不会:对 sqlite3 连接来说,with 只负责 提交(出错时回滚)。上面的运行显示语句块结束后连接仍可使用。如果还要关闭,就用 contextlib.closing() 包起来,或调用 conn.close()。


SectionSection第 07 节

Transactions — All-or-Nothing Changes 🔒Transactions — सब या कुछ नहीं 🔒事务 — 要么全成,要么全不成 🔒

A transaction is a group of changes treated as one unit. Either every change lands, or none does. Classic example: moving money from one account to another must change both sides — or neither.

Transaction बदलावों का ऐसा समूह है जिसे एक इकाई माना जाता है। या तो हर बदलाव होता है, या कोई नहीं। पुराना उदाहरण: एक खाते से दूसरे में पैसे भेजने पर दोनों तरफ़ बदलाव होना चाहिए — या किसी भी तरफ़ नहीं।

事务 是被当作一个整体的一组修改。要么全部生效,要么一个都不生效。经典例子:从一个账户转钱到另一个账户,两边都必须改动 — 或者都不改。

Figure 2 — The Transaction Lifecycleचित्र 2 — Transaction का जीवन-चक्र图 2 — 事务的生命周期
Transaction lifecycleBEGINBEGIN开始open transactiontransaction खोलें开启事务DO WORKकाम करें执行操作UPDATE ×2successसफल成功errorerror出错COMMITCOMMIT提交changes savedबदलाव save修改被保存ROLLBACKROLLBACK回滚every change undoneहर बदलाव वापस所有修改撤销DONEपूरा完成consistentसंगत数据一致Never half-done: the whole group commits, or the whole group rolls back.कभी आधा नहीं: पूरा समूह commit होता है, या पूरा rollback।绝不会只做一半:整组提交,或者整组回滚。

Both paths end with a consistent database.

दोनों रास्ते संगत database पर ख़त्म होते हैं।

两条路都以一致的数据库结束。

import sqlite3

def setup(path="bank.db"):
    with sqlite3.connect(path) as conn:
        conn.executescript("""
            DROP TABLE IF EXISTS accounts;
            CREATE TABLE accounts (
                id      INTEGER PRIMARY KEY,
                owner   TEXT NOT NULL,
                balance REAL NOT NULL CHECK (balance >= 0)   -- no overdrafts
            );
            INSERT INTO accounts VALUES (1, 'Ada', 500), (2, 'Grace', 100);
        """)

def balances():
    with sqlite3.connect("bank.db") as conn:
        return dict(conn.execute("SELECT owner, balance FROM accounts"))

def transfer(from_id: int, to_id: int, amount: float) -> None:
    conn = sqlite3.connect("bank.db")
    try:
        cur = conn.cursor()
        cur.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?",
                    (amount, to_id))
        if cur.rowcount != 1:
            raise ValueError(f"no account {to_id}")
        cur.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?",
                    (amount, from_id))
        conn.commit()               # both updates land together
    except Exception:
        conn.rollback()             # any error -> undo BOTH updates
        raise
    finally:
        conn.close()

setup()
print("start          :", balances())
transfer(1, 2, 100)
print("after 100      :", balances())
for args in [(2, 1, 999), (1, 7, 50)]:
    try:
        transfer(*args)
    except Exception as e:
        print(f"transfer{args} failed: {type(e).__name__}: {e}")
    print("               :", balances())
OUTPUT
start : {'Ada': 500.0, 'Grace': 100.0} after 100 : {'Ada': 400.0, 'Grace': 200.0} transfer(2, 1, 999) failed: IntegrityError: CHECK constraint failed: balance >= 0 : {'Ada': 400.0, 'Grace': 200.0} transfer(1, 7, 50) failed: ValueError: no account 7 : {'Ada': 400.0, 'Grace': 200.0}
🔧
Fixed: The Original Example Could Not Run — and Had a Silent Holeसुधार: मूल Example चल नहीं सकता था — और उसमें एक चुप छेद था已修正:原示例无法运行 — 还藏着一个无声的漏洞

The original used a bank.db with an accounts table that was never created, so it failed with "no such table". We added the setup. More important: an UPDATE whose WHERE matches nothing is not an error — it just changes 0 rows. Sending money to account 7 would take it from Ada and give it to no one, and the transaction would commit happily. Check rowcount and raise, as above. The CHECK (balance >= 0) rule makes the database itself refuse overdrafts.

मूल ने bank.db की accounts table इस्तेमाल की जो कभी बनाई ही नहीं गई, इसलिए वह "no such table" से fail होता। हमने setup जोड़ा। ज़्यादा ज़रूरी: जिस UPDATE का WHERE किसी से मेल न खाए, वह error नहीं है — बस 0 rows बदलता है। Account 7 को पैसे भेजने पर Ada से पैसे कटते और किसी को नहीं मिलते, और transaction ख़ुशी से commit हो जाता। ऊपर की तरह rowcount जाँचिए और raise कीजिए। CHECK (balance >= 0) नियम database से ही overdraft मना करवाता है।

原文用了 bank.db 里一个从未创建的 accounts 表,所以会因"no such table"而失败。我们补上了建表步骤。更重要的是:WHERE 一行都没匹配到的 UPDATE 不算错误 — 只是改动了 0 行。转账给 7 号账户,会从 Ada 那里扣钱却谁也没收到,而事务照样愉快地提交。请像上面那样检查 rowcount 并抛出异常。CHECK (balance >= 0) 规则让数据库自己拒绝透支。

🏦 Simulator: Transaction Lab🏦 Simulator: Transaction Lab🏦 模拟器:事务实验室

Six transfers, each really run against a fresh bank.db. Watch the statements, then compare the balances. The total money in the bank must never change.

छह transfers, हर एक नए bank.db पर सच में चलाया गया। Statements देखिए, फिर balances मिलाइए। बैंक में कुल पैसा कभी नहीं बदलना चाहिए।

六次转账,每次都在全新的 bank.db 上真实运行。观察执行的语句,再比较余额。银行里的总金额绝不应该改变。

Transaction LabTransaction Lab事务实验室6 real runs6 असली runs6 次真实运行
beforeपहले之前
afterबाद में之后

SectionSection第 08 节

Multiple Tables — Foreign Keys and JOINs 🔗कई Tables — Foreign Keys और JOINs 🔗多张表 — 外键与 JOIN 🔗

Real data has relationships. A customer places orders; an order names a book. We keep them in separate tables and connect them with a foreign key: a column that holds the id of a row in another table.

असली data में रिश्ते होते हैं। ग्राहक orders देता है; order में किताब का नाम होता है। हम उन्हें अलग tables में रखते हैं और foreign key से जोड़ते हैं: ऐसा column जो दूसरी table की किसी row का id रखता है।

真实数据之间有关系。顾客下订单,订单写着书名。我们把它们放在不同的表里,用 外键 连接:一个保存另一张表中某行 id 的列。

import sqlite3

with sqlite3.connect("shop.db") as conn:
    conn.execute("PRAGMA foreign_keys = ON")   # enforce FKs (SQLite has them OFF by default)

    conn.executescript("""
        CREATE TABLE IF NOT EXISTS customers (
            id    INTEGER PRIMARY KEY,
            name  TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL
        );
        CREATE TABLE IF NOT EXISTS orders (
            id          INTEGER PRIMARY KEY,
            customer_id INTEGER NOT NULL,
            book_title  TEXT    NOT NULL,
            qty         INTEGER NOT NULL,
            FOREIGN KEY (customer_id) REFERENCES customers(id)
        );
    """)

    # Insert customers
    conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
                 (1, "Ada", "ada@example.com"))
    conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
                 (2, "Grace", "grace@example.com"))

    # Insert orders that reference customers
    conn.executemany("INSERT INTO orders (customer_id, book_title, qty) VALUES (?, ?, ?)",
                     [(1, "Dune", 2), (1, "1984", 1), (2, "Foundation", 3)])

    # JOIN - combine rows from both tables
    rows = conn.execute("""
        SELECT c.name, o.book_title, o.qty
        FROM   orders o
        JOIN   customers c ON c.id = o.customer_id
        ORDER BY c.name, o.id
    """).fetchall()

    for name, title, qty in rows:
        print(f"{name:8s}  {title:12s}  x{qty}")
OUTPUT
Ada Dune x2 Ada 1984 x1 Grace Foundation x3

Run it a second time…इसे दूसरी बार चलाइए…再运行一次…

import sqlite3

with sqlite3.connect("shop.db") as conn:
    conn.execute("PRAGMA foreign_keys = ON")   # enforce FKs (SQLite has them OFF by default)

    conn.executescript("""
        CREATE TABLE IF NOT EXISTS customers (
            id    INTEGER PRIMARY KEY,
            name  TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL
        );
        CREATE TABLE IF NOT EXISTS orders (
            id          INTEGER PRIMARY KEY,
            customer_id INTEGER NOT NULL,
            book_title  TEXT    NOT NULL,
            qty         INTEGER NOT NULL,
            FOREIGN KEY (customer_id) REFERENCES customers(id)
        );
    """)

    # Insert customers
    conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
                 (1, "Ada", "ada@example.com"))
    conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
                 (2, "Grace", "grace@example.com"))

    # Insert orders that reference customers
    conn.executemany("INSERT INTO orders (customer_id, book_title, qty) VALUES (?, ?, ?)",
                     [(1, "Dune", 2), (1, "1984", 1), (2, "Foundation", 3)])

    # JOIN - combine rows from both tables
    rows = conn.execute("""
        SELECT c.name, o.book_title, o.qty
        FROM   orders o
        JOIN   customers c ON c.id = o.customer_id
        ORDER BY c.name, o.id
    """).fetchall()

    for name, title, qty in rows:
        print(f"{name:8s}  {title:12s}  x{qty}")
OUTPUT
Ada Dune x2 Ada 1984 x1 Ada Dune x2 Ada 1984 x1 Grace Foundation x3 Grace Foundation x3
🔧
Fixed: Running the Example Twice Duplicates Every Orderसुधार: Example दो बार चलाने पर हर Order दोहरा जाता है已修正:把示例运行两次会让每个订单重复

The tables use IF NOT EXISTS and the customers use INSERT OR IGNORE, so they are safe to re-run — but the orders are a plain INSERT, so every run adds three more. Seed data should be inserted once (check first, or give the rows fixed ids with INSERT OR IGNORE). We also added o.id to ORDER BY: without it, the order of Ada's two rows is not guaranteed.

Tables IF NOT EXISTS और customers INSERT OR IGNORE लेते हैं, इसलिए दोबारा चलाना सुरक्षित है — पर orders सादा INSERT हैं, इसलिए हर run तीन और जोड़ता है। Seed data एक बार डालना चाहिए (पहले जाँचिए, या rows को INSERT OR IGNORE के साथ तय ids दीजिए)। हमने ORDER BY में o.id भी जोड़ा: उसके बिना Ada की दो rows का क्रम पक्का नहीं है।

表使用了 IF NOT EXISTS,顾客使用了 INSERT OR IGNORE,所以可以安全重跑 — 但订单是普通的 INSERT,每运行一次就多三条。初始数据应只插入一次(先检查,或用 INSERT OR IGNORE 给行固定 id)。我们还在 ORDER BY 中加了 o.id:没有它,Ada 那两行的顺序是不确定的。

Foreign keys are OFF by default in SQLiteSQLite में Foreign keys default रूप से OFF हैंSQLite 默认关闭外键约束

import sqlite3

for fk in ("OFF", "ON"):
    conn = sqlite3.connect(":memory:")
    conn.execute(f"PRAGMA foreign_keys = {fk}")
    conn.executescript("""
        CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
        CREATE TABLE orders (id INTEGER PRIMARY KEY,
                             customer_id INTEGER REFERENCES customers(id));
        INSERT INTO customers VALUES (1, 'Ada');
    """)
    try:
        conn.execute("INSERT INTO orders (customer_id) VALUES (99)")   # no customer 99!
        print(f"foreign_keys={fk}: orphan order saved ->",
              conn.execute("SELECT * FROM orders").fetchall())
    except sqlite3.IntegrityError as e:
        print(f"foreign_keys={fk}: IntegrityError: {e}")
    conn.close()
OUTPUT
foreign_keys=OFF: orphan order saved -> [(1, 99)] foreign_keys=ON: IntegrityError: FOREIGN KEY constraint failed

With foreign_keys OFF, SQLite happily saved an order for customer 99, who does not exist. Turn it ON for every connection — it is a per-connection setting, not saved in the file.

foreign_keys OFF होने पर SQLite ने ग्राहक 99 का order ख़ुशी से save कर लिया, जो मौजूद ही नहीं। इसे हर connection के लिए ON कीजिए — यह हर connection की setting है, file में save नहीं होती।

foreign_keys 关闭时,SQLite 照样为并不存在的 99 号顾客保存了订单。要为 每一个 连接都打开它 — 这是连接级的设置,不会保存在文件里。

🔗 Simulator: JOIN Lab🔗 Simulator: JOIN Lab🔗 模拟器:JOIN 实验室

Three customers, three orders — and Linus has no orders. Pick a query and watch which rows get matched. All results are from real queries.

तीन ग्राहक, तीन orders — और Linus का कोई order नहीं। एक query चुनिए और देखिए कौन-सी rows मेल खाती हैं। सारे results असली queries से हैं।

三位顾客,三个订单 — Linus 没有订单。选一条查询,看看哪些行被匹配。所有结果都来自真实查询。

JOIN LabJOIN LabJOIN 实验室4 real queries4 असली queries4 条真实查询
resultresult结果

SectionSection第 09 节

Enter SQLAlchemy — The Toolkit for Bigger Apps 🧰SQLAlchemy — बड़े Apps का Toolkit 🧰走进 SQLAlchemy — 大型应用的工具包 🧰

sqlite3 is perfect for a single script. Bigger applications often need PostgreSQL or MySQL support, connection pooling, schema migrations (with Alembic), an object mapper and a query builder. That is SQLAlchemy — first released in 2006 and today one of the most widely used database toolkits in Python. Flask and FastAPI projects commonly use it (Flask-SQLAlchemy, SQLModel); Django is the main exception, with its own ORM.

sqlite3 एक script के लिए बढ़िया है। बड़े applications को अक्सर PostgreSQL या MySQL support, connection pooling, schema migrations (Alembic से), object mapper और query builder चाहिए। यही SQLAlchemy है — 2006 में पहली बार आया और आज Python के सबसे ज़्यादा इस्तेमाल होने वाले database toolkits में से एक। Flask और FastAPI projects आम तौर पर इसे लेते हैं (Flask-SQLAlchemy, SQLModel); Django मुख्य अपवाद है, जिसका अपना ORM है।

sqlite3 非常适合单个脚本。更大的应用通常需要 PostgreSQL 或 MySQL 支持、连接池、数据库迁移(配合 Alembic)、对象映射器和查询构建器。这就是 SQLAlchemy — 2006 年首次发布,如今是 Python 中使用最广泛的数据库工具包之一。Flask 和 FastAPI 项目常用它(Flask-SQLAlchemy、SQLModel);Django 是主要例外,它有自己的 ORM。

🔧
Fixed: SQLAlchemy Is Not "the Backend of Every Major Framework"सुधार: SQLAlchemy "हर बड़े Framework का Backend" नहीं है已修正:SQLAlchemy 并非"所有主流框架的后端"

The original said SQLAlchemy is "the backend of every major Python framework (FastAPI, Flask …)". FastAPI and Flask do not include a database layer at all; many of their projects choose SQLAlchemy. Django uses its own ORM. The comparison table below is also softened: switching databases is often small but not always "one string", and editors autocomplete column names — they do not make queries fully type-safe.

मूल में कहा गया कि SQLAlchemy "हर बड़े Python framework (FastAPI, Flask …) का backend" है। FastAPI और Flask में database layer है ही नहीं; उनके कई projects SQLAlchemy चुनते हैं। Django अपना ORM लेता है। नीचे की तुलना table भी नरम की गई है: database बदलना अक्सर छोटा काम है पर हमेशा "एक string" नहीं, और editors column नामों का autocomplete करते हैं — वे queries को पूरी तरह type-safe नहीं बनाते।

原文说 SQLAlchemy 是"所有主流 Python 框架(FastAPI、Flask …)的后端"。FastAPI 和 Flask 本身根本不带数据库层;只是它们的许多项目 选择 了 SQLAlchemy。Django 用的是自己的 ORM。下面的对比表也做了修正:切换数据库通常改动不大,但并不总是"只改一个字符串";编辑器能自动补全列名 — 但这并不能让查询做到完全类型安全。

📜 Raw SQL (sqlite3)Raw SQL (sqlite3)原生 SQL(sqlite3)
Aspectपहलू方面Behaviourव्यवहार表现
QueryQuery查询Hand-written SQL stringsहाथ से लिखी SQL strings手写 SQL 字符串
TyposTypos拼写错误Found when the SQL runsSQL चलने पर पता चलते हैंSQL 执行时才发现
Switch databasesDatabase बदलना切换数据库Rewrite queries that use SQLite-only featuresSQLite-विशेष features वाली queries दोबारा लिखें重写使用了 SQLite 特有功能的查询
RowsRows行Tuples / sqlite3.RowTuples / sqlite3.Row元组 / sqlite3.Row
🧰 SQLAlchemySQLAlchemySQLAlchemy
Aspectपहलू方面Behaviourव्यवहार表现
QueryQuery查询Python expressions (Core) or classes (ORM)Python expressions (Core) या classes (ORM)Python 表达式(Core)或类(ORM)
TyposTypos拼写错误A wrong column name fails in Python, before SQL is sent; editors autocompleteग़लत column नाम SQL भेजने से पहले Python में fail; editors autocomplete करते हैं列名写错会在发送 SQL 之前就在 Python 中报错;编辑器可自动补全
Switch databasesDatabase बदलना切换数据库Change the URL — plus fix any database-specific SQLURL बदलिए — और database-विशेष SQL ठीक कीजिए改 URL — 再修正与特定数据库相关的 SQL
RowsRows行Row objects, or your own Python classesRow objects, या आपकी अपनी Python classesRow 对象,或你自己的 Python 类

SetupSetup安装

pip install sqlalchemy

SectionSection第 10 节

SQLAlchemy Core — SQL, but Composable 🧩SQLAlchemy Core — SQL, पर जोड़ने लायक 🧩SQLAlchemy Core — 可组合的 SQL 🧩

The Core layer lets you build SQL as Python expressions. It is still SQL — you can even print it — but values are always sent as parameters, and a wrong column name fails at once.

Core layer आपको SQL को Python expressions की तरह बनाने देती है। यह अब भी SQL है — आप इसे print भी कर सकते हैं — पर values हमेशा parameters की तरह जाती हैं, और ग़लत column नाम तुरंत fail होता है।

Core 层让你用 Python 表达式构建 SQL。它仍然是 SQL — 甚至可以打印出来 — 但值总是作为参数发送,列名写错会立刻报错。

from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Float, select

# The URL tells SQLAlchemy which database to use:
#   sqlite:///bookstore_core.db         <- SQLite file
#   postgresql+psycopg://user:pw@host/db
#   mysql+pymysql://user:pw@host/db
engine = create_engine("sqlite:///bookstore_core.db", echo=False)

metadata = MetaData()

books = Table(
    "books", metadata,
    Column("id",     Integer, primary_key=True),
    Column("title",  String,  nullable=False),
    Column("author", String,  nullable=False),
    Column("price",  Float,   nullable=False),
    Column("stock",  Integer, default=0),
)

metadata.create_all(engine)   # CREATE TABLE only for tables that don't exist yet

# Insert
with engine.begin() as conn:                # begin() commits on exit
    conn.execute(books.insert(), [
        {"title": "Dune",       "author": "Frank Herbert", "price": 15.99, "stock": 12},
        {"title": "1984",       "author": "George Orwell", "price": 9.99,  "stock": 30},
        {"title": "Foundation", "author": "Isaac Asimov",  "price": 13.75, "stock": 22},
    ])

# SELECT - as a Python expression
with engine.connect() as conn:
    stmt = (select(books.c.title, books.c.price)
            .where(books.c.price < 14)
            .order_by(books.c.price))
    print(stmt)                              # the SQL SQLAlchemy builds
    print()
    for row in conn.execute(stmt):
        print(row.title, row.price)

# A column-name typo fails in Python, before any SQL is sent
try:
    select(books.c.titel)
except AttributeError as e:
    print("\ntypo caught:", type(e).__name__, e)
OUTPUT
SELECT books.title, books.price FROM books WHERE books.price < :price_1 ORDER BY books.price 1984 9.99 Foundation 13.75 typo caught: AttributeError titel

Look at the printed SQL: books.price < :price_1. The number 14 was not pasted into the text — it travels as a parameter, just like ? in sqlite3. We also gave this example its own file, bookstore_core.db, so it does not add duplicate books to the bookstore.db from Sections 03–06.

Print हुआ SQL देखिए: books.price < :price_1। संख्या 14 text में चिपकाई नहीं गई — यह parameter की तरह जाती है, बिल्कुल sqlite3 के ? जैसे। हमने इस example को अपनी file bookstore_core.db भी दी, ताकि यह Sections 03–06 के bookstore.db में दोहरी किताबें न जोड़े।

看打印出来的 SQL:books.price < :price_1。数字 14 没有被粘贴进文本 — 它作为参数传送,就像 sqlite3 中的 ?。我们还给这个示例单独用了文件 bookstore_core.db,这样它就不会往第 03–06 节的 bookstore.db 里添加重复的书。

📈
One URL, Many Databases — Mostlyएक URL, कई Databases — ज़्यादातर一个 URL,多种数据库 — 大体如此

The same Core and ORM code runs on SQLite, PostgreSQL, MySQL and others — you change the create_engine("…") URL and install the driver. Differences remain (types, some functions, how strict each database is), so test on the database you will really use.

वही Core और ORM code SQLite, PostgreSQL, MySQL और दूसरों पर चलता है — आप create_engine("…") URL बदलते हैं और driver install करते हैं। कुछ अंतर रहते हैं (types, कुछ functions, हर database कितना सख़्त है), इसलिए उसी database पर test कीजिए जिसे सच में इस्तेमाल करेंगे।

同一套 Core 和 ORM 代码可以运行在 SQLite、PostgreSQL、MySQL 等数据库上 — 只需更改 create_engine("…") 的 URL 并安装驱动。差异依然存在(类型、部分函数、各数据库的严格程度),所以要在真正要用的数据库上测试。


SectionSection第 11 节

SQLAlchemy ORM — Rows Become Python Objects 🧬SQLAlchemy ORM — Rows बनती हैं Python Objects 🧬SQLAlchemy ORM — 行变成 Python 对象 🧬

The ORM (Object-Relational Mapper) lets you treat rows as normal Python objects. Each class is a table. Each instance is a row. The Session tracks your changes and writes them to the database when you commit.

ORM (Object-Relational Mapper) आपको rows को सामान्य Python objects की तरह इस्तेमाल करने देता है। हर class एक table है। हर instance एक row है। Session आपके बदलाव याद रखता है और commit करने पर database में लिखता है।

ORM(对象关系映射器)让你把行当作普通的 Python 对象来使用。每个类是一张表,每个实例是一行。Session 会跟踪你的修改,并在提交时写入数据库。

Figure 3 — ORM Object Mappingचित्र 3 — ORM Object Mapping图 3 — ORM 对象映射
ORM mappingPYTHON CLASSPYTHON CLASSPYTHON 类class Book(Base):__tablename__ = "books"id: Mapped[int]title: Mapped[str]author: Mapped[str]price: Mapped[float]stock: Mapped[int]SQL TABLE booksSQL TABLE booksSQL 表 booksidtitleauthorpricestock1DuneHerbert15.991221984Orwell9.99303Found…Asimov13.7522Book(title="Dune", …)one instance = one rowएक instance = एक row一个实例 = 一行

Each attribute becomes a column; each instance becomes a row.

हर attribute एक column बनता है; हर instance एक row।

每个属性成为一列;每个实例成为一行。

from sqlalchemy import create_engine, String, Integer, Float, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

# 1) Base class for all models
class Base(DeclarativeBase):
    pass

# 2) A model - one class = one table
class Book(Base):
    __tablename__ = "books"

    id:     Mapped[int]   = mapped_column(Integer, primary_key=True)
    title:  Mapped[str]   = mapped_column(String(200), nullable=False)
    author: Mapped[str]   = mapped_column(String(100), nullable=False)
    price:  Mapped[float] = mapped_column(Float, nullable=False)
    stock:  Mapped[int]   = mapped_column(Integer, default=0)

    def __repr__(self) -> str:
        return f"Book({self.title!r}, £{self.price})"

# 3) Create the engine and tables
engine = create_engine("sqlite:///bookstore_orm.db")
Base.metadata.create_all(engine)

# 4) Insert - just create Python objects
with Session(engine) as session:
    session.add_all([
        Book(title="Dune",       author="Frank Herbert", price=15.99, stock=12),
        Book(title="1984",       author="George Orwell", price=9.99,  stock=30),
        Book(title="Foundation", author="Isaac Asimov",  price=13.75, stock=22),
    ])
    session.commit()

# 5) Query - Python style
with Session(engine) as session:
    stmt = select(Book).where(Book.price < 14).order_by(Book.price)
    for book in session.scalars(stmt):
        print(book)                      # uses __repr__

    # Update - change the object, then commit
    dune = session.scalar(select(Book).where(Book.title == "Dune"))
    dune.stock -= 1                      # plain Python attribute assignment
    session.commit()                     # SQL UPDATE runs automatically
    print("Dune stock now:", dune.stock)
OUTPUT
Book('1984', £9.99) Book('Foundation', £13.75) Dune stock now: 11
ℹ️
Its Own File Againफिर से अपनी File同样使用独立文件

The original used bookstore.db here too. If you ran Sections 03–06 and 10 first, this query would also return the earlier copies of 1984, Foundation and Brave New World, and the output would not match. A separate file (bookstore_orm.db) keeps the example honest.

मूल ने यहाँ भी bookstore.db लिया था। अगर आपने पहले Sections 03–06 और 10 चलाए हों, तो यह query 1984, Foundation और Brave New World की पहले वाली copies भी लौटाती, और output मेल नहीं खाता। अलग file (bookstore_orm.db) example को सच्चा रखती है।

原文这里也用了 bookstore.db。如果你先运行过第 03–06 节和第 10 节,这条查询还会返回之前的 1984、Foundation 和 Brave New World 副本,输出就对不上了。使用单独的文件(bookstore_orm.db)让示例保持真实。


SectionSection第 12 节

Relationships — One-to-Many Made Easy 👨‍👩‍👧Relationships — One-to-Many आसान 👨‍👩‍👧关系 — 轻松实现一对多 👨‍👩‍👧

Two related tables — users and their posts — connect naturally in the ORM. You can move in both directions: user.posts gives a list; post.author gives the user.

दो जुड़ी tables — users और उनके posts — ORM में स्वाभाविक रूप से जुड़ती हैं। आप दोनों दिशाओं में जा सकते हैं: user.posts एक list देता है; post.author user देता है।

两张相关的表 — 用户和他们的帖子 — 在 ORM 中自然地连接起来。你可以双向访问:user.posts 给出列表;post.author 给出用户。

Figure 4 — One-to-Many Relationshipचित्र 4 — One-to-Many Relationship图 4 — 一对多关系
One-to-manyUser (id=1)User (id=1)User(id=1)name: "Ada"email: ada@x.ioposts: [ … ]Post (id=101)Post (id=101)Post(id=101)title: "Hello, world"author_id: 1 ← FKPost (id=102)Post (id=102)Post(id=102)title: "Learning SQLAlchemy"author_id: 1 ← FKPost (id=103)Post (id=103)Post(id=103)title: "Why relationships…"author_id: 1 ← FKhas manyकई रखता है拥有多个belongs toका है属于post.author

The foreign key points back to the parent; the ORM walks both ways.

Foreign key parent की ओर इशारा करती है; ORM दोनों ओर चलता है।

外键指回父对象;ORM 可以双向访问。

from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase): pass

class User(Base):
    __tablename__ = "users"
    id:    Mapped[int] = mapped_column(primary_key=True)
    name:  Mapped[str]
    email: Mapped[str] = mapped_column(unique=True)

    # A User has many Posts - this line creates the collection
    posts: Mapped[list["Post"]] = relationship(back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    id:        Mapped[int] = mapped_column(primary_key=True)
    title:     Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))

    # Back-reference - post.author gives you the User
    author: Mapped[User] = relationship(back_populates="posts")

engine = create_engine("sqlite:///blog.db")
Base.metadata.create_all(engine)

# Create a user with posts, all in one go
with Session(engine) as s:
    ada = User(name="Ada", email="ada@x.io", posts=[
        Post(title="Hello, world"),
        Post(title="Learning SQLAlchemy"),
        Post(title="Why relationships rule"),
    ])
    s.add(ada)
    s.commit()

# Navigate BOTH ways - no SQL written by you
with Session(engine) as s:
    ada = s.scalar(select(User).where(User.name == "Ada"))
    print(f"{ada.name} has {len(ada.posts)} posts:")
    for p in ada.posts:
        print(f"  - {p.title:24s} (by {p.author.name})")
OUTPUT
Ada has 3 posts: - Hello, world (by Ada) - Learning SQLAlchemy (by Ada) - Why relationships rule (by Ada)

Run it a second time…इसे दूसरी बार चलाइए…再运行一次…

from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase): pass

class User(Base):
    __tablename__ = "users"
    id:    Mapped[int] = mapped_column(primary_key=True)
    name:  Mapped[str]
    email: Mapped[str] = mapped_column(unique=True)

    # A User has many Posts - this line creates the collection
    posts: Mapped[list["Post"]] = relationship(back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    id:        Mapped[int] = mapped_column(primary_key=True)
    title:     Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))

    # Back-reference - post.author gives you the User
    author: Mapped[User] = relationship(back_populates="posts")

engine = create_engine("sqlite:///blog.db")
Base.metadata.create_all(engine)

# Create a user with posts, all in one go
with Session(engine) as s:
    ada = User(name="Ada", email="ada@x.io", posts=[
        Post(title="Hello, world"),
        Post(title="Learning SQLAlchemy"),
        Post(title="Why relationships rule"),
    ])
    s.add(ada)
    s.commit()

# Navigate BOTH ways - no SQL written by you
with Session(engine) as s:
    ada = s.scalar(select(User).where(User.name == "Ada"))
    print(f"{ada.name} has {len(ada.posts)} posts:")
    for p in ada.posts:
        print(f"  - {p.title:24s} (by {p.author.name})")
OUTPUT
Traceback (most recent call last): ... (125 more lines) sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: users.email [SQL: INSERT INTO users (name, email) VALUES (?, ?)] [parameters: ('Ada', 'ada@x.io')] (Background on this error at: https://sqlalche.me/e/21/gkpj)
🔧
Fixed: The Relationship Example Crashes on the Second Runसुधार: Relationship Example दूसरी बार Crash होता है已修正:关系示例第二次运行会崩溃

email is unique=True, and the script always inserts Ada again, so the second run raises IntegrityError: UNIQUE constraint failed. Seed only when the user is missing (Section 14 shows the pattern). Also, the original output showed the titles neatly lined up, but its print had no padding; we added :24s so the real output matches.

email unique=True है, और script हर बार Ada को फिर डालती है, इसलिए दूसरा run IntegrityError: UNIQUE constraint failed देता है। Seed तभी कीजिए जब user मौजूद न हो (Section 14 यह pattern दिखाता है)। साथ ही, मूल output में titles सीधी कतार में दिखाए गए थे, पर उसके print में padding नहीं थी; हमने :24s जोड़ा ताकि असली output मेल खाए।

email 设置了 unique=True,而脚本每次都会再插入 Ada,所以第二次运行会抛出 IntegrityError: UNIQUE constraint failed。应只在用户不存在时才插入初始数据(第 14 节展示了这种写法)。另外,原输出中标题排得整整齐齐,但它的 print 并没有填充;我们加上了 :24s,让真实输出与之相符。

Lazy loading and the N+1 problemLazy loading और N+1 समस्या延迟加载与 N+1 问题

By default user.posts is loaded lazily: the first time you touch it, the ORM runs one more query. Loop over 20 users and touch each user's posts, and you run 1 + 20 queries. That is the N+1 problem. selectinload() fetches all the posts in one extra query.

Default रूप से user.posts lazy load होता है: पहली बार छूने पर ORM एक और query चलाता है। 20 users पर loop चलाइए और हर user के posts छुइए, तो 1 + 20 queries चलती हैं। यही N+1 समस्या है। selectinload() सारे posts एक अतिरिक्त query में ले आता है।

默认情况下 user.posts 是 延迟 加载的:第一次访问时,ORM 会再执行一条查询。遍历 20 个用户并访问每个人的帖子,就会执行 1 + 20 条查询。这就是 N+1 问题。selectinload() 只需一条额外查询就能取回所有帖子。

from sqlalchemy import ForeignKey, create_engine, select, event
from sqlalchemy.orm import (DeclarativeBase, Mapped, mapped_column, relationship,
                            Session, selectinload)

class Base(DeclarativeBase): pass
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    author: Mapped[User] = relationship(back_populates="posts")

engine = create_engine("sqlite://")                 # in-memory database
Base.metadata.create_all(engine)
with Session(engine) as s:
    s.add_all([User(name=f"user{i}", posts=[Post(title="p1"), Post(title="p2")])
               for i in range(20)])
    s.commit()

queries = []
event.listen(engine, "before_cursor_execute",
             lambda conn, cur, sql, *a: queries.append(sql))

for label, stmt in [("lazy (default)", select(User)),
                    ("selectinload  ", select(User).options(selectinload(User.posts)))]:
    queries.clear()
    with Session(engine) as s:
        total = sum(len(u.posts) for u in s.scalars(stmt))
    print(f"{label}: {total} posts loaded with {len(queries)} SQL queries")
OUTPUT
lazy (default): 40 posts loaded with 21 SQL queries selectinload : 40 posts loaded with 2 SQL queries

🔢 Simulator: N+1 Query Counter🔢 Simulator: N+1 Query Counter🔢 模拟器:N+1 查询计数器

Each dot is one SQL query that SQLAlchemy really sent (counted with an event listener). Change the number of users and the loading strategy.

हर बिंदु एक SQL query है जो SQLAlchemy ने सच में भेजी (event listener से गिनी गई)। Users की संख्या और loading strategy बदलिए।

每个圆点代表 SQLAlchemy 真实发出的一条 SQL 查询(用事件监听器统计)。改变用户数量和加载策略试试。

N+1 Query CounterN+1 Query CounterN+1 查询计数器9 real runs · 2 posts per user9 असली runs · हर user के 2 posts9 次真实运行 · 每个用户 2 篇帖子
users:users:用户数:
loading:loading:加载方式:
first SQL statementsपहले SQL statements前几条 SQL 语句

SectionSection第 13 节

Common Pitfalls (and Fixes) 🕳️आम ग़लतियाँ (और सुधार) 🕳️常见陷阱(及修正)🕳️

Mistakeग़लती错误What happensक्या होता है会发生什么Fixसुधार修正
String-built SQL: f"WHERE name='{n}'"String से बना SQL: f"WHERE name='{n}'"字符串拼接 SQL:f"WHERE name='{n}'"SQL injection; names like O'Brien crashSQL injection; O'Brien जैसे नाम crashSQL 注入;O'Brien 这样的名字会出错? placeholders or SQLAlchemy expressions? placeholders या SQLAlchemy expressions? 占位符或 SQLAlchemy 表达式
Forgetting commit()commit() भूलना忘记 commit()Changes lost when the connection closesConnection बंद होने पर बदलाव खो जाते हैं连接关闭时修改丢失with conn: (commits) or conn.commit()with conn: (commit करता है) या conn.commit()with conn:(会提交)或 conn.commit()
Never closing the connectionConnection कभी बंद न करना从不关闭连接Open handles; file locks; pool runs outखुले handles; file locks; pool ख़त्म句柄未释放;文件锁;连接池耗尽conn.close() or contextlib.closing() — the sqlite3 with does not closeconn.close() या contextlib.closing() — sqlite3 का with बंद नहीं करताconn.close() 或 contextlib.closing() — sqlite3 的 with 不会关闭
UPDATE / DELETE without WHEREWHERE के बिना UPDATE / DELETE没有 WHERE 的 UPDATE / DELETEEvery row changed / deletedहर row बदली / मिटी所有行被修改 / 删除Run SELECT with the same filter first; check rowcountपहले उसी filter से SELECT; rowcount जाँचें先用相同条件运行 SELECT;检查 rowcount
UPDATE that matches no rowकोई row न मिलने वाला UPDATE一行都没匹配到的 UPDATENo error — 0 rows change silentlyकोई error नहीं — चुपचाप 0 rows बदलती हैं不报错 — 悄悄改动 0 行Check cur.rowcount and raisecur.rowcount जाँचें और raise करें检查 cur.rowcount 并抛出异常
fetchall() on a million rowsलाखों rows पर fetchall()对上百万行调用 fetchall()Memory blows upMemory फट जाती है内存暴涨Loop over the cursor: for row in cur:Cursor पर loop: for row in cur:遍历游标:for row in cur:
No PRAGMA foreign_keys = ON (SQLite)PRAGMA foreign_keys = ON नहीं (SQLite)没有 PRAGMA foreign_keys = ON(SQLite)Foreign keys declared but not enforcedForeign keys घोषित पर लागू नहीं声明了外键却不强制执行Turn it on after every connectहर connect के बाद चालू करें每次连接后都打开它
N+1 queries (ORM)N+1 queries (ORM)N+1 查询(ORM)One extra query per parent rowहर parent row पर एक अतिरिक्त query每个父行多一次查询selectinload() / joinedload()selectinload() / joinedload()selectinload() / joinedload()
Using ORM objects after the Session closedSession बंद होने के बाद ORM objects इस्तेमाल करना在 Session 关闭后使用 ORM 对象DetachedInstanceError on lazy attributesLazy attributes पर DetachedInstanceError访问延迟属性时报 DetachedInstanceErrorLoad what you need inside the Session (e.g. selectinload)जो चाहिए उसे Session के अंदर load करें (जैसे selectinload)在 Session 内加载所需数据(如 selectinload)
One Session shared across threadsThreads के बीच साझा एक Session多个线程共享一个 SessionRace conditions, broken stateRace conditions, बिगड़ी हालत竞态条件,状态错乱One Session per request / taskहर request / task के लिए एक Session每个请求 / 任务一个 Session
🔧
Fixed: Connection-Leak Row Corrected; Three Rows Addedसुधार: Connection-Leak Row सुधारी; तीन Rows जोड़ीं已修正:修正了"连接泄漏"一行;新增三行

The original fix for a connection leak was "use with sqlite3.connect(...)" — which does not close the connection (Section 06). We also added three real mistakes shown on this page: an UPDATE that silently matches nothing (Section 07), O'Brien-style quotes breaking string-built SQL (Section 04), and using ORM objects after their Session closed (Bug Doctor).

Connection leak का मूल सुधार था "with sqlite3.connect(...) लीजिए" — जो connection बंद नहीं करता (Section 06)। हमने इस page पर दिखाई गई तीन असली ग़लतियाँ भी जोड़ीं: चुपचाप किसी से मेल न खाने वाला UPDATE (Section 07), string से बने SQL को तोड़ने वाले O'Brien जैसे quotes (Section 04), और Session बंद होने के बाद ORM objects इस्तेमाल करना (Bug Doctor)।

原文对连接泄漏给出的修正是"使用 with sqlite3.connect(...)"— 而它并不会关闭连接(第 06 节)。我们还补充了本页展示过的三个真实错误:悄悄一行都没匹配到的 UPDATE(第 07 节)、O'Brien 这类引号会破坏拼接的 SQL(第 04 节),以及在 Session 关闭后使用 ORM 对象(Bug 医生)。


SectionSection第 14 节

Real-World Example — A Full Task Tracker 📋असली उदाहरण — पूरा Task Tracker 📋实战示例 — 完整的任务跟踪器 📋

A complete mini-app: users have many tasks, tasks have a status, and we can query, update and report — all with the ORM and typed models.

एक पूरा mini-app: users के कई tasks हैं, tasks का status है, और हम query, update और report कर सकते हैं — सब ORM और typed models से।

一个完整的小应用:用户有多个任务,任务有状态,我们可以查询、更新和生成报告 — 全部使用 ORM 和带类型的模型。

from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import create_engine, String, ForeignKey, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase): pass

class User(Base):
    __tablename__ = "users"
    id:    Mapped[int] = mapped_column(primary_key=True)
    name:  Mapped[str] = mapped_column(String(80))
    tasks: Mapped[list[Task]] = relationship(
        back_populates="owner", cascade="all, delete-orphan"
    )

class Task(Base):
    __tablename__ = "tasks"
    id:       Mapped[int] = mapped_column(primary_key=True)
    title:    Mapped[str] = mapped_column(String(200))
    status:   Mapped[str] = mapped_column(String(20), default="open")
    created:  Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
    owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    owner:    Mapped[User] = relationship(back_populates="tasks")

engine = create_engine("sqlite:///tracker.db", echo=False)
Base.metadata.create_all(engine)

# -- Seed data -----------------------------------
with Session(engine) as s:
    if not s.scalar(select(User)):          # only on the first run
        s.add_all([
            User(name="Ada", tasks=[
                Task(title="Design schema",   status="done"),
                Task(title="Write ORM models", status="done"),
                Task(title="Add tests",       status="open"),
            ]),
            User(name="Grace", tasks=[
                Task(title="Draft docs",      status="open"),
                Task(title="Ship v1",         status="blocked"),
            ]),
        ])
        s.commit()

# -- Report: open tasks per user -----------------
with Session(engine) as s:
    stmt = (
        select(User.name, func.count(Task.id).label("n_open"))
        .join(Task, Task.owner_id == User.id)
        .where(Task.status == "open")
        .group_by(User.name)
    )
    print("Open tasks by user:")
    for name, n in s.execute(stmt):
        print(f"  {name:8s} {n} open")

    # Mark one task as done - the ORM notices the change
    t = s.scalar(select(Task).where(Task.title == "Add tests", Task.status == "open"))
    if t:
        t.status = "done"
        s.commit()
        print(f"\nmarked done: {t.title}")
OUTPUT
Open tasks by user: Ada 1 open Grace 1 open marked done: Add tests

Run it a second time…इसे दूसरी बार चलाइए…再运行一次…

from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import create_engine, String, ForeignKey, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase): pass

class User(Base):
    __tablename__ = "users"
    id:    Mapped[int] = mapped_column(primary_key=True)
    name:  Mapped[str] = mapped_column(String(80))
    tasks: Mapped[list[Task]] = relationship(
        back_populates="owner", cascade="all, delete-orphan"
    )

class Task(Base):
    __tablename__ = "tasks"
    id:       Mapped[int] = mapped_column(primary_key=True)
    title:    Mapped[str] = mapped_column(String(200))
    status:   Mapped[str] = mapped_column(String(20), default="open")
    created:  Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
    owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    owner:    Mapped[User] = relationship(back_populates="tasks")

engine = create_engine("sqlite:///tracker.db", echo=False)
Base.metadata.create_all(engine)

# -- Seed data -----------------------------------
with Session(engine) as s:
    if not s.scalar(select(User)):          # only on the first run
        s.add_all([
            User(name="Ada", tasks=[
                Task(title="Design schema",   status="done"),
                Task(title="Write ORM models", status="done"),
                Task(title="Add tests",       status="open"),
            ]),
            User(name="Grace", tasks=[
                Task(title="Draft docs",      status="open"),
                Task(title="Ship v1",         status="blocked"),
            ]),
        ])
        s.commit()

# -- Report: open tasks per user -----------------
with Session(engine) as s:
    stmt = (
        select(User.name, func.count(Task.id).label("n_open"))
        .join(Task, Task.owner_id == User.id)
        .where(Task.status == "open")
        .group_by(User.name)
    )
    print("Open tasks by user:")
    for name, n in s.execute(stmt):
        print(f"  {name:8s} {n} open")

    # Mark one task as done - the ORM notices the change
    t = s.scalar(select(Task).where(Task.title == "Add tests", Task.status == "open"))
    if t:
        t.status = "done"
        s.commit()
        print(f"\nmarked done: {t.title}")
OUTPUT
Open tasks by user: Grace 1 open
🔧
Fixed: datetime.utcnow Is Deprecated, and the Report Hides Users With 0 Open Tasksसुधार: datetime.utcnow Deprecated है, और Report 0 Open Tasks वाले Users छिपाती है已修正:datetime.utcnow 已弃用,报告还会隐藏没有未完成任务的用户

(1) The original used default=datetime.utcnow. Since Python 3.12 utcnow() is deprecated (it returns a time with no time zone). Use lambda: datetime.now(timezone.utc). (2) On the second run Ada is missing from the report: the inner JOIN + WHERE status = "open" drops users who have no open tasks. Use an outer join with the condition in the ON clause if you want "Ada 0 open". (3) We added Task.status == "open" to the lookup, so a re-run does not "mark done" a task that is already done.

(1) मूल ने default=datetime.utcnow लिया। Python 3.12 से utcnow() deprecated है (यह बिना time zone वाला समय लौटाता है)। lambda: datetime.now(timezone.utc) लीजिए। (2) दूसरे run में Ada report से ग़ायब है: inner JOIN + WHERE status = "open" उन users को हटा देता है जिनका कोई open task नहीं। "Ada 0 open" चाहिए तो outer join लीजिए और शर्त ON में रखिए। (3) हमने lookup में Task.status == "open" जोड़ा, ताकि दोबारा चलाने पर पहले से done task फिर "mark done" न हो।

(1) 原文用了 default=datetime.utcnow。从 Python 3.12 起 utcnow() 已弃用(它返回不带时区的时间)。请使用 lambda: datetime.now(timezone.utc)。(2) 第二次运行时,Ada 从报告中 消失 了:内连接 JOIN + WHERE status = "open" 会去掉没有未完成任务的用户。如果想看到"Ada 0 open",请用外连接并把条件放进 ON 子句。(3) 我们在查找时加了 Task.status == "open",这样重跑时不会把已完成的任务再"标记完成"一次。

🏆
What This Tracker Does Rightयह Tracker क्या सही करता है这个跟踪器做对了什么

Typed models with Mapped[]. A two-way relationship with back_populates. cascade="all, delete-orphan", so deleting a user also deletes their tasks. An aggregate query with func.count(). Seed data inserted only once. Changes tracked automatically — change an attribute and commit.

Mapped[] वाले typed models। back_populates वाला दो-तरफ़ा relationship। cascade="all, delete-orphan", ताकि user मिटाने पर उसके tasks भी मिटें। func.count() वाली aggregate query। Seed data सिर्फ़ एक बार। बदलाव अपने-आप track — attribute बदलिए और commit कीजिए।

使用 Mapped[] 的带类型模型。通过 back_populates 建立的双向关系。cascade="all, delete-orphan",删除用户时连同其任务一起删除。使用 func.count() 的聚合查询。初始数据只插入一次。修改自动跟踪 — 改属性然后提交即可。


SectionSection第 15 节

Bug Doctor, Golden Rules and Quiz 🏆Bug Doctor, सुनहरे नियम और Quiz 🏆Bug 医生、黄金法则与小测验 🏆

🩺 Simulator: Bug Doctor🩺 Simulator: Bug Doctor🩺 模拟器:Bug 医生

Six real database mistakes. Buggy code on the left, fixed code on the right, changed lines marked. Every output is from a real run.

छह असली database ग़लतियाँ। बाईं ओर buggy code, दाईं ओर ठीक किया हुआ, बदली lines चिह्नित। हर output असली run से है।

六个真实的数据库错误。左边是有 bug 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行。

Bug DoctorBug DoctorBug 医生6 bugs · real outputs6 bugs · असली outputs6 个 bug · 真实输出
❌ BuggyBuggy有 bug
✅ Fixedठीक किया已修正

How much faster is executemany()?executemany() कितना तेज़ है?executemany() 到底快多少?

import sqlite3, time, os

ROWS = [(f"Book {i}", "Author", 9.99, 5) for i in range(2000)]
def fresh():
    if os.path.exists("speed.db"): os.remove("speed.db")
    conn = sqlite3.connect("speed.db")
    conn.execute("CREATE TABLE books (title TEXT, author TEXT, price REAL, stock INT)")
    return conn
SQL = "INSERT INTO books VALUES (?, ?, ?, ?)"

conn = fresh(); t0 = time.perf_counter()
for r in ROWS:
    conn.execute(SQL, r); conn.commit()           # commit after EVERY row
a = time.perf_counter() - t0; conn.close()

conn = fresh(); t0 = time.perf_counter()
for r in ROWS:
    conn.execute(SQL, r)                          # loop, ONE commit at the end
conn.commit(); b = time.perf_counter() - t0; conn.close()

conn = fresh(); t0 = time.perf_counter()
conn.executemany(SQL, ROWS); conn.commit()        # executemany, one commit
c = time.perf_counter() - t0; conn.close()

print(f"loop + commit per row : {a * 1000:7.1f} ms")
print(f"loop + one commit     : {b * 1000:7.1f} ms")
print(f"executemany + commit  : {c * 1000:7.1f} ms")
OUTPUT
loop + commit per row : 1987.2 ms loop + one commit : 5.4 ms executemany + commit : 3.5 ms

The big win is not executemany() itself — it is committing once. Committing after every row forces SQLite to save to disk each time. Inside one transaction, a loop of execute() is already fast, and executemany() is a little faster and shorter to write.

बड़ा फ़ायदा ख़ुद executemany() नहीं — एक बार commit करना है। हर row के बाद commit करने से SQLite को हर बार disk पर save करना पड़ता है। एक transaction के अंदर execute() का loop पहले से तेज़ है, और executemany() थोड़ा और तेज़ और लिखने में छोटा है।

真正的提速不在 executemany() 本身 — 而在于 只提交一次。每插一行就提交一次,会迫使 SQLite 每次都写盘。在同一个事务中,循环 execute() 本来就很快,executemany() 只是更快一点、写起来更短。

💾 Databases in Python — Rules to RememberPython में Databases — याद रखने वाले नियमPython 数据库 — 必须记住的规则
1
Never build SQL with string concatenation or f-strings. Use parameterized queries — ? in sqlite3, expressions in SQLAlchemy. This is your main defence against SQL injection, and it also handles quotes like O'Brien.
String जोड़कर या f-strings से SQL कभी मत बनाइए। Parameterized queries लीजिए — sqlite3 में ?, SQLAlchemy में expressions। यह SQL injection से आपका मुख्य बचाव है, और O'Brien जैसे quotes भी संभालता है।
永远不要用字符串拼接或 f-string 构造 SQL。使用参数化查询 — sqlite3 用 ?,SQLAlchemy 用表达式。这是防御 SQL 注入的主要手段,也能处理 O'Brien 这样的引号。
2
Commit, and close. with conn: commits (or rolls back) in sqlite3 but does not close — use conn.close() or contextlib.closing(). In SQLAlchemy, with Session(engine) and with engine.begin() clean up for you.
Commit कीजिए, और बंद कीजिए। sqlite3 में with conn: commit (या rollback) करता है पर बंद नहीं करता — conn.close() या contextlib.closing() लीजिए। SQLAlchemy में with Session(engine) और with engine.begin() आपके लिए सफ़ाई करते हैं।
提交,并关闭。在 sqlite3 中,with conn: 负责提交(或回滚)但 不会 关闭 — 请用 conn.close() 或 contextlib.closing()。在 SQLAlchemy 中,with Session(engine) 和 with engine.begin() 会替你收尾。
3
Every UPDATE and DELETE needs a WHERE clause. Run SELECT … WHERE with the same filter first, and check rowcount afterwards. There is no undo after commit.
हर UPDATE और DELETE को WHERE चाहिए। पहले उसी filter से SELECT … WHERE चलाइए, और बाद में rowcount जाँचिए। Commit के बाद कोई undo नहीं।
每条 UPDATE 和 DELETE 都需要 WHERE 子句。先用相同条件运行 SELECT … WHERE,事后检查 rowcount。提交之后就无法撤销。
4
Group related changes in one transaction, and raise if a step did not do what you expected. On any error, roll back — the database stays consistent.
जुड़े बदलावों को एक transaction में रखिए, और कोई step उम्मीद के मुताबिक़ न चले तो raise कीजिए। किसी भी error पर rollback — database संगत रहता है।
把相关的修改放在同一个事务中,某一步没有达到预期就抛出异常。出现任何错误就回滚 — 数据库保持一致。
5
Enable foreign keys in SQLite: PRAGMA foreign_keys = ON after every connect (with SQLAlchemy, in a "connect" event listener). It is off by default for backwards compatibility.
SQLite में foreign keys चालू कीजिए: हर connect के बाद PRAGMA foreign_keys = ON (SQLAlchemy में "connect" event listener में)। पुरानी अनुकूलता के लिए यह default रूप से बंद है।
在 SQLite 中启用外键:每次连接后执行 PRAGMA foreign_keys = ON(在 SQLAlchemy 中放进 "connect" 事件监听器)。为了向后兼容,它默认是关闭的。
6
For bulk inserts, commit once — and use executemany(). Committing per row was hundreds of times slower in the run above.
Bulk inserts के लिए एक बार commit कीजिए — और executemany() लीजिए। ऊपर के run में हर row पर commit सैकड़ों गुना धीमा था।
批量插入时只提交一次 — 并使用 executemany()。在上面的运行中,每行提交一次要慢上几百倍。
7
One Session per unit of work — one web request, one task, one script run. Do not share a Session across threads, and load what you need before it closes.
हर काम की इकाई के लिए एक Session — एक web request, एक task, एक script run। Threads के बीच Session साझा न करें, और बंद होने से पहले जो चाहिए load कर लें।
每个工作单元一个 Session — 一次 Web 请求、一个任务、一次脚本运行。不要在线程间共享 Session,并在它关闭前加载好所需数据。
8
Watch for N+1 queries. If a loop over parent rows triggers a query per child list, use selectinload() or joinedload() to fetch the children up front.
N+1 queries पर नज़र रखिए। अगर parent rows पर loop हर child list के लिए query चलाए, तो children पहले से लाने के लिए selectinload() या joinedload() लीजिए।
留意 N+1 查询。如果遍历父行时每个子列表都触发一次查询,就用 selectinload() 或 joinedload() 预先取回子对象。
🔧
Fixed: Rules 2 and 6 Correctedसुधार: नियम 2 और 6 सुधारे गए已修正:规则 2 和 6 已修正

Rule 2 said the context managers close connections — not true for sqlite3 (Section 06). Rule 6 said executemany() is "10–100× faster because it batches the round-trips". SQLite runs inside your process, so there are no network round trips; the run above shows the real cost is committing per row, while executemany() versus a loop in one transaction is a small gain.

नियम 2 ने कहा कि context managers connections बंद करते हैं — sqlite3 के लिए सच नहीं (Section 06)। नियम 6 ने कहा कि executemany() "round-trips को batch करने से 10–100× तेज़" है। SQLite आपके process के अंदर चलता है, इसलिए कोई network round trips नहीं; ऊपर का run दिखाता है कि असली लागत हर row पर commit है, जबकि एक transaction में loop बनाम executemany() का फ़ायदा छोटा है।

规则 2 说上下文管理器会关闭连接 — 对 sqlite3 并不成立(第 06 节)。规则 6 说 executemany()"因为批量减少往返而快 10–100 倍"。SQLite 运行在你的进程内部,根本没有网络往返;上面的运行表明真正的开销在于每行都提交,而在同一事务中 executemany() 相比循环只快一点点。

Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题

🏆 QuizPick an answer to see why.जवाब चुनें और कारण देखें।选一个答案,看看原因。
🎯
Key Takeawayमुख्य सीख关键要点

A database is organised storage you query with SQL. Send values with placeholders, never f-strings. Group related changes in a transaction and check that each step did what you expected. Turn on foreign keys in SQLite, commit once for bulk work, and remember that the sqlite3 with commits but does not close. When the app grows, SQLAlchemy Core and ORM give you composable queries, Python objects and many databases — just watch for N+1 queries.

Database व्यवस्थित storage है जिससे आप SQL में सवाल पूछते हैं। Values placeholders से भेजिए, f-strings से कभी नहीं। जुड़े बदलाव एक transaction में रखिए और जाँचिए कि हर step ने वही किया जो आप चाहते थे। SQLite में foreign keys चालू कीजिए, bulk काम में एक बार commit कीजिए, और याद रखिए कि sqlite3 का with commit करता है पर बंद नहीं करता। App बढ़ने पर SQLAlchemy Core और ORM जोड़ने लायक queries, Python objects और कई databases देते हैं — बस N+1 queries पर नज़र रखिए।

数据库是用 SQL 查询的有序存储。用占位符传值,绝不要用 f-string。把相关修改放进一个事务,并检查每一步是否达到预期。在 SQLite 中打开外键,批量操作只提交一次,并记住 sqlite3 的 with 会提交但不会关闭连接。应用变大后,SQLAlchemy Core 和 ORM 带来可组合的查询、Python 对象和多数据库支持 — 只是要留意 N+1 查询。

🐍 Try Python Yourself

Write, run and visualize Python step by step. No install needed.