Python 的DB-API

从哪里可以找到数据库相关的接口呢?很简单,只需要前往Python 官网的数据库主题部
分即可。在那里,你可以找到全面的当前版本DB-API(2.0 版本)的链接,包括数据库模块、
文档、特殊兴趣小组等。从一开始起,DB-API 就被移动至PEP 249 中(PEP 248 中的老版
DB-API 1.0 标准已经废弃)。那么,DB-API 是什么呢?

DB-API 是阐明一系列所需对象和数据库访问机制的标准,它可以为不同的数据库适配
器和底层数据库系统提供一致性的访问。就像很多基于社区的成果一样,DB-API 也是强需
求驱动的。

在过去的日子里,曾有这样一种场景:有很多种数据库,并且很多人实现了他们自己
的数据库适配器。就像做无用功一样。这些数据库和适配器是在不同的时间被不同的人实
现的,在功能上完全没有一致性可言。但是,这意味着使用这些接口的应用代码需要与他
们选择使用的数据库模块进行定制化处理,接口的任何改变都会导致应用代码的变更。

由此,为了解决Python 数据库连接问题的特殊兴趣小组成立了,并且撰写了1.0 版本的
DB-API。该API 为不同的关系数据库提供了一致性的接口,并且使不同数据库间移植代码变
得更加简单,通常只需要修改几行代码即可。本章后面的部分你会看到一个相关的示例。

模块属性

DB-API 标准要求必须提供下文列出的功能和属性。一个兼容DB-API 的模块必须定义表
6-1 所示的几个全局属性。

DB-API 模块属性

数据属性
apilevel
该字符串(注意,不是浮点型)指明了模块需要兼容的DB-API 最高版本,比如,1.0、
2.0 等。该属性的默认值是1.0。

threadsafety
这是一个整型值,可选值如下。

  • 0:不支持线程安全。线程间不能共享模块。
  • 1:最小化线程安全支持:线程间可以共享模块,但是不能共享连接。
  • 2:适度的线程安全支持:线程间可以共享模块和连接,但是不能共享游标。
  • 3:完整的线程安全支持:线程间可以共享模块、连接和游标。

如果有资源需要进行共享,那么就需要诸如自旋锁、信号量之类的同步原语来达到原子
锁定的目的。由于此目的,磁盘文件和全局变量都并不可靠,甚至还会干扰到标准的互斥操
作。查阅threading 模块,或回顾第4 章,以获取如何使用锁的更多信息。

参数风格
DB-API 支持以不同的方式指明如何将参数与SQL 语句进行整合,并最终传递给服务器中执
行。该参数是一个字符串,用于指定构建查询行或命令时使用的字符串替代形式(见表6-2)。

数据库参数风格paramstyle

函数属性
connect()函数通过Connection 对象访问数据库。兼容模块必须实现connect()函数,该函
数创建并返回一个Connection 对象。表6-3 所示为connect()的参数。
可以使用包含多个参数的字符串(DSN)来传递数据库连接信息,也可以按照位置传递每个
参数,或者是使用关键字参数的形式传入。下面是PEP 249 中给出的使用connect()函数的例子。

connect(dsn='myhost:MYDB',user='guido',password='234$')

使用DSN 还是独立参数主要基于所连接的系统。比如,如果你使用的是像ODBC(Open
Database Connectivity)或JDBC(Java Database Connectivity)的API,则需要使用DSN;
而如果你直接使用数据库,则更倾向于使用独立的登录参数。另一个使用独立参数的原因
是很多数据库适配器并没有实现对DSN 的支持。下面是一些没有使用DSN 的connect()调
用。需要注意的是,并不是所有的适配器都会严格按照标准实现,比如MySQLdb 使用了
db 而不是database。

MySQLdb.connect(host='dbserv', db='inv', user='smith')
• PgSQL.connect(database='sales')
• psycopg.connect(database='template1', user='pgsql')
• gadfly.dbapi20.connect('csrDB', '/usr/local/database')
• sqlite3.connect('marketing/test')

数据库参数风格paramstyle

