SQL 基础知识

在前面的课程中,您学习了如何将联网数据整合到自己的应用中,还学习了如何使用协程来处理并发任务。在本衔接课程中,您将学习另一项能帮助您构建优质应用的基本 Android 开发技能:实现数据持久性。即使您以前从没听过“持久性”这个词,您可能也曾经在使用应用时遇到过数据持久性现象。从列购物清单到在相册应用中翻看几年前的照片,再到暂停某一款游戏然后又恢复,应用都能利用数据持久性提供无缝衔接的用户体验。虽然用户很容易认为这些功能是理所当然的,但持久保留数据是开发者构建高品质应用的必备技能。

在本单元的后面部分,您将详细了解 Android 应用中的数据持久性。此外,您还将了解一个名为 Room 的库,应用可以借助它对数据库执行读写操作。不过,在深入了解如何在 Android 应用中实现数据持久性之前,您必须先熟悉关系型数据库的基础知识以及如何使用 SQL(“结构化查询语言”的缩写)读取和操作数据。如果您已经熟悉这些概念,那您可以利用本课进行复习,确保您在学习 Room 时对这些概念记忆犹新。如果您不熟悉,那也完全没有问题!我们并不要求您现在具备任何数据库知识。此 Codelab 结束时,您将掌握开始学习如何在 Android 应用中使用数据库所需的全部基础知识和技能。

前提条件

  • 在 Android Studio 中导航到某个项目。

学习内容

  • 关系型数据库的结构:表、列和行
  • SELECT 语句,包括 WHEREORDER BYGROUP BYLIMIT 子句
  • 如何使用 SQL 插入、更新和删除行

所需条件

  • 一台安装了 Android Studio 的计算机。

什么是关系型数据库?

在计算中,数据库只是一种可通过电子方式访问和写入的结构化数据集合。凡是您可使用 Kotlin 在应用中表示的信息,数据库都可以存储与之相同的信息。在移动设备上,数据库通常用于保存正在运行的应用中的数据,以便在下次打开该应用时无需从互联网等其他来源检索这些数据即可对其进行访问。这就是我们所说的数据持久性。

谈到数据持久性时,您往往会听到“关系型数据库”一词。关系型数据库是一种常用的数据库类型,它将数据整理成表、列和行。编写 Kotlin 代码时,您会创建用于表示对象的类。关系型数据库中的表具有与其相同的作用。不过,除了表示数据之外,表还可以引用其他表,因此您可以在表之间建立关系。一个典型的例子是由“学生”、“教师”和“课程”组成的表。一门课程会有一位教师,但一名学生可能有多门课程。数据库可以表示这些表之间的关系,这就是您经常会听到“关系型数据库”一词的原因。

ef61dd2663e4da82.png

可以用表之间的关系表示现实中的关系。

表、列和行

定义表或您要表示的数据仅仅是创建关系型数据库的第一步。除此之外,您还需要考虑每个表中要存储的具体信息。具体属性以列表示。列由名称和数据类型组成。您已经通过在 Kotlin 中使用类熟悉了属性。您可以用同样的方式来看待 SQL 表。表就像类定义一样,用于描述您想表示的“事物”的类型。列是使用表中每个条目所创建的“事物”的具体属性。

PlantidINTEGERspeciesTEXT (string)nameTEXT (string)colorTEXT (string)

GardenidINTEGERnameTEXT (string)lengthINTEGERwidthINTEGER

Plant

id

整数

species

文本(字符串)

name

文本(字符串)

color

文本(字符串)

Garden

id

整数

name

文本(字符串)

length

整数

width

整数

表的各个条目称为行。行就像是 Kotlin 中类的实例。每行都有对应于每列的数据。表提供的是模板,而行定义的是表中存储的实际数据。

id

species

name

color

1

茶树

绿色

2

紫松果菊

紫松果菊

紫色

3

香阿魏

阿魏

绿色

主键

请注意,在上面的示例中,有一列是 id 属性。虽然自然界中的植物种类或您在数据库中表示的任何其他信息可能没有已经方便地编好号的 ID,但是对于数据表中的行来说,具有某种唯一标识符很重要。此标识符通常称为主键,并且对于表中的每行而言具有唯一性。如果您需要在一个数据表中引用另一个表中的行,主键就非常有用。举例而言,在另外一个名为“garden”的表中,您希望将花园与园中的所有植物种类相关联。您可以使用 plants 表中的主键来引用 garden 表中或数据库里任何其他表中某个条目内的植物。

正是有了主键,我们才有可能在关系型数据库中建立关系。虽然您在本课程中不会使用包含多个表的数据库,但拥有唯一 ID 也将有助于查询、更新和删除表中的现有项。

