一、缘起
豆豆每次到马老师家上课的时候,总要在他那巨大的书柜前徜徉许久。
那真的是一个巨大的书柜——马老师家没有书房,因为他的客厅就是书房。既然是书房,总要有个书柜吧,何况马老师是爱书之人——所以他把客厅的整整一面墙壁都变成了书柜。
豆豆站在书柜前,总感觉面前就是知识的海洋。
马老师喜欢骑行,喜欢读书。他常说,行万里路,读万卷书——或许这是世界上最美好的两件事情吧。除了心爱的自行车和单反,这个书柜就是他的骄傲。
令豆豆感觉神奇的是,这么大的书柜,马老师要找的书,往往很快就能找出来。
“马老师,你真能记住每一本书的存放位置吗?” 有一次豆豆忍不住问了一句。
“当然不是啊,我哪有那么好的记忆力?”
“可是你怎么能那么快找到任何一本书呢?”
“这个啊,一方面,这里的书虽然多,但都是我一本一本亲手整理的,分类存放。基本上你一说我就知道在哪个区域放着。” 马老师说,“另外,我还有一个程序来管理书库。如果我找不到的书,可以迅速通过书名或者作者啊这些信息找到详细资料和存放的位置信息,很方便。”
马老师说着打开了自己的Notion在线笔记。
豆豆看了一下统计信息,整整2281本书,不禁叹了一声。
“其实,我们使用PyQt也可以自己做一个简单的图书管理程序,只要有基本的录入和查询功能,就能解决日常问题了。” 马老师若有所思,“你也该了解一下数据库了,豆豆。”
“数据库?” 豆豆好像听马老师说过这个词,不过还是那种“熟悉的陌生感”。
“是的,数据库。” 马老师说,“咱们今天开始利用数据库技术做一个图书管理程序,名字就叫——万卷书库。”
二、聊聊数据库
什么是数据库?字面理解,数据库就是存储数据的“仓库”。如果你熟悉Windows系统,可能会发现Windows的资源管理器左侧的快捷方式中有一些特别的链接,比如“文档”、“音乐”和“图库”——
这里的图库并不是指一个单独的文件夹,你可以把电脑上的图片文件夹添加进来,这样就能在这里统一查看管理所有的图片。也就是说,它提供了一种图片的组织方式,便于你存储和检索。同样,数据库也是一种存储和管理数据的方式,你可以把它想象成一个巨大的Excel,里面有许多工作表,我们可以在这里管理数据。
可能你会问,我们以前学习过csv文件的读取和写入,为什么不把数据放到这种文件当中,而是要用数据库呢?要回答这个问题,我们先来回忆一下以前在学习Scratch时做的“成绩管理”程序,那时我们能操作的数据只有“列表”,也就相当于表格的“一列”,要处理“姓名”、“语文成绩”、“数学成绩”、“英语成绩”,我们就得用多个列表来存储数据:
这里,列表之间的数据关系是靠我们程序来维护的。比如要查找豆豆的数学成绩,我们就需要先在“姓名”列表找“豆豆”,得到对应的元素编号为1,然后再去数学列表中找编号同样为1的成绩。万一数据很多,不小心删除了数学这个列表中某个数字,导致对应关系失效,那我们的查询结果就全错了。另外,如果正好姓名中有不只一个“豆豆”,你想找的又不是第一个“豆豆”,那就麻烦了。因为Scratch并不提供查找列表中所有符合条件数据的功能。毕竟它只是一个用于编程学习的工具嘛。
后来我们学习了用Python操作csv格式的文件,虽然可以按整行存取数据了,你要按某个属性查找还是很困难。另外,如果你要保存的数据不只一个表格,而这些表格之间又有某些“关系”——比如一个表格存储的是班级,另一个表格存储了学生成绩,你要想计算某个班级的学科平均分怎么办?如果你想让多个用户都同时查询或修改这份数据怎么办?
数据库就是用来专业解决这些问题的。我们之所以要讲解数据库,就是因为它非常适合处理结构复杂到一定程度,数量比较大、使用者又多的数据。相对来说今天我们要处理的“图书”数据并不算复杂,数据量也不大,但通过它的处理我们可以学习到数据库的强大功能。
相对于其它的处理方式,数据库有许多优点:
那我们从哪里开始学习数据库呢?当前,数据库已经是一种非常成熟的技术,在大学里还是一门专业课,相关理论丰富,可选的产品也特别多,这些总体上可以分为两类: - 关系型数据库:存储数据在表格中,表格之间通过关系连接。这里的“关系”是一个专业术语,我们先简单理解为数据之间的关联规则就可以了。 - 非关系型数据库:存储数据的方式更加灵活,不依赖于表格。
我们今天要处理的图书是结构非常清晰的“表格”型数据,所以我们就从关系数据库中选择一个最简单的产品——SQLite 来学习吧。
三、SQLite的基本操作
SQLite这个名字可以看成是“SQL”和“Lite”的缩写,可以理解为“轻量、小型”的“SQL”,这里的SQL又是“SQL(Structured Query Language)”的缩写——它就是大名鼎鼎的“结构化查询语言”,一种专门用来与关系型数据库通信的标准编程语言,可以用于数据库的管理和操作,比如定义数据的结构、对数据进行增加、删除、修改、查询等。所以,SQLite就是“轻量SQL数据库”的意思,是一种非常容易学习和应用的数据库。
“数据库”这个词有时是一门学科或技术,有时是指一种数据库管理程序,有时又指的是用数据库管理程序建立的数据库实例,你可以根据上下文来理解。
你可能会说:怎么操作数据库还要再学一门编程语言?!我们明明是在学Python,前面学了HTML,这会又来学SQL,好麻烦啊……不过,在实际的软件开发中,多种语言配合完成一个任务是很常见的,毕竟每种编程语言都有自己擅长的应用场景。SQL就是专门用于数据库操作场景的,而且它比较简单,你只要告诉数据库“做什么”,而不用告诉它“怎么做”,对应的数据库管理软件会根据自己对SQL语言的理解去做对应的操作——这也确保了你只要学会SQL,就不用一一学习不同的数据库管理软件了(除了少量的语法“方言”和特色的函数),因为大家用的是数据库界的“普通话”——SQL来沟通的。
SQL的语法不复杂,我们可以简单地将它分成四类命令(也可以说是语句): - 数据定义类:用于创建、修改或删除数据库对象(比如数据库中的表),包括CREATE(创建)、ALTER(修改)、DROP(删除)这三种语句; - 数据操作类:通常所说的“增删改查”,包括INSERT(插入)、DELETE(删除)、UPDATE(更新)、SELECT(选择,也就是查找符合条件的数据)这四类语句; - 数据控制类:包括GRANT(对用户授权访问相应的数据库对象)和REVOKE(撤销授权); - 事务控制类:包括BEGIN TRANSACTION(开始事务)、COMMIT(提交事务)、ROLLBACK(回滚,即撤销事务)等。
每种语句也就是你可以向数据库下达的特定“命令”,使用时要根据相应的语法来编写完整语句。注意这里所有的SQL关键字都是大写,实际上小写也是可以的,大写是一种好习惯。
这些语句必须亲自动手演练才能掌握。下面我们就一步一步学习创建SQLite数据库并做一些基本的操作演示。
3.1 建立数据库和表
一般来说,商业级的数据库都会有一个专门的管理端软件,称为“数据库管理系统(DMBS)”,用它来对数据库服务提供配置和控制功能。而SQLite非常“轻量”,它不需要安装启动专门引擎或数据管理程序,直接用代码建立数据库文件就可以操作。建立文件可以直接用Python内置的sqlite3库,非常方便。
现在,新建并打开文件夹,创建一个db_test.py文件,输入以下内容:
importsqlite3# 连接到SQLite数据库# 如果文件不存在,会自动在当前目录创建一个数据库文件conn=sqlite3.connect('books.db')cursor=conn.cursor()# 创建一个名为books的表cursor.execute(''' CREATE TABLE books ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL, year INTEGER )''')conn.commit()# 提交事务conn.close()# 关闭连接
运行这个程序,会发现当前目录下多了一个books.db文件:
这个文件就是你的代码创建的SQLite数据库。但是如果你点击这个文件想查看它的内容,会得到一行提示:
也就是说,这个数据库文件不是直接可编辑的文本,需要专门的工具才能浏览它的内容。我们这里不需要去找那些专业级数据管理软件,直接在VSCode的扩展市场中查找并安装一个名为SQLite的扩展插件就可以:
安装完成后,可以在VSCode中右键点击books.db这个数据库文件,在弹出的菜单中选择Open Database,打开数据库:
点击之后,右侧并没有出现数据库的内容,而是在左侧的资源管理器中增加了一个扩展栏目SQLITE EXPLORER(SQLite 浏览工具):
点击这一栏将它打开,你就会看到books.db,展开是我们代码创建的books表,再展开会看到表里的字段(也就是属性,表中的一列)和它们的数据类型:
看到了代码创建的数据库结果,我们可以再回过来看看刚才的代码:
importsqlite3conn=sqlite3.connect('books.db')cursor=conn.cursor()
这几行就是用来导入Python内置的sqlite3库,再把数据库名称作为参数调用它的connect方法,就得到了一个针对这个数据库的连接对象,下面的操作都是基于这个连接的,这有点像文件打开再读写的过程。后面的语句生成的cursor则是“游标”,它是一个用来对数据库做具体操作的对象,我们要使用它向数据库发送命令,查询表中的内容,或者执行其它的操作。
这里,因为我们第一次运行代码,books.db文件不存在,所以连接的时候就直接创建了一个。如果同名文件存在则会自动跳过创建,不会强制重新创建,把原来的数据覆盖掉,这一点可以放心。
接下来这行代码调用游标的Execute(执行)方法,传递给数据库相应的命令语句:
cursor.execute(''' CREATE TABLE books ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL, year INTEGER )''')
为了让命令显示更直观,我们采用了多行字符串,使用三引号来界定。字符串的内容就是我们要学的第一个SQL命令CREATE(创建)。这里使用它创建的是“表(Table)”对象。
在关系数据库中,“表”是最基本的存储结构,和我们看到的CSV文件一样,它由行和列组成。行可以称为“记录”,列可以称为“字段”或“属性”。创建表的时候我们主要是指定表里面有哪些列,以及每列的数据类型、约束等。
CREATE TABLE的语法如下:
CREATETABLE【表名】(【列名】【数据类型】【约束】,【列名】【数据类型】【约束】,...);
其实就是先指定表名,后面用括号写出用英文逗号分隔开的列定义。每列的定义包括列名、数据类型和约束。 - 列名最好是英文的便于输入 - 数据类型包括如INTEGER ,TEXT ,REAL(也可以写成FLOAT), DATE等,代表整数、字符串、浮点数和日期。 - 后面的“约束”是对列的约束,比如PRIMARY KEY 代表“主键”。什么是主键?可以理解为能够唯一标识表中一行记录的列或者列的组合。比如,如果我们建立一个学生信息表,学生姓名可能重复,所以不能作为每行记录的唯一标识,其它像性别、年龄更不行,所以我们一般会用“学号”这一列来作为主键,唯一标识每一名学生。
在设计数据库表的时候有一些约定俗成的原则,为表指定主键列就是其中之一。主键可以是多个列的组合,但为了简单,我们一般用唯一的数字来做主键,这个列常常命名为id。
NOT NULL代表这一列的内容不能为空,比如学生姓名,虽然不是主键,但如果为空这一条数据就没意义,所以不能为空。UNIQUE是指列中的值必须唯一(有些列虽然不是主键,但也不能重复)。- 其它还有一些约束,当前用不到,等用到的时候我们再讲。
SQL语句后面的英文分号;是多条语句之间的分隔符,可以养成在SQL语句末尾加上分号的习惯避免错误。
虽然这个语法看上去有点麻烦,但大多数时候,我们都是直接把现有语句复制过来修改一下就可以了。需要注意的是,表的名称是不能重复的,而且最好是有意义的名称,比如books、students,一看就知道里面保存的是什么数据,而不要用table1、table2这样无意义的名称。
再看下面这两行代码:
conn.commit()# 提交事务conn.close()# 关闭连接
对于第一次接触数据库的你来说,可能不理解什么是“事务”。这里,你可以把它理解为必须整批处理的一系列操作,这些操作要么全部生效,要么全部不生效。
为什么要这样规定呢?设想以下场景:
某银行的数据库中保存着每一个储户的存款余额。如果乐乐的银行账户有1000元,悠悠有500元。现在乐乐要转给悠悠100元,银行的软件系统应该会对数据做以下操作: 1. 乐乐的余额减去100元变成900元; 2. 悠悠的余额加上100元变成600元; 3. 生成一条转账记录供双方查询。
看上去没什么问题,可是如果在极端情况下,银行的系统出现了问题(比如停电,虽然这不大可能),刚刚把乐乐的余额减去100,悠悠的余额还没增加呢,也没生成转账记录,电脑断电关机了,后面操作无法继续。这样数据库里乐乐的余额就成了900元,而悠悠是500元,凭空少了100元——因为停电产生了数据的错误。我们称之为破坏了数据库的“完整性和一致性”。
如果某银行的系统有这种漏洞,你敢在这家银行存款吗?
因此,数据库中就有了事务的概念,事务中的操作必须同时成功或撤销。比如上面的这种转账操作就是典型的事务,如果第一条执行完成了,后面没执行,整个操作会被撤销;要么是全部执行完成,“提交”到数据库,才会生效。
所以,我们在对数据进行修改后,一般都会执行连接对象的commit方法,表示要把刚刚对数据库所做的全部操作都提交到数据库。如果不提交,前面的执行就会全部撤销。
有些数据库操作会自动提交,但作为初学者我们还是养成主动提交修改给数据库的习惯吧。
与commit对应的操作是rollback(撤销),这一般用于我们的程序捕获异常,发现异常后主动撤销对数据库的操作。用到时候我们再说。
最后一行容易理解,关闭连接。就像文件打开用完之后要关闭一样,我们最好用完数据库要养成关闭的习惯。不过一般是在程序最后关闭,而不是在执行一个操作时频繁的打开和关闭数据库。
好了,建立数据库表的操作讲解完成。由于是第一次接触数据库,第一次使用SQL命令,你可能会有点头大,不过没关系,这种感觉会随着练习量的增加而逐步消失。到时候你会发现SQL其实挺简单的。
上面这段代码代如果你再次运行,就会提示出错sqlite3.OperationalError: table books already exists。那是因为数据库中已经存在了books表,所以就不能创建同名的表了。
3.2 向表中写入数据
我们完成了数据库的连接和创建,又在数据库中建立了一个名为books的表,准备用来保存图书信息。现在表里有什么内容呢?
在SQLite 浏览工具中右键点books表,选择Show Table菜单显示表内容,右侧出现的内容是空的,这是因为我们还没有向表中插入数据:
怎样向表中写入数据?当然是我们前面介绍过的insert语句,它的语法是这样的:
INSERTINTO【表名】(【列名1】,【列名2】...)VALUES(【值1】,【值2】,...)
在INSERT INTO(插入到)命令后是表名,接下来的括号中是用英文逗号分隔的字段列表,然后是VALUES(值)关键字后括号写值列表。这里的字段列表不一定要包含表中定义的全部列,但你至少得把主键列和那些定义时约束不能为空(NOT NULL)的列写上,后面按前面列名的顺序写上具体值。
现在写一个具体例子,在books表上点击右键,并选择New Query [insert],会在右侧打开一个新的代码窗口,它甚至把INSERT INTO的基本框架都写好了(前面用--开头的是注释,这是SQL语言的注释语法):
我们把VALUES后面的括号中加上相应的值,然后在空白处点鼠标右键,在弹出菜单中执行Run Query(执行查询,也就是SQL命令):
右侧又增加了一个结果窗口。如果你运行的是查询数据的命令,它会显示查询结果。但我们运行的是INSERT INTO,不是查询数据,所以列表是空的,但这时命令已经完成执行了,点击上面的SQL按钮可以看到刚刚执行的具体命令:
现在在左侧的SQLite 浏览工具中右键点books表,再次选择Show Table菜单,就能看到表中已经有一条记录了,就是我们刚刚插入到表的数据:
同时你也会看到,其实Show Table也是通过执行SQL命令来从表中查询获得数据后显示的。它执行的是SELECT(选择)语句,这是SQL中最最常用的语句了,它非常强大,可以生成非常复杂的结果,但最简单的时候就是上面显示的这样:
SELECT*FROMbooks;
SELECT关键字(选择)后面是逗号分隔的列名,如果是表中的全部列,那就直接用*来代表。后面FROM 加表名就行。这种最简单的写法是从表中取出所有的行和列。如果要获得指定条件的数据,我们可以这么写:
SELECT*from【表名】WHERE【条件1】【and或者or】【条件2】...
测试一下。通过SQLite 浏览工具中右键点books表,选择New Query [Select],并输入以下SQL语句:
-- SQLiteSELECTid,title,author,yearFROMbooksWHEREauthor='刘慈欣';
运行后发现结果和刚才显示表是一样的,毕竟表中就只有一条数据。SELECT语句的WHERE条件是可以用AND 或OR连接起来的,这里我们只用指定了作者为刘慈欣作为查询条件。
其实SELECT语句中表名后面还可以增加一些其它子语句,例如排序(ORDER BY)子句、分组(GROUP BY)子句等。这些就留到我们用到时候再说吧,你只要知道,SELECT这种类型的语句绝不是现在看到的那么简单,它能实现非常强大的功能。
3.2 数据的修改和删除
做到了数据的插入和查询,现在要考虑剩下的两种操作——更新和删除了。这也是两种很常见的操作,就是对数据库中已有内容的修改。
先看更新:
UPDATE【表名】SET【列名1】=【值1】,【列名1】=【值1】,...WHERE【条件1】【条件2】【条件3】...
UPDATE是“更新”的意思,后面跟表名,SET(设置)后面为每一个列指定新的值,最后是条件,当然也是可以用AND或OR连接的。如果你不指定WHERE条件,就会把表中每行的指定列都更新,一定要注意,这意味着原来列中的全部数据被覆盖掉。
试验一下——因为SQLite 浏览工具不支持建立UPDATE语句,我们就建立SELECT或INSERT语句,然后手动修改命令如下:
-- SQLiteUPDATEbooksSETtitle='三体2'WHEREid=1;SELECT*FROMbooks;
这里我写了两条SQL语句(用分号隔开,一条SQL语句可以分多行来写,所以我们不能用换行来分隔多条SQL语句),分别是更新和查询。执行时会先更新,再把表中数据查询出来:
可以看到,编号为1的图书名称已经修改为“三体2”了,UPDATE执行成功。
再来看删除语句的语法:
DELETEFROM【表名】WHERE【条件】
这个很简单,直接指定表名和WHERE条件就会从表中删除符合条件的数据。不带WHERE条件的语句意味着清空表里的全部数据。因此要执行这类语句时强烈建议你多检查几次代码。
把上面SQLite 浏览工具中的语句修改如下:
-- SQLiteDELETEFROMbooksWHEREid=1;SELECT*FROMbooks;
运行,这次是先执行删除,再查询:
18 执行删除语句.png表中什么都没有了,DELETE语句执行成功。
至此,我们完成了最基本的“增删改查”操作的练习。
四、总结
“今天就学习到这里吧,豆豆” 。
看到豆豆有些疲倦的神色,马老师适时中止了讲解,今天学习的内容已经不少了,需要消化吸收,于是他开始安排作业。
我们刚刚接触SQL,先不要急着了解它的全部语法,从最基础的语句,开始多做一些基本练习,在实践中体会每一种命令的用法是最好的学习方式。
SQL语言最强大的地方就是它的灵活性和表达能力,也就是前面说的,它允许我们以声明性的方式表达复杂的数据操作和查询,而不需要编写具体的程序代码来处理数据。程序员或者DBA(也就是数据库管理员)只管下命令,具体操作让数据库去做。这就让数据库管理和数据分析变得更高效了。
今天的作业:
- 建立一个学生信息数据库,命名为
students.db - 在数据库中建立一张表,表名为
students,里面至少包含学号、姓名、性别、年龄、班级、出生日期、联系方式、地址等属性; - 使用语句向表中插入三条数据,姓名分别是乐乐、悠悠和豆豆,具体属性自定;
- 使用语句查询信息,并测试更新和删除数据后再次查询。