异常
异常同样需要包含在兼容的模块中,如表6-4 所示。

DB-API 异常类

Connection 对象

应用与数据库之间进行通信需要建立数据库连接。它是最基本的机制,只有通过数据库
连接才能把命令传递到服务器,并得到返回的结果。当一个连接(或一个连接池)建立后,
可以创建一个游标,向数据库发送请求,然后从数据库中接收回应。

Connection 对象方法
Connection 对象不需要包含任何数据属性,不过应当定义表6-5 所示的几个方法。

Connection 对象方法

当使用close()时,这个连接将不能再使用,否则会进入到异常处理中。
如果数据库不支持事务处理,或启用了自动提交功能,commit()方法都将无法使用。如
果你愿意,可以实现单独的方法用来启动或关闭自动提交功能。因为本方法是DB-API 中的
一部分,所以对于不支持事务处理的数据库而言,只需要在方法中实现“pass”即可。

如果RDBMS 不支持游标,那么cursor()仍然会返回一个尽可能模仿真实游标的对象。这
是最基本的要求。每个适配器开发者都可以为他的接口或数据库专门添加特殊属性。

DB-API 建议适配器开发者为连接编写所有的数据库模块异常(见表6-4),但并没
有做强制要求。如果没有,则认为Connection 对象将会抛出对应模块级别的异常。当你
完成数据库连接并关闭游标时,需要对所有操作执行commit(),并对你的连接执行
close()。

Cursor 对象

当建立连接后,就可以和数据库进行通信了。正如6.1 节所述,游标可以让用户提交数
据库命令,并获得查询的结果行。Python DB-API 游标对象总能提供游标的功能,即使是那
些不支持游标的数据库。此时,如果你创建了一个数据库适配器,还必须要实现cursor 对象,
以扮演类似游标的角色。这样,无论你将数据库系统切换到支持游标的数据库还是不支持游
标的数据库,都能保持Python 代码的一致性。

当游标创建好后,就可以执行查询或命令(或多个查询和命令),并从结果集中取回一行
或多行结果。表6-6 所示为Cursor 对象的数据属性和方法。

游标对象最重要的属性是execute*()和fetch*()方法,所有针对数据库的服务请求都
是通过它们执行的。arraysize 数据属性在为fetchmany()设置默认大小时非常有用。当然,
在不需要时关闭游标是个好主意, 而如果你的数据库支持存储过程,可能会用到
callproc()。

Cursor 对象属性

类型对象和构造函数

通常,两个不同系统间的接口是最脆弱的。比如在Python 对象和C 类型中进行转换就是
这样。类似地,在Python 对象和原生数据库对象间也存在这个问题。作为一个使用Python
的DB-API 的程序员,虽然你传递给数据库的参数是字符串,但是数据库可能需要将其转换
为多种不同的类型,从而可以对任何特定查询都能给出正确的数据类型。

例如,一个Python 字符串是应该转换成VARCHAR、TEXT、BLOB,还是原生BINARY
对象,抑或是DATE 或TIME 对象呢?为数据库提供期望格式的输入必须非常小心,因此,
DB-API 的另一个需求是创建构造函数,从而构建可以简单地转换成适当数据库对象的特
殊对象。表6-7 给出了用于此目标的一些类。SQL 的NULL 值对应于Python 的NULL
对象None。

类型对象和构造函数

API 版本变更
从1.0 版本(1996 年)修订为2.0 版本(1999 年),DB-API 做了几个重要的变更。

  • 从API 中移除了之前必须的dbi 模块。

  • 更新了类型对象。

  • 添加了新属性用于提供更好的数据库绑定。

  • 重新定义了callproc()的语义以及execute()的返回值。

  • 转换为基于类的异常。
    DB-API 2.0 版本发布后,曾在2002 年添加了一些刚才提到过的可选的扩展。自此之后,
    就再也没有重大变更了。关于DB-API 的持续讨论一直在DB-SIG 的邮件列表中进行。过去
    五年,对DB-API 的下一版本的可能性进行了讨论,该版本暂时命名为DB-API 3.0。它将包
    含如下特性。

  • 当有一个新的结果集时,nextset()可以给出一个更好的结果值。

  • 将float 转为Decimal。

  • 改善参数风格的灵活性以及对其的支持情况。

  • 预编译语句或语句缓存。

  • 完善事务模型。

  • 确定DB-API 可移植性的角色。

  • 添加单元测试。