数据类型

就像定义 Kotlin 类的属性一样,数据库中的列可以有许多可能的数据类型。列可以表示字符、字符串、数字(无论是否为小数)或二进制数据。日期和时间等其他数据可以用数字表示,也可以用字符串表示,具体视用例而定。使用 Room 时,您使用的主要是 Kotlin 类型,但它们会在后台映射到 SQL 类型。

SQL

访问关系型数据库时,无论是直接访问还是使用 Room 等库进行访问,都需要使用 SQL

什么是 SQL?SQL(有时读作“sequel”)代表“结构化查询语言”,您可以借助它来读取和操作关系型数据库中的数据。不过不用担心,您不必仅仅为了在应用中实现数据持久性而学习一种全新的编程语言。与 Kotlin 等编程语言不同,SQL 只包含几种用于对数据库执行读写操作的语句。如果了解每种语句的基本格式,这就只是简单的填空问题了。您只需要填入您将在数据库中读取或写入的具体信息即可。

下面介绍了最常用的 SQL 语句,以及您将要使用的 SQL 语句。

SELECT

从数据表中获取特定信息,并可按各种方式对结果进行过滤和排序。

INSERT

向表中添加新行。

UPDATE

更新表中现有的一行或多行。

DELETE

从表中移除现有的一行或多行。

现在,您需要有一个数据库,然后才能使用 SQL 执行操作。在接下来显示的屏幕上,您将设置一个示例项目,其中包含的数据库可供您用来练习 SQL 查询。

本课要下载的起始代码与以前的 Codelab 略有不同。我们会提供一个简单的 Android Studio 项目来创建可供您用于练习 SQL 查询的数据库,而不是让您基于现有项目来构建项目。运行一次应用后,您将能够使用名为 Database Inspector 的 Android Studio 工具访问数据库。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

5b0a76c50478a73f.png

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

21f3eec988dcfbe9.png

  1. Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用可以正常使用。
  5. Project 工具窗口中浏览项目文件,了解应用的实现方式。

在继续学习下一部分之前,请完成以下步骤,以确保您设置的是入门级项目。

  1. 运行应用。应用应显示一个如下屏幕。

3c62c10fad7c0136.png

  1. 在 Android Studio 中,依次选择 View > Tool Windows > Database Inspector,打开 Database Inspector。
  2. 您应该会在底部看到一个新的标签页,标有“Database Inspector”。加载过程可能需要几秒钟的时间,但您应该会在左侧看到一个数据表列表,您可以选择对其中的数据表运行查询。

8c2b12249b4f652a.png

完成以下练习时,您将在 Database Inspector 中运行查询。确保在左侧窗格中选择正确的表 (park),然后点击 Open New Query Tab 按钮,此时您应该会看到一个可供您输入 SQL 命令的文本框。

bb06b5ce9ac4ba72.png

SQL 语句是一种用于访问(读取或写入)数据库的命令,有点像一行代码。您在 SQL 中可以执行的最基本的操作是简单地获取某个表中的所有数据。如需执行此操作,首先要使用 SELECT 语句,它表示您要读取数据。接下来,添加一个星号 (*)。此处是用于指定要选择的列的,使用星号是一种简写形式,代表选择所有列。然后,使用 FROM 关键字,后跟数据表的名称 park。在 Database Inspector 中运行以下命令,然后您会看到包含所有行和列的整个表。

SELECT * FROM park

如果只想选择具体的某一列,而不是数据表中的所有列,可以指定列名称。

SELECT city FROM park

您还可以选择多个具体列,每列用英文逗号分隔。

SELECT name, established, city FROM park

有时,选择数据库中的所有行并非完全必要。您可以添加 SQL 语句的子句部分,进一步缩小结果的范围。

其中一个子句是 LIMIT,可用于对返回的行数设置限制。因此,以下查询只会返回前五个结果,而不是返回全部 23 个结果。

SELECT name FROM park
LIMIT 5

最常用也最实用的一个子句是 WHERE 子句。使用 WHERE 子句,您可以根据一个或多个列过滤结果。

SELECT name FROM park
WHERE type = "national_park"

还有一个“不等于”(!=) 运算符。以下查询列出了面积超过 100,000 英亩且不属于 recreation_area 的所有公园。通过 WHERE 子句,您还可以使用 ANDOR 等布尔运算符添加多个条件。

SELECT name FROM park
WHERE type != "recreation_area"
AND area_acres > 100000

练习

SQL 查询对于回答有关数据的各种问题非常有用,而最好的练习方式是自己动手编写查询。在接下来的几个步骤中,您将有机会编写查询来回答特定问题。请务必先在 Database Inspector 中进行测试,然后再继续。

