Skip to content

Commit 71ec94e

Browse files
committed
update: sqlalchemy posts
1 parent 2d51bbe commit 71ec94e

3 files changed

Lines changed: 257 additions & 1 deletion

File tree

content/posts/2026-01-26_sqlalchemy.md

Lines changed: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ tags = ['Python', 'Database']
88

99
### SQLALchemy Core and SQLALchemy ORM
1010

11-
SQLALchemy 分位两个模块:Core 和 ORM (Object-Relational Mapping)。
11+
SQLALchemy 分为两个模块:Core 和 ORM (Object-Relational Mapping)。
1212
Core 模块包含对所有受支持数据库方言的集成逻辑,一组用于描述数据库表的类,用于 Python 语言生成 SQL 语句。
1313
ORM 模块在 Python 应用程序中引入了一层抽象,使得许多数据库操作可以根据对 Python 对象执行的操作自动推导出来。
1414

@@ -140,6 +140,8 @@ Model 子类通过类属性定义:
140140

141141
`__tablename__` 属性定义数据库表名称,常见命名规范是表名称使用复数小写形式,这和模型名称形成对比,即使用单数的 camel case 驼峰命名法。
142142

143+
> `Mapped[]` 是一个范型容器,主要作用是告诉 IDE 和静态检查器,这个属性在 Python 层属于什么类型。
144+
143145
剩余定义的属性是表中的列,`Mapped[t]` 类型声明用于定义每个列,其中 `t` 是 Python 类型,例如 `int`, `str``datetime`
144146
对于 year 这样的普通列来说完全足够了,如果列需要额外的功能,就要将其赋值给一个 `mapped_column()` 构造器。
145147

@@ -788,3 +790,211 @@ session.get(Product, 23) # Product(23, 'CT-80')
788790
如果结果不存在,则会返回 `None`
789791

790792
## Indexs
793+
794+
数据库在搜索信息的时候,实际上有多种不同的算法。
795+
当给予一个查询时,数据库会决定使用更合适更高效的算法。
796+
797+
有一种算法总是可用的:表扫描 table scan。
798+
表扫描 table scan 操作包括在按顺序读取条目时,对所有行依次评估 evaluate 查询过滤器 query filters。
799+
800+
表扫描是数据库没有其他方法后,最后会使用的算法,或者当的表足够小,没必要使用复杂算法时。
801+
作为数据库的设计者,需要确保数据通过合适的方式进行索引,从而支持更加复杂的搜索算法。
802+
803+
当列被标记为索引后,数据库会维护一个二叉树结构的数据结构,来让该列的查询和排序更加高效。
804+
805+
例如下面的例子:
806+
807+
- id
808+
- name
809+
- manufacturer
810+
- year
811+
812+
`id` 列是主键,数据库会自动将该列索引,因此根据主键搜索有很多优化。
813+
`name`, `manufacturer``year` 这样的列经常用于 `where()`, `group_by()``order_by()` 之类的查询,当前没有被索引,因此会一行一行查询。
814+
815+
对于需要索引的列,在模型定义中添加 `index=True`:
816+
817+
```Python
818+
class Product(Model):
819+
__tablename__ = 'products'
820+
821+
id: Mapped[int] = mapped_column(primary_key=True)
822+
name: Mapped[str] = mapped_column(String(64), index=True)
823+
manufacturer: Mapped[str] = mapped_column(String(64), index=True)
824+
year: Mapped[int] = mapped_column(index=True)
825+
country: Mapped[str] = mapped_column(String(32))
826+
cpu: Mapped[str] = mapped_column(String(32))
827+
828+
def __repr__(self):
829+
return f'Product({self.id}, "{self.name}")'
830+
```
831+
832+
## Constrains
833+
834+
另一个好的数据库设计实践是为数据库分配约束。
835+
`Product` 模型有一个叫做 `PRIMARY KEY` 的约束,即 `primary_key=True` 参数。
836+
除了主键外,还有 `UNIQUE``NOT NULL` 这些其他列常用的约束。
837+
838+
有唯一约束 `UNIQUE` 的列不能有重复值,通过参数 `unique=True` 实现。
839+
非空约束 `NOT NULL` 防止列拥有一个空的或为定义的值。
840+
通过 `Mapped[t]` 定义的字段默认会有 `NOT NULL` 约束,如果需要允许空值,则使用 `Mapped[Optional[t]]`
841+
842+
下面是添加约束的 `Product` 类:
843+
844+
```Python
845+
from typing import Optional
846+
from sqlalchemy import String
847+
848+
class Product(Model):
849+
__tablename__ = 'products'
850+
851+
id: Mapped[int] = mapped_column(primary_key=True)
852+
name: Mapped[str] = mapped_column(String(64), index=True, unique=True)
853+
manufacturer: Mapped[str] = mapped_column(String(64), index=True)
854+
year: Mapped[int] = mapped_column(index=True)
855+
country: Mapped[Optional[str]] = mapped_column(Sting(32))
856+
cpu: Mapped[Optional[str]] = mapped_column(String(32))
857+
858+
def __repr__(self):
859+
return f'Product({self.id}, "{self.name}")'
860+
```
861+
862+
## Deletions
863+
864+
在之前已经介绍了通过 `add()` 来添加项,我们也可以通过 `delete()` 删除项。
865+
866+
```Python
867+
session = Session()
868+
p = session.get(Product, 23)
869+
session.delete(p)
870+
session.commit()
871+
```
872+
873+
一旦该删除被提交,将无法恢复
874+
875+
```Python
876+
p = session.get(Product, 23)
877+
print(p) # None
878+
```
879+
880+
## Exercises
881+
882+
1. 1983 年的前 3 个字母表排序的结果
883+
884+
```Python
885+
from sqlalchemy import select
886+
887+
with Session() as session:
888+
query = select(Product).where(Product.year == 1983).order_by(Product.name).limit(3)
889+
products = session.scalars(query).all()
890+
891+
for p in products:
892+
print(p)
893+
```
894+
895+
2. 含有 "Z80" 的 CPU
896+
897+
```Python
898+
with Session() as session:
899+
query = select(Product).where(Product.cpu.like('%Z80%'))
900+
# query = select(Product).where(Product.cpu.contains('Z80'))
901+
products = session.scalars(query).all()
902+
903+
for p in products:
904+
print(p)
905+
```
906+
907+
3. 1990 年前,CPU 含有 "Z80" 或 "6502" 的产品,并按名称字母表排序
908+
909+
```Python
910+
with Session() as session:
911+
query = (
912+
select(Product)
913+
.where(
914+
or_(Product.cpu.contains('Z80'), Product.cpu.contains('6502')),
915+
Product.year < 1990
916+
)
917+
.order_by(Product.name)
918+
)
919+
products = session.scalars(query).all()
920+
921+
for p in products:
922+
print(p)
923+
```
924+
925+
4. 在 1980s 年代有产品的制造商
926+
927+
```Python
928+
with Session() as session:
929+
query = (
930+
select(Manufacturer)
931+
.join(Manufacturer.products)
932+
.distinct()
933+
.where(Product.year >= 1980, Product.year < 1990)
934+
)
935+
manufacturers = session.scalars(query).all()
936+
937+
for m in manufacturers:
938+
print(m)
939+
```
940+
941+
5. 名称以 T 字母开头的生产商名称,并按照字母表排序
942+
943+
```Python
944+
with Session() as session:
945+
query = select(Manufacturer).where(Manufacturer.name.startswith('T')).order_by(Manufacturer.name)
946+
manufacturers = session.scalars(query).all()
947+
948+
for m in manufacturers:
949+
print(m)
950+
```
951+
952+
6. 在 Croatia 制造产品的最早、最晚的年份,以及产品数量
953+
954+
```Python
955+
from sqlalchemy import func
956+
957+
with Session() as session:
958+
query = (
959+
select(
960+
func.min(Product.year),
961+
func.max(Product.year),
962+
func.count()
963+
)
964+
.where(Product.country == 'Croatia')
965+
)
966+
results = session.execute(query).one()
967+
968+
print(results)
969+
```
970+
971+
7. 每年发布的产品:结果应从产品数量最多的开始排序,没有产品的年份应该忽略
972+
973+
```Python
974+
with Session() as session:
975+
query = (
976+
select(Product.year, func.count())
977+
.group_by(Product.year)
978+
.order_by(func.count().desc())
979+
)
980+
results = session.execute(query).all()
981+
982+
for year, count in results:
983+
print(year, count)
984+
```
985+
986+
8. 国家在 USA 的生产商
987+
988+
```Python
989+
with Session() as session:
990+
query = (
991+
select(Manufacturer)
992+
.join(Manufacturer.products)
993+
.where(Product.country == 'USA')
994+
.distinct()
995+
)
996+
manufacturers = session.scalars(query).all()
997+
998+
for m in manufacturers:
999+
print(m)
1000+
```

