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 是一个必须了解的工具,本文只是探索的一些非常基本的功能,基本上也用得上。对于从事过后台开发的朋友来说,这些都是很熟悉的。


相关文章
|
16天前
|
开发框架 数据建模 中间件
Python中的装饰器:简化代码,增强功能
在Python的世界里,装饰器是那些静悄悄的幕后英雄。它们不张扬,却能默默地为函数或类增添强大的功能。本文将带你了解装饰器的魅力所在,从基础概念到实际应用,我们一步步揭开装饰器的神秘面纱。准备好了吗?让我们开始这段简洁而富有启发性的旅程吧!
25 6
|
26天前
|
IDE 数据挖掘 开发工具
Python作为一种广受欢迎的高级编程语言,以其简洁的语法和强大的功能吸引了众多初学者和专业开发者
Python作为一种广受欢迎的高级编程语言,以其简洁的语法和强大的功能吸引了众多初学者和专业开发者
36 7
|
29天前
|
存储 缓存 测试技术
Python中的装饰器:功能增强与代码复用的利器
在Python编程中,装饰器是一种强大而灵活的工具,它允许开发者以简洁优雅的方式增强函数或方法的功能。本文将深入探讨装饰器的定义、工作原理、应用场景以及如何自定义装饰器。通过实例演示,我们将展示装饰器如何在不修改原有代码的基础上添加新的行为,从而提高代码的可读性、可维护性和复用性。此外,我们还将讨论装饰器在实际应用中的一些最佳实践和潜在陷阱。
|
1月前
|
SQL Java 数据库连接
深入 MyBatis-Plus 插件:解锁高级数据库功能
Mybatis-Plus 提供了丰富的插件机制,这些插件可以帮助开发者更方便地扩展 Mybatis 的功能,提升开发效率、优化性能和实现一些常用的功能。
201 26
深入 MyBatis-Plus 插件:解锁高级数据库功能
|
11天前
|
关系型数据库 MySQL 数据库
Python处理数据库:MySQL与SQLite详解 | python小知识
本文详细介绍了如何使用Python操作MySQL和SQLite数据库,包括安装必要的库、连接数据库、执行增删改查等基本操作,适合初学者快速上手。
80 15
|
18天前
|
测试技术 Python
探索Python中的装饰器:简化代码,增强功能
在Python的世界中,装饰器是那些能够为我们的代码增添魔力的小精灵。它们不仅让代码看起来更加优雅,还能在不改变原有函数定义的情况下,增加额外的功能。本文将通过生动的例子和易于理解的语言,带你领略装饰器的奥秘,从基础概念到实际应用,一起开启Python装饰器的奇妙旅程。
33 11
|
14天前
|
Python
探索Python中的装饰器:简化代码,增强功能
在Python的世界里,装饰器就像是给函数穿上了一件神奇的外套,让它们拥有了超能力。本文将通过浅显易懂的语言和生动的比喻,带你了解装饰器的基本概念、使用方法以及它们如何让你的代码变得更加简洁高效。让我们一起揭开装饰器的神秘面纱,看看它是如何在不改变函数核心逻辑的情况下,为函数增添新功能的吧!
|
23天前
|
设计模式 监控 程序员
Python中的装饰器:功能增强与代码复用的利器####
本文深入探讨了Python中装饰器的工作原理、应用场景及其在提升代码可读性、减少重复劳动方面的优势。不同于传统方法的冗长和复杂,装饰器提供了一种优雅且高效的方式来增强函数或方法的功能。通过具体实例,我们将揭示装饰器如何简化错误处理、日志记录及性能监控等常见任务,使开发者能够专注于核心业务逻辑的实现。 ####
|
24天前
|
机器人 计算机视觉 Python
Python作为一种高效、易读且功能强大的编程语言,在教育领域的应用日益广泛
Python作为一种高效、易读且功能强大的编程语言,在教育领域的应用日益广泛
39 5
|
23天前
|
SQL 测试技术 数据库