Python SQLAlchemy:需要了解的 3 个数据库操作功能

简介: Python SQLAlchemy:需要了解的 3 个数据库操作功能

image.png

Python 是一种多功能且富有表现力的编程语言,由于其简单性和可读性,已成为软件开发的强大动力。本文学习 Python 数据库的处理库 SQLAlchemy

Python SQLAlchemy 是一个强大且多功能的 Python SQL 工具包和对象关系映射 (ORM) 系统,提供了一整套众所周知的企业级持久性模式,专为高效和高性能的数据库访问而设计。它可以处理从小型简单查询到高负载场景中复杂事务的所有事务。它为开发人员提供了一个高级的 Pythonic 接口来与关系数据库交互,允许使用 Python 类和对象来处理数据库,而不是编写复杂的 SQL 查询,并以这种方式抽象代码,删除所有支持的数据库脚本语句,例如 PostgreSQL、MySQL、SQLite 和 Oracle 等等。许多流行的 Python 框架都使用 SQLAlchemy,例如 Django、Flask 和 Pyramid。

本文一起来学习 SQLAlchemy 库的三个有用功能,在大部分的项目开发中都可能用得上的功能。

下面先来了解一些基础知识。

基础知识

从这里开始介绍一些基本的知识,对于大多数人来说基本都是熟悉的知识。

数据模型

本文将以一个保存用户记录的简单 SQL 表作为实例数据,代码将重点关注 SQLAlchemy 集成,以实现对数据存储的需求,不涉及 Web 框架或任何其他复杂性。

为示例定义的基本用户模型如下:


from pydantic import BaseModel
from typing import Optional
class UserSignUp(BaseModel):
    name: str
    surname: Optional[str] = None
    birth_year: Optional[int] = None
    notes: Optional[str] = None

在上面的代码片段中,定义了一个 Pydantic 模型,它是想要在数据库中保存记录的数据的模型。Pydantic 是 Python 中最新且最有前途的库,用于处理数据验证、结构和建模。如果不熟悉它,其实跟 Mongoose 、Prisma 实现原理相似。

存储 Storage

出于存储目的,将使用最简单的 SQL 数据库系统之一,即 SQLite。为此,需要定义一个引擎来与文件系统中实际的 SQLite 数据库文件进行通信,并创建一个会话对象,以便可以与数据库进行交互以进行操作。


from datetime import datetime
from pydantic import BaseModel
import os
import json
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql import func
from sqlalchemy import Column, DateTime, Integer, String
project_dir = os.path.dirname(os.path.abspath(__file__))
engine = create_engine("sqlite:///{}".format(os.path.join(project_dir, "./storage.db")),
                       connect_args={"check_same_thread": False})
Base = declarative_base()
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=True)
    surname = Column(String, nullable=True)
    birth_year = Column(Integer, nullable=True)
    notes = Column(String, nullable=True)
    register_date = Column(DateTime, default=func.now())
    def __repr__(self):
        dict_repr = {c.name: getattr(self, c.name) for c in self.__table__.columns}
        for key, value in dict_repr.items():
            if isinstance(value, datetime):
                dict_repr[key] = datetime.isoformat(value)
        return json.dumps(dict_repr, indent=2)
Base.metadata.create_all(bind=engine)

在上面的代码片段中,项目文件夹路径用于指向数据库路径。数据库名称定义为 storage.db,如果不存在,则会自动创建。

此外,使用 SQLAlchemy ORM 模块的声明函数来构造一个基类,然后由 User 类来使用,User 类表示数据库中保存用户记录的表。

表用户行被定义为包含比我们为用户注册引入的数据模型额外的两个字段。

User 表包含注册引入的数据模型额外的两个字段。

  • 一个自动增量整数,作为已索引并用作表主键的行的 ID。
  • 注册日期字段,它将当前时间戳作为默认参数,这意味着正在数据库中写入的条目的时间戳。

数据库表定义类还包含一个额外的魔术方法,__repr__ 方法在此处定义以实现自定义序列化器。这样做的目的是可以直接打印数据库记录,即数据记录行,因为它们是使用 SQLAlchemy 库从表中获取的。

最后,通过调用模块 create_all 的方法,Base.metadata 在创建的数据库中实例化定义的表结构。因此,列及其数据类型和主键定义现在都存在。

写入数据库表

定义并创建了数据库表,现在来尝试写入一条记录。首先使用之前创建的引擎对象来创建一个到数据库的会话。


from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db_session = session_local()

然后,使用声明的数据模型创建一个要注册的新用户。