所有练习都将以前面各部分积累的知识为基础,而且此 Codelab 结尾部分还会通过演示来检查您的答案。

您编写的第一个查询只是简单地返回数据库中的每行。

SELECT * FROM park

不过,也许您并不想返回一个很长的结果列表。SQL 还提供了聚合函数,可以帮助您将数据缩减为有意义的单一值。例如,假设您想知道 park 表中的行数。您可以不使用 SELECT * ...,而是使用 COUNT() 函数并传入 *(代表所有行)或行名称,查询就会改为返回所有行的计数。

SELECT COUNT(*) FROM park

另一个有用的聚合函数是 SUM() 函数,用于将某列中的值相加。以下查询仅过滤国家公园(因为只有这些条目的 park_visitors 列为非 null 值),并将每个公园的游客总数相加。

SELECT SUM(park_visitors) FROM park
WHERE type = "national_park"

值得注意的是,您仍然可以对 null 值使用 SUM(),但是值只会被视为零。以下查询将返回与上面的查询相同的结果。不过,当您开始在应用中使用 SQL 时,依然建议您尽可能具体一些以避免错误。

SELECT SUM(park_visitors) FROM park

除了聚合值之外,还存在其他有用的函数,例如分别用于获取最大值和最小值的 MAX()MIN()

SELECT MAX(area_acres) FROM park
WHERE type = 'national_park'

获取 DISTINCT 值

您可能会注意到,在某些行中,列的值与其他行相同。例如,type 列可能存在的值数量有限。您可以使用 DISTINCT 关键字消除查询结果中的重复值。例如,如需获取 type 列的所有唯一值,您可以使用以下查询。

SELECT DISTINCT type FROM park

您也可以在聚合函数中使用 DISTINCT,这样就可以直接返回计数,而无需您自己列出唯一的 type 并计算其总数。

SELECT COUNT(DISTINCT type) FROM park

练习

请花点时间运用您学到的知识,看看您能否编写出以下查询。请务必使用 Database Inspector 验证您的代码能否正常运行。

在前面的示例中,可能很难查找具体条目。万幸的是,您还可以使用 ORDER BY 子句对 SELECT 语句的结果进行排序。在查询末尾的 WHERE 子句(若有)后添加一个 ORDER BY 子句,然后只需指定您想作为排序依据的列名称即可。以下示例获取数据库中每个公园的名称,但它是按字母顺序对结果排序。

SELECT name FROM park
ORDER BY name

默认情况下,结果依升序排序,但您可以在 order by 子句中添加 ASCDESC 关键字,让结果依升序或降序排序。您无需指定 ASC,因为第一个查询本来就是依升序列出结果,但是如果您想获取依降序排序的结果,就可以将 DESC 关键字添加到 ORDER BY 子句末尾。

SELECT name FROM park
ORDER BY name DESC

为了让结果更容易阅读,您还可以选择按列对结果进行分组。在 ORDER BY 子句(若有)前,您可以选择指定 GROUP BY 子句和一个列。这样做会针对 GROUP BY 中的列将结果分成子集,而且对于每一列,结果都会根据查询的其余部分进行过滤和排序。

SELECT type, name FROM park
GROUP BY type
ORDER BY name

通过示例能够对此获得最深刻的理解。您可以查看每种类型存在多少个公园,并获取每种类型的单独计数,而不是计算数据库中所有公园的总数。

SELECT type, COUNT(*) FROM park
GROUP BY type
ORDER BY type

练习

请花点时间运用您学到的知识,看看您能否编写出以下查询。请务必使用 Database Inspector 验证您的代码能否正常运行。

问题 4:编写一个 SQL 查询,获取游客数量最多的前 5 家公园的名称及其游客数量,并依降序排序。

为了充分利用借助 Room 在 Android 应用中持久保留数据的能力,您需要能够写入数据。除了查询数据库之外,还有用于插入、更新和删除行的 SQL 语句。当您稍后在衔接课程 2 中学习使用 Room 写入数据时,需要掌握有关这些语句的基础知识。

INSERT 语句

如需添加新行,应使用 INSERT 语句。INSERT 语句后跟 INTO 关键字和您要在其中添加行的表的名称。在 VALUES 关键字后的括号中,(依序)为每列提供值,并用英文逗号分隔每个值。INSERT 语句的格式如下所示。

INSERT INTO table_name
VALUES (column1, column2, ...)

如需向 park 表添加一行,INSERT 语句应如下所示。值与 park 表中的列的定义顺序相符。请注意,有些数据并未指定。现在这样做是没有问题的,因为您在插入行后随时可以对其进行更新。