content/posts/2026-03-02_python-distilled-09.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,42 @@ os.environ['NAME'] = 'VALUE'
384384
与命令行选项类似,编码错误的环境变了可能会生成使用 `surrogateescape` 错误处理策略的字符串。
385385

386386
## Files and File Objects
387+
388+
如果要打开一个文件,使用内置的 `open()` 函数。
389+
它也结合上下文管理器打开文件:
390+
391+
```Python
392+
# read text file all at once
393+
with open('filename.txt', 'rt') as file:
394+
data = file.read()
395+
396+
# read a file line-by-line
397+
with open('filename.txt', 'rt') as file:
398+
for line in file:
399+
...
400+
401+
# write to a text file
402+
with open('out.txt', 'wt') as file:
403+
file.write('Some output\n')
404+
print('More output', file=file)
405+
```
406+
407+
在大多数情况下,使用 `open()` 是编辑文件的直接方式:
408+
409+
```Python
410+
open('name.txt') # Open name.txt for reading
411+
open('name.txt', 'rt') # Same above
412+
open('name.txt', 'wt') # Open for writing
413+
open('name.txt', 'r+t') # Read and write
414+
open('name.txt', 'at') # Append to file
415+
open('name.txt', 'bt') # Binary mode read
416+
open('name.txt', 'wt') # Binary mode write
417+
# 't' means text
418+
```
419+
420+
## Filenames
421+
422+
为了打开文件,需要文件名。
423+
该名称既可以是绝对路径,也可以是相对路径。
424+
对于相对路径,是相对于当前文件路径的,可以通过 `os.getcwd()` 获取,即当前工作目录。
425+
如果要修改,使用 `os.chadir(newdir)` 来修改工作目录。
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
+++
2+
date = '2026-03-10T8:00:00+01:00'
3+
draft = true
4+
title = 'SQLALchemy - Relationships'
5+
categories = ['Note']
6+
tags = ['Python', 'Database']
7+
+++

0 commit comments

Comments
 (0)