23 Python操作数据库

使用简单的纯文本方式只能实现有限的功能,不能进行快速查询,只有把数据全部读到内存中才能自己遍历。不过在实际应用中,我们操作的数据大小经常远远超过内存,根本无法全部读入内存。
为了便于程序保存和读取数据,并直接通过条件快速查询指定的数据,于是出现了数据库(Database)这种专门用于集中存储和查询的软件。
这里将介绍在Python 3.5中使用PyMySQL连接数据库,并实现简单的增、删、改、查。

数据库介绍

数据库历史非常久远,早在1950年就诞生了。经历了网状数据库、层次数据库,我们现在广泛使用的关系数据库是20世纪70年代在关系模型的基础上诞生的。
目前,广泛使用的关系数据库分为付费型和免费型。付费型数据库主要有以下几种:
(1)Oracle,典型的“高富帅”,收费昂贵,产品确实好,当前很多大型公司仍然使用它。
(2)SQL Server,微软自家产品,Windows定制专款。
(3)DB2,IBM的产品。
(4)Sybase,曾经跟微软关系非常亲密,后来关系破裂,使用的人比较少了,已逐渐淡出大家的视野。
这些数据库都是不开源而且付费的,最大的好处是出了问题可以找厂家解决。不过在Web的世界里,通常需要部署成千上万数据库服务器,如果使用付费型数据库,赚的钱都会被拿去买服务器了。所以,无论是Google、Facebook,还是国内的BAT,无一例外都选择免费的开源数据库。当前流行的免费数据库有以下几种:
(1)MySQL,当前使用最为广泛的开源数据库。
(2)PostgreSQL,学术气息有点重,其实挺不错,不过知名度没有MySQL高。
(3)SQLite,嵌入式数据库,适合桌面和移动应用。
作为Python开发工程师,选择哪款免费数据库呢?当然是MySQL。因为MySQL普及率最高,出了错可以很容易找到解决方法,而且围绕MySQL有一大堆监控和运维工具,安装和使用很方便。
为了继续后面的学习,你需要从MySQL官方网站(http://www.mysql.com)下载并安装MySQL Community Server。
你也许还听说过NoSQL数据库,很多NoSQL宣传速度和规模远远超过关系数据库,是否有很多同学觉得有了NoSQL就不需要SQL了呢?这样的想法是错误的,在搞明白NoSQL之前,还需要先明白SQL,在SQL的基础上学习NoSQL很容易,反过来就不行了。
本章主要介绍Python如何操作数据库,并不是单纯介绍数据库,如果你想从零学习关系数据库和基本的SQL语句,还需查看相关资料。

Python数据库API

Python数据库API是为方便统一操作数据库而提出的一个标准接口(API),也称为DB-API。
在没有Python DB-API之前,各数据库之间的应用接口非常混乱,实现各不相同。如果项目需要更换数据库,就需要进行大量修改,非常不便。Python DB-API的出现就是为了解决这些问题。
Python所有数据库接口程序都在一定程度上遵守Python DB-API规范。DB-API定义了一系列必需的对象和数据库存取方式,以便为各种各样的底层数据库系统和数据库接口程序提供一致的访问接口。由于DB-API为不同数据库提供了一致的访问接口,因此在不同的数据库之间移植代码成为一件轻松的事情。
DB-API规范包括全局变量、异常、连接、游标和类型等基本概念,下面我们逐一进行介绍。

1 全局变量

DB-API规范规定数据库接口模块必须实现一些全局属性以保证兼容性。Python提供了3个描述数据库模块特性的全局变量,如表1所示。

1 Python DB-API模块特性全局变量

apilevel指的是API级别,是一个字符串常量,表示这个DB-API模块所兼容的DB-API最高版本号。例如,版本号是1.0、2.0,如果未定义,就默认是1.0。

线程安全等级Threadsafety是一个整数,取值范围如下:

0表示不支持线程安全,多个线程不能共享此模块。

1表示初级线程安全支持,线程可以共享模块,但不能共享连接。

2表示中级线程安全支持,线程可以共享模块和连接,但不能共享游标。

3表示完全线程安全支持,线程可以共享模块、连接及游标。

参数风格(paramstyle)表示执行多次类似查询时,参数如何被拼接到SQL查询中。值format表示标准字符串格式化(使用基本的格式代码),可以在参数中进行拼接的地方插入%s。值pyformt表示扩展的格式代码,用于字典拼接,如%(foo)。除了Python风格之外,还有3种接合方式:qmark的意思是使用问号,numeric表示使用:1或:2格式的字段(数字表示参数的序号),而named表示:foobar这样的字段。其中,foobar为参数名。

16.2.2 异常

为了能尽可能准确地处理错误,DB-API中定义了一些异常。这些异常被定义在层次结构中,可以通过一个except块捕捉多种异常。

异常的层次如表2所示。

2 DB-API常见异常

3 连接和游标

为了使用基础数据库系统,首先必须连接它。连接数据库需要使用具有恰当名称的connect函数。该函数有多个参数,具体使用哪个参数需要根据数据库类型进行选择。DB-API定义了表3所示的参数作为准则(建议将这些参数按表中给定的顺序传递)。参数类型为字符串类型。

3 connect函数常用参数

connect函数返回连接对象,这个连接对象表示目前和数据库的会话。连接对象支持的方法如表4所示。

4 连接对象方法

rollback方法可能不可用,因为不是所有数据库都支持事务。

commit方法总是可用的,不过如果数据库不支持事务,它就没有任何作用。

cursor方法指游标对象。通过游标执行SQL查询并检查结果。游标比连接支持更多方法,而且在程序中更好用。表5是游标方法的概述,表6是游标特性的概述。

5 游标对象方法
6 游标对象特性

游标对象最重要的属性是execute*()fetch*()方法。所有对数据库服务器的请求都由这两个方法完成。对fetchmany()方法来说,设置一个合理的arraysize属性很有用。当然,在不需要时最好关掉游标对象。

4 类型

每一个插入数据库中的数据都对应一个数据类型,每一列数据对应同一个数据类型,不同列对应不同的数据类型。在数据库操作的过程中,为了能够正确与基础SQL数据库进行数据交互操作,DB-API定义了用于特殊类型和值的构造函数及常量,所有模块都要求实现表7所示的构造函数和特殊值。


7 DB-API构造函数和特殊值

注:新纪元指1970-01-01 00:00:01 utc时间

数据库操作

前面我们介绍了数据库的基本概念,本节具体介绍数据库的连接及增、删、改、查、操作。
下面的示例以数据库为TEST,表名为employee,employee表字段为FIRST_NAME、LAST_NAME、AGE、SEX、INCOME和CREATE_TIME。
连接数据库TEST使用的用户名为root,密码为root。
在系统上已经安装了Python PyMySQ模块。若不知道怎么安装,则可查阅相关资料。
如果对SQL语句不熟悉,就要先了解数据库的一些基本操作,以方便更好理解接下来的内容。

1 数据库连接

下面是连接MySQL TEST数据库的实例。

#! /usr/bin/python
# -*-coding:UTF-8-*-

import pymysql

def db_connect():
    # 打开数据库连接
    db = pymysql.connect("localhost", "root", "root", "test")

    # 使用cursor()方法创建一个游标对象cursor
    cursor = db.cursor()

    # 使用execute()方法执行SQL查询
    cursor.execute("SELECT VERSION()")

    # 使用 fetchone() 方法获取单条数据
    data = cursor.fetchone()
    print ("Database version : %s " % data)

    # 关闭数据库连接
    db.close()

def main():
    db_connect()

if __name__ == "__main__":
    main()

执行结果如下:

Database version : 5.5.28

2 创建数据库表

如果数据库连接存在,我们就可以使用execute()方法为数据库创建表。创建表EMPLOYEE的代码如下:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import pymysql

def create_table():
    db = pymysql.connect("localhost", "root", "root", "test")
    # 使用 cursor() 方法创建一个游标对象cursor
    cursor = db.cursor()

    # 使用 execute() 方法执行 SQL,如果表存在就删除
    cursor.execute("DROP TABLE IF EXISTS EMPLOYEE")

    # 使用预处理语句创建表
    sql = """CREATE TABLE EMPLOYEE (
          FIRST_NAME      CHAR(20) NOT NULL,
          LAST_NAME      CHAR(20),
          AGE INT,
          SEX CHAR(1),
          INCOME FLOAT,
          CREATE_TIME DATETIME)"""
    try:
         cursor.execute(sql)
         print("CREATE TABLE SUCCESS.")
    except Exception as e:
         print("CREATE TABLE FAILED,CASE:%s" % e)
    finally:
         # 关闭数据库连接
         db.close()

def main():
    create_table()

if __name__ == "__main__":
    main()

执行结果如下:

CREATE TABLE SUCCESS.

从MySQL客户端查看表结构,如图1所示。
1 employee表结构

3 数据库插入

下面使用SQL INSERT语句向表EMPLOYEE插入记录(注意使用了datetime模块)。

#! /usr/bin/python
# -*-coding:UTF-8-*-

import pymysql
import datetime

def insert_record():
    db = pymysql.connect("localhost", "root", "root", "test")

    # 使用cursor()方法获取操作游标
    cursor = db.cursor()

    # SQL 插入语句
    sql = "INSERT INTO EMPLOYEE(FIRST_NAME,LAST_NAME, AGE, SEX, INCOME," \
           " CREATE_TIME) VALUES('%s', '%s', %d, '%c', %d, '%s')" \
           % ('xiao', 'zhi', 22, 'M', 30000, datetime.datetime.now())

    try:
        # 执行sql语句
        cursor.execute(sql)
        # 提交到数据库执行
        db.commit()
        print("INSERT SUCCESS.")
    except Exception as e:
        print('INSERT INTO MySQL table failed.Case:%s' % e)
        # 如果发生错误就回滚
        db.rollback()
    finally:
         # 关闭数据库连接
         db.close()

def main():
    insert_record()

if __name__ == "__main__":
    main()

执行结果如下:

INSERT SUCCESS.

从MySQL客户端查看表插入结果,如图16-2所示。


2 插入数据结果

4 数据库查询

Python查询MySQL使用fetchone()方法获取单条数据,使用fetchall()方法获取多条数据。

  • fetchone():该方法获取下一个查询结果集。结果集是一个对象。
  • fetchall():接收全部返回结果行。
  • Rowcount:这是一个只读属性,返回执行execute()方法后影响的行数。

下面的示例用于查询EMPLOYEE表中salary(工资)字段大于10000的所有数据。

#! /usr/bin/python
# -*-coding:UTF-8-*-

import pymysql

def query_data():
    # 打开数据库连接
    db = pymysql.connect("localhost", "root", "root", "test")
    # 使用cursor()方法获取操作游标
    cursor = db.cursor()

    # SQL 查询语句
    sql = "SELECT * FROM EMPLOYEE WHERE INCOME > %d" % 10000
    try:
        # 执行SQL语句
        cursor.execute(sql)
        # 获取所有记录列表
        results = cursor.fetchall()
        for row in results:
           fname = row[0]
           lname = row[1]
           age = row[2]
           sex = row[3]
           income = row[4]
           create_time = row[5]
           # 输出结果
           print("first_name=%s,last_name=%s,age=%d,\n"
                  "sex=%s,income=%d,create_time=%s" %
               (first_name, last_name, age, sex, income, create_time))
    except Exception as e:
        print("Error: unable to fecth data.Error info:%s" % e)
    finally:
         # 关闭数据库连接
         db.close()

def main():
    query_data()

if __name__ == "__main__":
    main()

执行结果如下:

first_name=xiao,last_name=zhi,age=22,
sex=M,income=30000,create_time=2016-10-05 22:29:37

5 数据库更新

下面的示例将EMPLOYEE表中SEX字段值为'M'的记录的AGE字段值增加1:

#! /usr/bin/python
# -*-coding:UTF-8-*-

import pymysql

def update_table():
    # 打开数据库连接
    db = pymysql.connect("localhost", "root", "root", "test")

    # 使用cursor()方法获取操作游标
    cursor = db.cursor()

    # SQL 更新语句
    sql = "UPDATE EMPLOYEE SET AGE = AGE + 1 WHERE SEX = '%s'" % 'M'
    try:
        # 执行SQL语句
        cursor.execute(sql)
        # 提交到数据库执行
        db.commit()
print("UPDATE SUCCESS.")
    except Exception as e:
        print('UPDATE MySQL table failed.Case:%s' % e)
        # 发生错误时回滚
        db.rollback()
    finally:
         # 关闭数据库连接
         db.close()

def main():
    update_table()

if __name__ == "__main__":
    main()

执行结果如下:

UPDATE SUCCESS.

从MySQL客户端查看更新结果,如图3所示。可以发现,AGE变为23了。


3 表更新结果

6 数据库删除

删除操作用于删除数据表中对应的数据。
下面演示删除数据表EMPLOYEE中AGE大于22的所有数据。

#! /usr/bin/python
# -*-coding:UTF-8-*-

import pymysql

def delete_record():
    # 打开数据库连接
    db = pymysql.connect("localhost", "root", "root", "test")

    # 使用cursor()方法获取操作游标
    cursor = db.cursor()

    # SQL 删除语句
    sql = "DELETE FROM EMPLOYEE WHERE AGE > %d" % 22
    try:
        # 执行SQL语句
        cursor.execute(sql)
        # 提交修改
        db.commit()
        print("DELETE SUCCESS.")
    except Exception as e:
         print("DELETE RECORD FAILED.Case:%s" % e)
         # 发生错误时回滚
         db.rollback()
    finally:
         # 关闭连接
         db.close()

def main():
    delete_record()

if __name__ == "__main__":
    main()

行结果如下:

DELETE SUCCESS.

从MySQL客户端查看删除结果,如图4所示。可以看到,之前插入的一条数据被删除了。

4 记录删除

事务

事务机制可以确保数据的一致性。

事务具有4个属性:原子性、一致性、隔离性、持久性,这4个属性通常称为ACID特性。

  • 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中的所有操作要么都做,要么都不做。
  • 一致性(Consistency):事务必须使数据库从一个一致性状态变为另一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。也就是一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability):持续性也称永久性(Permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来其他操作或故障不应该对其有任何影响。

Python DB-API 2.0的事务提供了两个方法,即commit和rollback。前面删除方法中的一段代码就使用了事务(前面的插入、更新都使用了事务):

# SQL删除记录语句
ql = "DELETE FROM EMPLOYEE WHERE AGE > %d" % 22
    try:
        # 执行SQL语句
        cursor.execute(sql)
        # 提交修改
        db.commit()
        print("DELETE SUCCESS.")
    except Exception as e:
         print("DELETE RECORD FAILED.Case:%s" % e)
         # 发生错误时回滚
         db.rollback()
    finally:
         # 关闭连接
         db.close()

在Python数据库编程中,支持事务的数据库在游标建立时会自动开始一个隐形数据库事务。
commit()方法提交所有更新操作,rollback()方法回滚当前游标的所有操作。每一个方法都开启一个新事务。

调试

初学者跟数据库打交道时很容易碰到形形色色的问题,可能一个非常简单的问题也会导致你无法找到问题所在。此时我们需要处理以下几个问题:
(1)程序中有没有我们期望去做却没有实现的功能?找到运行该功能的代码,并确保这段代码如你所期望地运行了。
(2)程序中有没有运行某种不该出现的功能的代码?
(3)有没有一段代码产生的效果和你所期望的不一致?确保你完全明白这段代码,特别是牵涉对其他Python模块的函数或方法调用时。阅读调用到的函数的文档。使用简单的测试用例测试它们并检查结果。
为了能够编程,我们需要对程序如何工作有一个思维模型。如果编写了一段和你预料不同的代码,常常问题在于你的思维模型。
修正思维模型的最佳方法是将程序划分成不同部分(通常是函数和方法),并独立测试每一个部分。一旦找到模型和真实世界的偏差,就能够解决问题。
在开发过程中应当分组件进行构建和测试。当发现一个问题时,只需要检查一小部分不确认是否正确的代码即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容