关系数据库

现在我们可以准备开始学习了,不过首先会有一个问题摆在我们面前:“Python 有哪些
数据库系统的接口呢?”或者说是:“Python 支持哪些平台呢?”答案是:“几乎所有的数据
库系统。”下面是一个大概(但不详尽)的接口列表。

商业RDBMS

  • IBM Informix
  • Sybase
  • Oracle
  • Microsoft SQL Server
  • IBM DB2
  • SAP
  • Embarcadero Interbase
  • Ingres
    开源RDBMS
  • MySQL
  • PostgreSQL
  • SQLite
  • Gadfly
    数据库API
  • JDBC
  • ODBC
    非关系数据库
  • MongoDB
  • Redis
  • Cassandra
  • SimpleDB
  • Tokyo Cabinet
  • CouchDB
  • Bigtable(通过Google App Engine 的数据库API)

数据库和Python:适配器

对于每种支持的数据库,Python 都有一个或多个适配器用于连接Python 中的目标数
据库系统。比如Sybase、SAP、Oracle 和SQLServer 这些数据库就都存在多个可用的适配
器。我们需要做的事情就是挑选出最合适的适配器。你的挑选标准可能包括:它的性能如
何,它的文档和/或网站是否有用,是否有一个活跃的社区,驱动的质量和稳定性如何等。
需要记住的是,大多数适配器只提供给你连接数据库的基本需求,所以你还需要寻找一些
额外的特性。请记住,你需要负责编写更高级别的代码,比如线程管理和数据库连接池管
理等。

如果你不希望有太多的交互操作,比如你希望少写一些SQL 语句,或者尽可能少地参与
数据库管理的细节,那么你可以考虑ORM,本话题将在本章后面的小节中进行展开。

现在,让我们看几个使用适配器模块与关系数据库进行通信的例子。真正的秘密在于建
立连接。一旦你建立连接,并使用DB-API 的对象、属性和对象方法,你的核心代码就会看
起来很相似,而无须去管它使用了哪个适配器以及RDBMS。

使用数据库适配器的示例

首先,让我们看一些代码片段,包括创建数据库、创建表和使用表。这里会给出使用
MySQL、PostgreSQL 和SQLite 的例子。

MySQL
本例中我们将使用MySQL,以及Python 中著名的MySQL 适配器:MySQLdb(即
MySQL-Python)。而当我们的话题转为Python 3 时,会讨论另一个MySQL 适配器:MySQL
Connector/Python。在下面的代码中,我们会故意留下错误,从而让你能够自己想到创建
处理程序。

首先我们以管理员的身份登录数据库,创建数据库并赋予权限,然后再以普通用户的身
份重新登录数据库客户端,具体代码如下所示。