user_sign_up = UserSignUp(
    name="Quintion",
    surname="Tang",
    notes="一个工程师"
)

最后,使用 Users 的数据库表类创建一个条目,以使用建立的会话写入数据库并提交更改。


try:
    user_to_store = User(
        name=user_sign_up.name,
        surname=user_sign_up.surname,
        birth_year=user_sign_up.birth_year,
        notes=user_sign_up.notes
    )
    db_session.add(user_to_store)
    db_session.commit()
    print(user_to_store)
except IntegrityError as e:
    db_session.rollback()
    print(f"用户注册时出错:{str(e)}")

如果没有因任何完整性错误引发异常,则应写入该记录并将其提交到数据库。


{
  "id": 1,
  "name": "Quintion",
  "surname": "Tang",
  "birth_year": null,
  "notes": "一个工程师",
  "register_date": "2023-09-10T14:29:50"
}

请注意,birth_year 字段为空,因为首先没有为其提供任何整数,并且在写入操作期间会自动为 register_date 生成时间戳。

1、数据验证和预处理

在实际用例中,在存储之前,首先都需要预先验证或处理数据。

来看看将如何在这里做到这一点。例如,假设想要将每个字符串字段大写。如果需要在数据库中对数据进行标准化,这样的过程将对于后期数据处理分析非常有用。统一字符串字段大写的好处是在后续相关功能(如检索)不必担心区分大小写的问题。

那么在 SQLAlchemy 中是如何使用其 ORM validates 装饰器来完成此操作。


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=True)
    surname = Column(String, nullable=True)
    birth_year = Column(Integer, nullable=True)
    notes = Column(String, nullable=True)
    register_date = Column(DateTime, default=func.now())
    # 这里对数据进行预处理
    @validates('name', 'surname', 'notes')
    def normalize(self, _, value):
        if type(value) == str:
            return value.upper()
    def __repr__(self):
        dict_repr = {c.name: getattr(self, c.name) for c in self.__table__.columns}
        for key, value in dict_repr.items():
            if isinstance(value, datetime):
                dict_repr[key] = datetime.isoformat(value)
        return json.dumps(dict_repr, indent=2)

请注意,验证装饰器接受定义为字符串数据类型列的所有三列的参数 @validates('name', 'surname', 'notes')。当然,这不是强制性的,可以使用任意数量的列并选择不同的操作进行预处理。

函数 normalize 将应用于所有选定的三列,传入数据将通过此功能将要写入数据库的数据进行相应处理以返回所期望的大写字符串。

再次运行的写入操作现在将打印以下内容:


{
  "id": 1,
  "name": "QUINTION",
  "surname": "TANG",
  "birth_year": null,
  "notes": "一个工程师",
  "register_date": "2023-09-10T15:29:50"
}

2、用于动态查询数据的列属性

在 Python SQLAlchemy 中,column_property 函数用于在 SQLAlchemy 类上创建计算属性或别名属性,这个属性通常源自数据库表中的一列或多列。column_property 函数允许在 SQLAlchemy 模型上定义其他属性,这些属性不直接映射到数据库中的单个列,而是从现有列数据通过一定的计算或派生。

在上面用到的 User 数据表中,通常存储的是具体的年月日,而要获取用户的年龄就需要经过一定的计算。


from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy.sql import func, extract
from sqlalchemy.orm import column_property, validates
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=True)
    surname = Column(String, nullable=True)
    birth_year = Column(Integer, nullable=True)
    # 计算获取年龄列数据,此数据在数据库中并非真实存在
    age = column_property(extract('year', func.now()) - birth_year)
    notes = Column(String, nullable=True)
    register_date = Column(DateTime, default=func.now())
    @validates('name', 'surname', 'notes')
    def normalize(self, _, value):
        if type(value) == str:
            return value.upper()
    def __repr__(self):
        dict_repr = {c.name: getattr(self, c.name) for c in self.__table__.columns}
        for key, value in dict_repr.items():
            if isinstance(value, datetime):
                dict_repr[key] = datetime.isoformat(value)
        dict_repr['age'] = self.age
        return json.dumps(dict_repr, indent=2)

上面代码定义了新字段 age,它不是列类型,而是包含一个短表达式。使用来自 SQLAlchemy 的 sql 模块的 extract 方法,由 func.now() 派生的时间戳中提取年份。以前见过这个,每当查询表并动态派生时间戳时,该函数就会运行。通过提取当前时间戳的年份字段,减去用户的出生年份,就可以计算出用户的年龄。这个值不会存储在数据表行中的任何位置,但会在查询数据表的时候返回,是动态计算的。