INSERT INTO park
VALUES (null, 'Googleplex', 'Mountain View', 12, null, 0, '')

另请注意,您要传入 null 作为 ID。虽然您可以提供一个具体的数字,但这种做法其实并不方便,因为应用会不得不跟踪最新 ID 以确保没有重复项。不过,您可以配置数据库,使主键自动递增,这里就是这样做的。采用这种做法,您可以传入 null,然后由系统自动选择下一个 ID。

使用 WHERE 子句指定名为 "Googleplex" 的公园,以验证该条目是否已创建。

SELECT * FROM park
WHERE name = 'Googleplex'

UPDATE 语句

创建行后,您可以随时更改其内容。您可以使用 UPDATE 语句完成此操作。与您已经看到的所有其他 SQL 语句一样,您首先需要指定表名称。在 SET 子句中,只需将您要更改的每列设为新值即可。

UPDATE table_name
SET column1 = ...,
column2 = ...,
...
WHERE column_name = ...
...

在 Googleplex 条目中,一个现有属性已更新,一些其他字段已填充(这些字段之前有值,但该值是空字符串 "")。您可以使用 UPDATE 语句一次更新多个(或所有)字段。

UPDATE park
SET area_acres = 46,
established = 1088640000,
type = 'office'
WHERE name = 'Googleplex'

查看查询结果中反映的更新

SELECT * FROM park
WHERE name = 'Googleplex'

DELETE 语句

最后,您还可以使用 SQL 命令从数据库中删除行。同样,指定表名,然后像使用 SELECT 语句时一样,使用 WHERE 子句针对要删除的行提供条件。由于 WHERE 子句可以与多个行匹配,因此您可以使用单个命令删除多行。

DELETE FROM table_name
WHERE <column_name> = ...

Googleplex 不是国家公园,因此请尝试使用 DELETE 语句从数据库中移除此条目。

DELETE FROM park
WHERE name = 'Googleplex'

使用 SELECT 语句进行验证,确保该行已删除。查询应该不会返回结果,这意味着所有名为“Googleplex”的行均已成功删除。

SELECT * FROM park
WHERE name = 'Googleplex'

插入、更新和删除数据方面要介绍的内容就是这些。您只需要知道要执行的 SQL 命令的格式,并指定与数据库中的列相匹配的值。下一个 Codelab 将介绍 Room,届时您的学习重点将主要集中于从数据库读取数据上。插入、更新和删除数据将在衔接课程 2 中详细介绍。

希望这些实践练习有助于巩固您对 SQL 概念的理解。如果您在理解任何概念时遇到困难,或者想检查您的答案,尽可参考下方的答案

问题 1:编写一个 SQL 查询来获取游客数量低于 1,000,000 人的所有公园的名称。

此问题询问公园名称(单个列)并要求游客数量低于 1,000,000 人,可在 WHERE 子句中指定此条件。

SELECT name FROM park
WHERE park_visitors < 1000000

问题 2:编写一个 SQL 查询来获取 park 表中不同城市的数量

列的总计数可使用 COUNT() 函数计算得出,但由于您只需要不同的城市(因为有些城市有多座公园),因此您可以在 COUNT() 函数中的列名称前使用 DISTINCT 关键字。

SELECT COUNT(DISTINCT city) FROM park

问题 3:编写一个 SQL 查询来获取位于旧金山的公园的游客总数。

游客总数可使用 SUM() 函数计算得出。此外,您还需要使用一个 WHERE 子句,以便仅指定位于旧金山的公园。

SELECT SUM(park_visitors) FROM park
WHERE city = "San Francisco"

问题 4:编写一个 SQL 查询,获取游客数量最多的前 5 家公园(仅名称)及其游客数量,并依降序排序。

该查询需要获取 name 和 park_visitors 两列。使用 ORDER BY 子句,在 park_visitors 列中依降序对结果进行排序。由于您不需要对另一列中的结果分组并在相应组中排序,因此无需使用 GROUP BY 子句。

SELECT name, park_visitors FROM park
ORDER BY park_visitors DESC
LIMIT 5

总结:

  • 借助关系型数据库,您可以存储整理成表、列和行的数据。
  • 您可以使用 SQL SELECT 语句从数据库中检索数据。
  • 您可以在 SELECT 语句中使用多个不同子句(包括 WHEREGROUP BYORDER BYLIMIT)让查询变得更具体。
  • 您可以使用聚合函数将多个行中的数据合并到一列中。
  • 您可以分别使用 SQL INSERTUPDATEDELETE 语句添加、更新和删除数据库中的行。

了解详情