>>> import MySQLdb
>>> cxn = MySQLdb.connect(user='root')
>>> cxn.query('DROP DATABASE test')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
_mysql_exceptions.OperationalError: (1008, "Can't drop, database
'test'; database doesn't exist")
>>> cxn.query('CREATE DATABASE test')
>>> cxn.query("GRANT ALL ON test.* to ''@'localhost'")
>>> cxn.commit()
>> > cxn.close()

在上面的代码中,并没有使用游标。一些适配器有Connection 对象,这些对象可以使用
query()方法执行SQL 查询,不过不是所有的适配器都能这样。因此,建议或者不要使用这个
方法,或者事先检查适配器中该方法是否可用。

commit()方法是可选的,因为MySQL 默认开启了自动提交。下面我们要重新作为普通用
户登录这个新数据库,创建表,然后通过Python 执行一些常用的SQL 查询和命令。这次会
使用游标以及execute()方法。

下面的代码展示了创建表的方法。重复创建表(在没有事先删除表的情况下)会导致一
个错误发生。

>>> cxn = MySQLdb.connect(db='test')
>>> cur = cxn.cursor()
>>> cur.execute('CREATE TABLE users(login VARCHAR(8), userid INT)')
0L

现在,向数据库中添加一些行,并对其进行查询。

>>> cur.execute("INSERT INTO users VALUES('john', 7000)")
1L
>>> cur.execute("INSERT INTO users VALUES('jane', 7001)")
1L
>>> cur.execute("INSERT INTO users VALUES('bob', 7200)")
1L
>>> cur.execute("SELECT * FROM users WHERE login LIKE 'j%'")
2L
>>> for data in cur.fetchall():
... print '%s\t%s' % data
...
john 7000
ja ne 7001

最后一个功能是更新表,包括更新和删除行。

>>> cur.execute("UPDATE users SET userid=7100 WHERE userid=7001")
1L
>>> cur.execute("SELECT * FROM users")
3L
>>> for data in cur.fetchall():
... print '%s\t%s' % data
...
john 7000
jane 7100
bob 7200
>>> cur.execute('DELETE FROM users WHERE login="bob"')
1L
>>> cur.execute('DROP TABLE users')
0L
>>> cur.close()
>>> cxn.commit()
>> > cxn.close()

MySQL 是目前最流行的开源数据库之一,因此存在可用的Python 适配器并不令人意外。

PostgreSQL
另一个流行的开源数据库是PostgreSQL。与MySQL 不同,Postgres 至少包含3 种Python
适配器:psycopg、PyPgSQL 和PyGreSQL。还有一种适配器,叫PoPy,目前已废弃,并且
在2003 年将其项目与PyGreSQL 进行了合并。目前剩下的这三种适配器都有其自己的特性和
优缺点,所以根据实践对其进行选择更加明智。

当我们介绍各种适配器的使用方法时,需要注意PyPgSQL 自2006 年起就不再开发了,
而PyGreSQL 则是在2009 年发布的最新版本(4.0)。这两种适配器不再活跃,使得psycopg
成为PostgreSQL 适配器的唯一引领者,因此本书的示例最终将使用该适配器。psycopg 目前
已进入到第二个版本,这意味着虽然相关的示例中使用了版本1 的psycopg 模块,但是当下
载它时,需要使用psycopg2 来代替。

庆幸的是,这几种适配器的接口都很相似,所以可以创建一个应用,并对比这三种适配器
的性能(如果性能对你来说很重要)。下面的代码是每种适配器创建Connection 对象的代码。

psycopg

>>> import psycopg
>> > cxn = psycopg.connect(user='pgsql')

PyPgSQL

>>> from pyPgSQL import PgSQL
>> > cxn = PgSQL.connect(user='pgsql')

PyGreSQL

>>> import pgdb
>> > cxn = pgdb.connect(user='pgsql')

下面是一些可以用于这三种适配器的通用代码。

>>> cur = cxn.cursor()
>>> cur.execute('SELECT * FROM pg_database')
>>> rows = cur.fetchall()
>>> for i in rows:
... print i
>>> cur.close()
>>> cxn.commit()
>> > cxn.close()

最后,你可以看到每种适配器的输出结果有些许不同。

数据库适配器示例应用

在下面的例子中,我们将演示如何使用Python 来访问数据库。为了能够尽可能多地演示
多样性和代码,这里添加了对3 种不同数据库系统的支持:Gadfly、SQLite 以及MySQL。为
了后续加入更多的东西,首先会给出Python 2.x 下的完整源码,但不会给出逐行解释。

应用的运行与之前小节中描述的要点非常相似,所以你应该可以在没有完整解释的情况
下理解其功能,只需要从底部的main()函数开始即可(为了保持简单,对于拥有服务器的完
整系统,如MySQL,我们将直接作为root 用户进行登录,当然这种做法在生产环境中不提
倡)。下面是应用的源码,其文件名为ushuffle_db.py。

#!/usr/bin/env python3
"""
Database Operations Module (2025-08-26)
Optimized version supporting SQLite, MySQL and Gadfly
"""

import os
import logging
from random import randrange as rand
from typing import Tuple, Optional, Dict, Any, Iterator


# Constants and Configuration
class Config:
    COLSIZ = 10
    FIELDS = ('login', 'userid', 'projid')
    RDBMS_MAP = {'s': 'sqlite', 'm': 'mysql', 'g': 'gadfly'}
    DB_NAME = 'test'
    DB_USER = 'root'
    NAME_LEN = 16


# Logging Setup
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('db_operations.log'),
        logging.StreamHandler()
    ]
)