由于字段 age 不是实际的列,因此它的值不会包含在 self.__table__.columns 字典中。当然还有其他的方法来达到这一目的。

下面来看看写入数据库和查询数据的结果。


user_sign_up = UserSignUp(
    name="Quintion",
    surname="Tang",
    notes="一个工程师",
    birth_year=1988
)
try:
    user_to_store = User(
        name=user_sign_up.name,
        surname=user_sign_up.surname,
        birth_year=user_sign_up.birth_year,
        notes=user_sign_up.notes
    )
    db_session.add(user_to_store)
    db_session.commit()
    print(user_to_store)
except IntegrityError as e:
    db_session.rollback()
    print(f"用户注册时出错:{str(e)}")

这次,提供出生年份,以便执行年龄计算。


{
  "id": 1,
  "name": "QUINTION",
  "surname": "TANG",
  "birth_year": 1988,
  "notes": "一个工程师",
  "register_date": "2023-09-10T15:58:31",
  "age": 35
}

现在,字段 age 会动态填充并在每次查询时返回。

3、主键之上多列的唯一约束

熟悉 SQL 数据库系统的人从一开始就知道每个表都需要包含一个主键。主键是唯一标识表中每条记录(行)的基本概念。它确保指定列中不存在重复值或空值,从而保证数据的完整性和唯一性。

然而,有时,需要对记录条目进行更多限制。在某些场景中,主键并不能满足所有需求。

举一个例子,假设注册用户与现有数据表中的用户具有完全相同的 namesurname ,在表主键只是一个增量数字的时候,是允许插入的。为了说明问题,验证相同用户名信息是否是同一个人,可以通过使用字段 namesurnamebirth_year 来标识两个不同的记录,也就是说,如果现有用户的所有的这三个字段都相同,则应拒绝插入,这种规则称为唯一性索引

下面来看看如何在表中强制执行这样的规则:


import json
from sqlalchemy import UniqueConstraint
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy.sql import func, extract
from sqlalchemy.orm import column_property, validates
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=True)
    surname = Column(String, nullable=True)
    birth_year = Column(Integer, nullable=True)
    age = column_property(extract('year', func.now()) - birth_year)
    notes = Column(String, nullable=True)
    register_date = Column(DateTime, default=func.now())
    # 增加多字段唯一性规则
    __table_args__ = (UniqueConstraint('name', 'surname', 'birth_year', name='unique_fullname_per_birth_year'),)
    @validates('name', 'surname', 'notes')
    def normalize(self, _, value):
        if type(value) == str:
            return value.upper()
    def __repr__(self):
        dict_repr = {c.name: getattr(self, c.name) for c in self.__table__.columns}
        for key, value in dict_repr.items():
            if isinstance(value, datetime):
                dict_repr[key] = datetime.isoformat(value)
        dict_repr['age'] = self.age
        return json.dumps(dict_repr, indent=2)

对象 UniqueConstraint 提供的表参数,对象采用不同的列名作为参数,这些列名称形成每个记录的唯一性,还可以为规则提供名称。

这一特性就不再演示其效果了。下面是完整代码:


from datetime import datetime
from pydantic import BaseModel
from typing import Optional
import os
import json
from sqlalchemy import create_engine, UniqueConstraint
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy.sql import func, extract
from sqlalchemy.orm import column_property, validates
project_dir = os.path.dirname(os.path.abspath(__file__))
engine = create_engine("sqlite:///{}".format(os.path.join(project_dir, "./storage.db")),
                       connect_args={"check_same_thread": False})
Base = declarative_base()
class UserSignUp(BaseModel):
    name: str
    surname: Optional[str] = None
    birth_year: Optional[int] = None
    notes: Optional[str] = None
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=True)
    surname = Column(String, nullable=True)
    birth_year = Column(Integer, nullable=True)
    age = column_property(extract('year', func.now()) - birth_year)
    notes = Column(String, nullable=True)
    register_date = Column(DateTime, default=func.now())
    __table_args__ = (UniqueConstraint('name', 'surname', 'birth_year', name='unique_fullname_per_birth_year'),)
    @validates('name', 'surname', 'notes')
    def normalize(self, _, value):
        if type(value) == str:
            return value.upper()
    def __repr__(self):
        dict_repr = {c.name: getattr(self, c.name) for c in self.__table__.columns}
        for key, value in dict_repr.items():
            if isinstance(value, datetime):
                dict_repr[key] = datetime.isoformat(value)
        dict_repr['age'] = self.age
        return json.dumps(dict_repr, indent=2)
