许多年以前,在我刚接触数据科学和数据库的时候,经常需要从MySQL
中获取数据进行计算。一开始采用的方法是使用pymysql
执行SQL语句,然后将返回的结果处理成pandas
的DataFrame以便后续计算;随后知道了pandas.read_sql
函数,便如获至宝。
当时使用read_sql
函数的主要方式如下:
conn = pymysql.connect(...)
df = pd.read_sql(sql, conn)
那个时候,我被告知,程序只要可以正常结束即可,警告信息可以直接忽略。但实际上这不是一个好习惯。如果某段带有警告的程序在将来的业务中被复用,就会有变成bug的可能。
如果按照我最初的方式使用read_sql
,那么将会得到如下一个警告:
UserWarning: pandas only support SQLAlchemy connectable(engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested, please consider using SQLAlchemy.
这个警告的意思是说,向read_sql
传入的第二个参数不符合它的预期:pandas
希望用户传入一个由SQLAlchemy
创建的数据库连接对象或者字符串;也可以传入符合DBAPI2的数据库连接对象,但数据库必须是sqlite3
。
至此,暂时无需深入理解
SQLAlchemy
,将其视为一个可以创建数据库连接的工具即可。实际上,如果传入字符串,pandas
会将其转化为SQLAlchemy
连接对象。
而上述由pymysql
创建的conn
是一个符合DBAPI2、用于连接MySQL
的对象,所以pandas
给出了警告。
众所周知,关系型数据库的种类很多,它们之间的语法差异也不可谓不大,处理这些差异是一项繁杂的工作——而SQLAlchemy
对这些关系型数据库都进行了完善的测试,因此,pandas
直接使用SQLAlchemy
来避免可能出现的各种问题。
SQLAlchemy
很多文章都说,SQLAlchemy
的架构是如何好,功能是如何丰富。实际上,即使它们说的是真的,对于我这种不熟悉数据库架构、只会进行增删改查操作的数据库小白来说,也没办法理解并感同身受。我所关心的是,如何使用SQLAlchemy
来支撑我的日常工作。因此,我将从使用情景出发,结合SQLAlchemy
的使用文档,来总结它的用法。
在运行代码前,注意一下版本:
import sqlalchemy
print(sqlalchemy.__version__)
# 1.4.41
另外,请读者注意,下文的每一个代码块都需要继承其上所有代码块生成的变量才能顺利执行。
用户和数据库进行交互的流程可以简化为:建立连接-进行操作-关闭连接。大多数现代应用程序和数据库的交互是非常频繁的,这就要求数据库连接保持长时间的通畅;此外,多个程序或进程往往需要同时访问数据库,因此需要多个连接。注意到,连接的建立是需要消耗资源的,反映到用户体验上,就是可能会产生卡顿。为了保证应用程序的流畅性,人们发明了连接池技术。
通俗来讲,连接池就是预先创建一定数量的连接放在一起,像一个“池子”一样,需要用的时候,从池子中取出一个连接使用,用完后再放回去。因为连接是事先创建好的,而「取一个连接使用,完成后放回」所消耗的资源相较于每次都从头创建连接而言,是可以忽略不计的。于是,使用连接池可以很好地改善应用程序的运行效率。
通过create_engine
可以创建一个Engine
对象。Engine
的原义为“引擎”,在这里可以引申为连接池:
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://root:[email protected]:3306/platform', echo=True)
不难看出,create_engine
的第一个参数是数据库的连接信息,它告诉了SQLAlchemy
如下信息:数据库类型是MySQL
,连接数据库用的第三方驱动是PyMySQL
,后面则是数据库的常规配置信息。
没错,SQLAlchemy
本身并不提供连接驱动,而是通过第三方的DBAPI驱动来连接数据库。指定echo=True
是为了在接下来的操作中将engine
执行的所有SQL打印到控制台。
create_engine
使用了一种「延迟初始化」的机制,这意味着,直到用户执行了第一条确切的SQL命令,SQLAlchemy
才会创建连接池。关于这一点,可以在create_engine
执行前后通过执行SQL:show processlist
进行验证。
应用程序中最常用的一个场景是,将编写好的SQL发送至数据库服务器执行,并获取返回结果。利用SQLAlchemy
,可以这样操作:
from sqlalchemy import text
with engine.connect() as conn:
result = conn.execute(text("select 'hello world!'"))
print(result.all())
在控制台,会有类似如下的输出:
2023-01-13 15:14:03,249 INFO sqlalchemy.engine.Engine SELECT DATABASE()
2023-01-13 15:14:03,249 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-01-13 15:14:03,252 INFO sqlalchemy.engine.Engine SELECT @@sql_mode
2023-01-13 15:14:03,252 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-01-13 15:14:03,253 INFO sqlalchemy.engine.Engine SELECT @@lower_case_table_names
2023-01-13 15:14:03,253 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-01-13 15:14:03,255 INFO sqlalchemy.engine.Engine select 'hello world!'
2023-01-13 15:14:03,255 INFO sqlalchemy.engine.Engine [generated in 0.00024s] {}
[('hello world!',)]
除了最后一行是print
打印的输出,其余均是engine
在执行过程中产生的日志。
read_sql
将engine
对象作为read_sql
的第二个参数传入,则不会产生第一章中说的警告:
import pandas as pd
df = pd.read_sql("select * from test", engine) # 数据表自行创建
print(df)
print
打印的内容如下:
id name age
0 1 张三 12
1 2 李四 14
2 3 王五 13
实际上,将engine
作为参数传入read_sql
后读取数据,已经能够解决一开始提到的问题了。然而,这其实并未真正发挥SQLAlchemy
的作用。前文提到,不同的数据库类型具有不同的语法,这在应用升级或代码迁移时常常会产生问题。当然可以为了适配各种数据库而编写多套代码——但这未免太不优雅了。
这个问题的产生是基于以下两个事实:
问题的解决需要从源头入手:语法不同这件事没法改变,那只好从第二点入手,即,避免编写原生SQL。仔细想想,其实这一点是可以做到的。
以select操作为例,从某个表中查询数据是有一个标准范式的:从哪里(from)查,限定条件(where)是什么,是否联合查询(join),排序规则是什么(order by),是否限定数量(limit)等。
以此为启发,读者大概能想到,SQLAlchemy
将这些范式进行了梳理与总结,将原生SQL中的固定部分(范式内容)和自定义部分(条件内容)进行分离,从而使用户只需专注业务的逻辑限制,而不必费心于繁杂的SQL语法。
这里可以把元数据简单理解为数据库中的表及其结构信息。
要想从表中查询数据,首先需要知道表是什么样子的,即首先要获取表的元数据。在SQLAlchemy
中,可以通过如下方式获取已有表的元数据:
metadata = sqlalchemy.MetaData()
test = sqlalchemy.Table('test', metadata, autoload=True, autoload_with=engine)
print(repr(test))
打印的内容如下:
2023-01-13 17:18:46,502 INFO sqlalchemy.engine.Engine SHOW CREATE TABLE `test`
2023-01-13 17:18:46,503 INFO sqlalchemy.engine.Engine [raw sql] {}
Table('test', MetaData(), Column('id', INTEGER(), table=<test>, primary_key=True, nullable=False, comment='id'), Column('name', VARCHAR(charset='utf8', collation='utf8_general_ci', length=10), table=<test>, nullable=False, comment='名称'), Column('age', SMALLINT(), table=<test>, comment='年龄'), schema=None)
print
打印的内容是test的表结构信息。通过日志信息还可以发现,表结构信息实际上就是通过执行show create table
得到的。
已经拿到了表的信息,就可以执行select操作了:
from sqlalchemy import select
stmt = select(test).where(test.c.name == "张三")
print(stmt)
stmt
的创建过程就是2.3节提到的,对经过梳理的范式进行填充得到的:select
表明是进行读操作;传入的test
对象声明了数据来源表;.where
对数据进行了筛选。
test.c
是SQLAlchemy
用于记录表字段的对象,可以通过print(test.c.keys())
来查看该表的所有字段名称。
上述代码块输入如下内容:
SELECT test.id, test.name, test.age
FROM test
WHERE test.name = :name_1
可以看出,这就是一个结构化的查询语句了。
获取查询结果,执行如下代码:
with engine.connect() as conn:
row = conn.execute(stmt)
print(row.all())
控制台打印:
2023-01-13 17:33:29,172 INFO sqlalchemy.engine.Engine SELECT test.id, test.name, test.age
FROM test
WHERE test.name = %(name_1)s
2023-01-13 17:33:29,173 INFO sqlalchemy.engine.Engine [cached since 601.2s ago] {'name_1': '张三'}
[(1, '张三', 12)]
作为入门,本文简单地总结了SQLAlchemy
的基本用法,也只是触及这个享誉颇多的第三方库的皮毛。实际上,要深入研究SQLAlchemy
的运行机制,需要了解更多有关关系型数据库设计原则与应用程序开发原则的内容,如对象关系映射(ORM)、数据库抽象面临的问题等。这些内容就显得不是那么容易理解。
总之,无论在多大规模的应用程序中,使用SQLAlchemy
来和关系型数据库进行交互,都是一件近似最优解的方案。