# Formatting Utilities
tformat = lambda s: str(s).title().ljust(Config.COLSIZ)
cformat = lambda s: s.upper().ljust(Config.COLSIZ)

# Sample Data
NAMES = (
    ('aaron', 8312), ('angela', 7603), ('dave', 7306),
    ('davina', 7902), ('elliot', 7911), ('ernie', 7410),
    ('jess', 7912), ('jim', 7512), ('larry', 7311),
    ('leslie', 7808), ('melissa', 8602), ('pat', 7711),
    ('serena', 7003), ('stan', 7607), ('faye', 6812),
    ('amy', 7209), ('mona', 7404), ('jennifer', 7608),
)


class DatabaseManager:
    def __init__(self):
        self.connection = None
        self.cursor = None
        self.db_type = None
        self.db_exc = None

    def setup(self) -> str:
        """Setup and return selected database type"""
        while True:
            try:
                choice = input('''
Choose a database system:
(M)ySQL 
(G)adfly
(S)QLite 
Enter choice: ''').strip().lower()[0]
                if choice in Config.RDBMS_MAP:
                    self.db_type = Config.RDBMS_MAP[choice]
                    return self.db_type
                raise ValueError
            except (IndexError, ValueError):
                print("Invalid choice. Please enter M, G or S.")

    def connect(self) -> bool:
        """Establish database connection"""
        db_dir = f"{self.db_type}_{Config.DB_NAME}"

        try:
            if self.db_type == 'sqlite':
                import sqlite3
                self.db_exc = sqlite3
                os.makedirs(db_dir, exist_ok=True)
                self.connection = sqlite3.connect(
                    os.path.join(db_dir, Config.DB_NAME)
                )

            elif self.db_type == 'mysql':
                import mysql.connector
                from mysql.connector import errorcode
                self.db_exc = mysql.connector

                try:
                    self.connection = mysql.connector.connect(
                        user=Config.DB_USER,
                        database=Config.DB_NAME,
                        autocommit=True
                    )
                except self.db_exc.Error as err:
                    if err.errno == errorcode.ER_BAD_DB_ERROR:
                        # Create database if not exists
                        conn = mysql.connector.connect(user=Config.DB_USER)
                        cursor = conn.cursor()
                        cursor.execute(f"CREATE  DATABASE {Config.DB_NAME}")
                        conn.close()
                        self.connection = mysql.connector.connect(
                            user=Config.DB_USER,
                            database=Config.DB_NAME
                        )
                    else:
                        raise

            elif self.db_type == 'gadfly':
                from gadfly import gadfly
                self.db_exc = gadfly
                try:
                    self.connection = gadfly(Config.DB_NAME, db_dir)
                except IOError:
                    os.makedirs(db_dir, exist_ok=True)
                    self.connection = gadfly()
                    self.connection.startup(Config.DB_NAME, db_dir)

            self.cursor = self.connection.cursor()
            return True

        except ImportError as e:
            logging.error(f"Database  driver not installed: {e}")
        except Exception as e:
            logging.error(f"Connection  failed: {e}")
        return False

    def execute_sql(self, sql: str, params=None) -> bool:
        """Execute SQL with error handling"""
        try:
            if params:
                self.cursor.execute(sql, params)
            else:
                self.cursor.execute(sql)
            return True
        except self.db_exc.Error as e:
            logging.error(f"SQL  execution failed: {e}\nSQL: {sql}\nParams: {params}")
            return False

    def create_table(self) -> bool:
        """Create users table with indexes"""
        try:
            sql = f"""
            CREATE TABLE IF NOT EXISTS users (
                login VARCHAR({Config.NAME_LEN}),
                userid INTEGER,
                projid INTEGER,
                INDEX idx_projid (projid),
                INDEX idx_userid (userid)
            )"""
            return self.execute_sql(sql)
        except Exception as e:
            logging.error(f"Table  creation failed: {e}")
            return False

    def drop_table(self) -> bool:
        """Drop users table"""
        return self.execute_sql("DROP  TABLE IF EXISTS users")

    def rand_name_generator(self) -> Iterator[Tuple[str, int]]:
        """Generate random names from sample data"""
        pick = set(NAMES)
        while pick:
            yield pick.pop()

    def insert_data(self, batch_size: int = 5) -> int:
        """Insert data in batches"""
        count = 0
        generator = self.rand_name_generator()

        if self.db_type in ('sqlite', 'gadfly'):
            query = "INSERT INTO users VALUES(?, ?, ?)"
        else:  # mysql
            query = "INSERT INTO users VALUES(%s, %s, %s)"

        while True:
            batch = [
                (*next(generator), rand(1, 5))
                for _ in range(batch_size)
            ]
            if not batch:
                break
            if self.execute_sql(query, batch):
                count += len(batch)

        return count

    def update_groups(self) -> Tuple[int, int, int]:
        """Randomly update user groups"""
        fr = rand(1, 5)
        to = rand(1, 5)

        if self.db_type in ('sqlite', 'gadfly'):
            query = "UPDATE users SET projid=? WHERE projid=?"
        else:
            query = "UPDATE users SET projid=%s WHERE projid=%s"

        self.execute_sql(query, (to, fr))
        return fr, to, self.cursor.rowcount

    def delete_group(self) -> Tuple[int, int]:
        """Randomly delete a user group"""
        rm = rand(1, 5)

        if self.db_type in ('sqlite', 'gadfly'):
            query = "DELETE FROM users WHERE projid=?"
        else:
            query = "DELETE FROM users WHERE projid=%s"

        self.execute_sql(query, (rm,))
        return rm, self.cursor.rowcount

    def display_data(self):
        """Display formatted table data"""
        if not self.execute_sql("SELECT  * FROM users"):
            return

        headers = ''.join([f"{field.upper():<{Config.COLSIZ}}"
                           for field in Config.FIELDS])
        print(f"\n{headers}")

        for data in self.cursor.fetchall():
            row = ''.join([f"{str(item).title():<{Config.COLSIZ}}"
                           for item in data])
            print(row)

    def close(self):
        """Close database connection"""
        if self.cursor:
            self.cursor.close()
        if self.connection:
            if self.db_type == 'mysql':
                self.connection.commit()
            self.connection.close()