Base.metadata.create_all(bind=engine)
session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db_session = session_local()
user_sign_up = UserSignUp(
    name="chris",
    surname="karvouniaris",
    notes="some notes about me",
    birth_year=1992
)
try:
    user_to_store = User(
        name=user_sign_up.name,
        surname=user_sign_up.surname,
        birth_year=user_sign_up.birth_year,
        notes=user_sign_up.notes
    )
    db_session.add(user_to_store)
    db_session.commit()
    print(user_to_store)
except IntegrityError as e:
    db_session.rollback()
    print(f"用户注册时出错:{str(e)}")

总结

在处理任何规模的 Python 项目中的数据库时,SQLAlchemy 是一个必须了解的工具,本文只是探索的一些非常基本的功能,基本上也用得上。对于从事过后台开发的朋友来说,这些都是很熟悉的。


相关文章
|
18天前
|
关系型数据库 MySQL 数据库连接
python脚本:连接数据库,检查直播流是否可用
【10月更文挑战第13天】本脚本使用 `mysql-connector-python` 连接MySQL数据库,检查 `live_streams` 表中每个直播流URL的可用性。通过 `requests` 库发送HTTP请求,输出每个URL的检查结果。需安装 `mysql-connector-python` 和 `requests` 库,并配置数据库连接参数。
116 68
|
10天前
|
SQL Java 数据库连接
深入 MyBatis-Plus 插件:解锁高级数据库功能
Mybatis-Plus 提供了丰富的插件机制,这些插件可以帮助开发者更方便地扩展 Mybatis 的功能,提升开发效率、优化性能和实现一些常用的功能。
77 26
深入 MyBatis-Plus 插件:解锁高级数据库功能
|
2天前
|
缓存 监控 测试技术
Python中的装饰器:功能扩展与代码复用的利器###
本文深入探讨了Python中装饰器的概念、实现机制及其在实际开发中的应用价值。通过生动的实例和详尽的解释,文章展示了装饰器如何增强函数功能、提升代码可读性和维护性,并鼓励读者在项目中灵活运用这一强大的语言特性。 ###
|
5天前
|
缓存 开发者 Python
探索Python中的装饰器:简化代码,增强功能
【10月更文挑战第35天】装饰器在Python中是一种强大的工具,它允许开发者在不修改原有函数代码的情况下增加额外的功能。本文旨在通过简明的语言和实际的编码示例,带领读者理解装饰器的概念、用法及其在实际编程场景中的应用,从而提升代码的可读性和复用性。
|
10天前
|
设计模式 缓存 测试技术
Python中的装饰器:功能增强与代码复用的艺术####
本文将深入探讨Python中装饰器的概念、用途及实现方式,通过实例演示其如何为函数或方法添加新功能而不影响原有代码结构,从而提升代码的可读性和可维护性。我们将从基础定义出发,逐步深入到高级应用,揭示装饰器在提高代码复用性方面的强大能力。 ####
|
11天前
|
缓存 测试技术 数据安全/隐私保护
探索Python中的装饰器:简化代码,增强功能
【10月更文挑战第29天】本文通过深入浅出的方式,探讨了Python装饰器的概念、使用场景和实现方法。文章不仅介绍了装饰器的基本知识,还通过实例展示了如何利用装饰器优化代码结构,提高代码的可读性和重用性。适合初学者和有一定经验的开发者阅读,旨在帮助读者更好地理解和应用装饰器,提升编程效率。
|
18天前
|
开发者 Python
探索Python中的装饰器:简化代码,增强功能
【10月更文挑战第22天】在Python的世界里,装饰器是一个强大的工具,它能够让我们以简洁的方式修改函数的行为,增加额外的功能而不需要重写原有代码。本文将带你了解装饰器的基本概念,并通过实例展示如何一步步构建自己的装饰器,从而让你的代码更加高效、易于维护。
|
14天前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
30 1
|
20天前
|
开发框架 Python
探索Python中的装饰器:简化代码,增强功能
【10月更文挑战第20天】在编程的海洋中,简洁与强大是航行的双桨。Python的装饰器,这一高级特性,恰似海风助力,让代码更优雅、功能更强大。本文将带你领略装饰器的奥秘,从基础概念到实际应用,一步步深入其内涵与意义。
|
19天前
|
关系型数据库 MySQL 数据处理
探索Python中的异步编程:从asyncio到异步数据库操作
在这个快节奏的技术世界里,效率和性能是关键。本文将带你深入Python的异步编程世界,从基础的asyncio库开始,逐步探索到异步数据库操作的高级应用。我们将一起揭开异步编程的神秘面纱,探索它如何帮助我们提升应用程序的性能和响应速度。