def main():
    """Main execution function"""
    db_manager = DatabaseManager()

    print(f"\n{' Database Operations ':=^40}")
    print(f"Date: 2025-08-26 (Tuesday)")

    # Step 1: Setup database type
    db_type = db_manager.setup()
    print(f"\n[1/6] Connecting to {db_type.upper()}  database...")

    # Step 2: Connect to database
    if not db_manager.connect():
        print("ERROR: Database not supported or unreachable")
        return

    # Step 3: Create table
    print("\n[2/6] Creating users table...")
    if not db_manager.create_table():
        print("ERROR: Failed to create table")
        return

        # Step 4: Insert data
    print("\n[3/6] Inserting sample data...")
    count = db_manager.insert_data()
    print(f"Inserted {count} records")
    db_manager.display_data()

    # Step 5: Update groups
    print("\n[4/6] Updating user groups...")
    fr, to, num = db_manager.update_groups()
    print(f"Moved {num} users from group {fr} to {to}")
    db_manager.display_data()

    # Step 6: Delete group
    print("\n[5/6] Deleting random group...")
    rm, num = db_manager.delete_group()
    print(f"Deleted {num} users from group {rm}")
    db_manager.display_data()

    # Cleanup
    print("\n[6/6] Cleaning up...")
    db_manager.drop_table()
    db_manager.close()
    print("\nOperation completed successfully")


if __name__ == '__main__':
    main()

更多推荐