第十三章 MySQL数据库与JDBC编程

本章要点

  • 关系数据库和SQL语句
  • DML语句和语法
  • DDL语句和语法
  • 简单查询语句的语法
  • 多表连接查询
  • 子查询
  • JDBC数据库编程步骤
  • 执行SQL语句的三种方法
  • 使用PreparedStatement执行SQL语句
  • 使用CallableStatement调用存储过程
  • 使用ResultSetMetaData分析结果集元数据
  • 理解并掌握RowSet、RowSetFactory
  • 离线RowSet
  • 使用RowSet控制分页
  • 使用DatabaseMetaData分析数据库元数据
  • 事务的基础知识
  • SQL语句中的事务控制
  • JDBC编程中的事务控制

这里我是真的对SQL一点基础都没有,纯粹的从0开始学习,SQL数据库也是必须学习的内容,必须专心学好!一起加油!
Java通过JDBC可以非常简单方便的操作各种主流数据库,这也是Java语言的巨大魅力所在。
由于Java的跨平台性,所以使用JDBC编写的程序不仅可以跨数据库也可以跨平台,具有非常优秀的可移植性。
程序使用JDBC API以统一的方式来连接不同的数据库,然后通过Statement对象来执行标准的SQL语句,并可以获得SQL语句访问数据库的结果,因此掌握标准的SQL语句是学习JDBC编程的基础。

JDBC基础

JDBC全程是 Java Database Connectivity,即Java数据库连接,它是一种可执行SQL语句的Java API。程序可以通过JDBC API连接到关系数据库,并使用结构化查询语言(SQL,数据库标准的查询语言)来完成对数据库的查询、更新。
与其他数据库编程环境相比,JDBC为数据库开发提供了标准的API,所以使用JDBC开发的数据库可以实现跨平台运行,而且可以跨数据库(如果全部使用标准的SQL)。也就是说,如果使用JDBC开发一个数据库应用,则该应用既可以在Windows平台运行,也可以在UNIX等其他平台上运行;既可以使用MySQL数据库,也可以使用Oracle等数据库,而程序无须进行任何修改。

JDBC简介

通过JDBC就可以实现同一种API访问不同的数据库系统。有了JDBC API 就没有必要为了访问其中一种数据库特意区学习一组API,不同的数据库使用不同的数据库驱动程序即可。
最早的时候,Sun公司希望自己开发一组JavaAPI,程序员通过这组JavaAPI即可操作所有的数据库系统,但后来Sun发现这个目标具有不可实现性——因为数据库系统太多了,而且各数据库系统的内部特性又各不相同。后来Sun就指定了一组标准的API,它们只是接口,没有提供实现类——这些实现类由各数据库厂商提供实现,这些实现类就是驱动程序。而程序员使用JDBC时只需要面向标准的JDBCAPI编程即可,当需要在数据库之间切换时,只要更换不同的实现类(即更换数据库驱动程序)就行,这是面向接口编程的典型应用。
不同的数据库跨平台实现需要不同的驱动程序,下图显示了JDBC驱动示意图。
正是通过JDBC驱动的转换,才使用相同JDBCAPI编写的程序,在不同的数据库系统上运行良好。Sun提供的JDBC可以完成以下三各基本工作。
第十三章 MySQL数据库与JDBC编程_第1张图片

  • 建立于数据库的连接。
  • 执行SQL语句。
  • 获得SQL语句的执行结果。

通过JDBC的这三各功能。应用程序即可访问、操作数据库系统。

JDBC驱动程序

数据库驱动是JDBC程序和数据库之间的转换层,数据库驱动程序负责将JDBC调用映射成特定的数据库调用。下图显示了JDBC示意图。
第十三章 MySQL数据库与JDBC编程_第2张图片
大部分数据库系统,例如Oracle和Sybase等,都有相对应的JDBC驱动程序,当需要连接某个特定的数据库时,必须有相应的数据库驱动程序。
还有一种名为ODBC的技术,其全称为 Open Database Connectivity,即开放数据库连接。ODBC和JDBC很像,严格来说,应该是JDBC模仿了ODBC的设计。ODBC也允许应用程序通过一组通用的API访问不同的数据库管理系统,从而使得基于ODBC的应用程序可以在不同数据库之间切换,同样,ODBC也需要各数据厂商提供相应的驱动程序,而ODBC则负责管理这些驱动程序。

JDBC驱动通常由如下4种类型。

  • 第1种JDBC驱动:称为JDBC-ODBC桥,这种驱动最早实现的JDBC驱动程序,主要目的是为了快速推广JDBC。这种驱动将JDBCAPI映射到ODBCAPI。这种方式在Java8中已经被删除了。
  • 第2种JDBC驱动:直接将JDBC API应色号成数据库特定的客户端API。这种驱动包含特定数据库的本地代码,用于访问特定数据库的客户端。
  • 第3种JDBC驱动:支持三层结构的JDBC访问方式,主要用于Applet阶段,通过Applet访问数据库。
  • 第4种JDBC驱动:是纯Java的,直接与数据库实例交互。这种驱动是智能的,它知道数据库使用的底层协议。这种驱动是目前最流行的JDBC驱动。

早期为了让Java程序操作Access这种伪数据库,可能需要使用JDBC-ODBC桥,但JDBC-ODBC桥不适合在并发访问数据库的情况下使用,但固有的性能和扩展能力也非常有限,因此Java8删除了JDBC-ODBC桥驱动。基本上Java应用也很少使用Access这种伪数据库。

通常建议选择第4种JDBC驱动。

SQL语法。

SQL语句是通用的命令语句,而JDBC API只是执行SQL语句的工具,JDBC允许对不同平台、不同数据库采用相同的编程接口来执行SQL语句,在开始JDBC编程之前掌握基本的SQL知识。

除标准的SQL语句之外,所有的数据库都会在标准的SQL语句基础上进行扩展,增加一些额外的功能,这些额外的功能属于特定的数据库系统,不能在所有的数据库系统上都通用。因此,如果向让数据库应用程序可以跨数据库运行,则应该尽量少用这些属于特定数据库的拓展。

关系数据库基本概念和MySQL基本命令

严格来说,数据库(Database)仅仅是存放用户数据的地方。当用户访问、操作数据库中的数据时,就需要数据库管理系统的帮助。数据库管理系统的全称是Database Management System,简称DBMS。习惯上常常把数据库和数据库管理系统笼统地称为数据库,通常所说的数据库即包括存储用户数据的部分,也包括管理数据库的管理系统。
DBMS是所有数据的知识库,它负责管理数据的存储、安全、一致性、并发、恢复和访问等操作。DBMS有一个数据字典(有时也被称为系统表),用于存储它拥有的每个事务的相关信息,例如名字、结构、位置和类型,这种关于数据的数据也被称为元数据。
在数据库发展历史中,按时间排序主要出现了如下几种类型的数据库系统。

  • 网状型数据库
  • 层次型数据库
  • 关系数据库
  • 面向对象数据库

在上面4种数据库系统中,关系数据库是理论最成熟、应用最广泛的数据库。从20世纪70年代末开始,关系数据库理论逐渐成熟,随之涌现出大量商用的关系数据库。关系数据库理论经过30多年的发展已经相当完善,在大量数据的查找、排序操作上非常成熟且快速,并对数据库系统的并发、隔离有非常完善的解决方案。
面向对象数据库则是由面向对象编程语言催生的新型数据库。目前有些数据库系统如Oracle11g等开始增加面向对象特性,但面向对象数据库还没有大规模地商业应用。
对于关系数据库而言,最基本的数据存储单元就是数据表,因此可以简单地把数据库想象成大量数据表的集合(当然,数据库绝不仅由数据表组成)。
数据表是存储数据的逻辑单元,可以把数据表想象成由行和列组成的表格,其中每一行也被称为一条记录,每一列也被称为一个字段。为数据库建表时,通常需要指定该表包含多少列,每列的数据类型信息,无须指定该数据表包含多少行——因为数据表的行是动态改变的,每行用于保存一条用户数据。除此之外,还应该为每个数据表指定一个特殊列,该特殊列的值可以唯一地标识此行的记录,则该特殊列被称为主键列。
MySQL数据库的一个实例(Sever Instance)可以同时包含多各数据库,MySQL使用如下命令来查看当前实例下包含多少数据库。

show databases;

MySQL默认已分号作为每条命令的结束符,所以在每条MySQL命令结束后都应该输一个英文分号(;)。
如果用户需要创建新的数据库,则可以使用如下命令:

create database [IF NOT EXISTS] 数据库名;

如果用户需要删除指定数据库,则可以使用如下命令:

drop database 数据库名;

建立了数据库之后,如果像操作该数据库(例如为该数据库建表,在该数据库中执行查询等操作),则需要进入该数据库。进入指定数据库可以使用如下命令。

use 数据库名;

进入指定数据库后,如果需要查询该数据库下包含多少各数据表,则可以使用如下命令:

desc tables;

如果想查看指定数据表的表结构(查看该表有多少列,每列的数据类型等信息),则可以使用如下命令:

desc 表名

下面截图将显示使用MySQL命令行客服端执行这些命令的效果。
第十三章 MySQL数据库与JDBC编程_第3张图片

这是初次登入MySQL的效果,这里我的用户名是默认,默认为 root,密码也是简单的随便设置了一个
这里推荐的登入格式为:

mysql -h localhost -P 3306 -u root -p
@mysql 数据库
@-h 要连接的服务端
@localhost 本地服务器
@-P 这里和小写的-P不同 大写代表了端口
@3306 MySQL默认端口为3306
@-u 用户名
@root MySQL默认的用户名
@-p 输入密码

在这里插入图片描述
这里一定要先设置一下环境变量
将自己安装好的MySQL的bin路径添加到系统的path环境变量中。
第十三章 MySQL数据库与JDBC编程_第4张图片
在这里插入图片描述
这里我们先测试 上面的 show databases;代码
第十三章 MySQL数据库与JDBC编程_第5张图片
这里一定不要忘记加上英文分号否则容易出现下面的情况,可以用快捷键退出,比较麻烦。
第十三章 MySQL数据库与JDBC编程_第6张图片
尤其在创建表单填写数据时,这种操作很麻烦。
在这里插入图片描述
删除指定数据库
在前期刚学习时不建议随意尝试删除数据库,应在熟悉并掌握后再去尝试!
在这里插入图片描述
新建数据库:
通过指令(代码):

create database if not exists xyz;新建数据库xyz

第十三章 MySQL数据库与JDBC编程_第7张图片
第十三章 MySQL数据库与JDBC编程_第8张图片
我们通过删除数据库代码来删除新建的 xyz数据库。
第十三章 MySQL数据库与JDBC编程_第9张图片
第十三章 MySQL数据库与JDBC编程_第10张图片
在这里插入图片描述
查看当前数据库内所有数据表。
注意这里的指令。
在这里插入图片描述
第十三章 MySQL数据库与JDBC编程_第11张图片
截取了部分的内容,内容过长。
第十三章 MySQL数据库与JDBC编程_第12张图片
很显然,use的语句效果是打开指定数据库:

//use 数据库名
use sys;//一定要记得加上分号(英文)

为了更方便地使用登入数据库的命令,可以将MySQL安装路径下的bin目录添加到系统PATH环境变量中。实际上,开始菜单中的MySQL.8.0Command Line Client - Unicode菜单项就是一条mysql命令。
MySQL数据库通常支持如下两种存储机制。

  • MyISAM:这是MySQL早期默认的存储机制,对事务支持不够好。
  • InnoDB:InnoDB提供事务安全的存储机制。InnoDB通过建立行级锁来保证事务完整性,并以Oracle风格的共享锁来处理Select语句。系统默认启动InnoDB存储机制,如果不想使用InnoDB表,则可以使用skip-innodb选项。

对比两种存储机制,不难发现InnoDB比MyISAM多了事务支持的功能,而事务支持是JavaEE最重要的特性,因此通常推荐使用InnoDB存储机制。如果使用5.0以上的版本的MySQL数据库系统,通常无须指定数据表的存储机制,因为系统默认使用InnoDB存储机制。如果需要再建表时显式指定存储机制,则可以再标准建表语法的后面添加下面任意一句:

  • ENGINE=MyISAM:强制使用MyISAM存储机制。
  • ENGINE=InnoDB:强制使用InnoDB存储机制。

SQL语句基础

SQL的全称是Structured Query Language,也就是结构化查询语言。SQL是操作和检索关系数据库的标准语言,标准的SQL语法可用于操作任何关系数据库。
使用SQL语句,程序员和数据库管理员(DBA)可以完成如下任务。

  • 在数据库中检索信息。
  • 对数据库的信息进行更新。
  • 改变数据库的结构。
  • 更改系统的安全设置。
  • 增加或回收用户对数据、表的许可权限。

在上面5各任务中,一般程序员可以管理前3个任务,后面2个任务通常由DBA来完成。
标准的SQL语句通常可分为如下几种类型。

  • 查询语句:主要由select关键字完成,查询语句是SQL语句中最复杂、功能最丰富的语句。
  • DML(Data Manipulation Language,数据库操作语言)语句:主要由insert、update和delete三个关键字完成。
  • DDL(Data Definition Language,数据定义语言)语句:主要由create、alter、drop和truncate四个关键字完成。
  • DCL(Data Control Language,数据控制语言)语句:主要由grant和revoke两个关键字完成。
  • 事务控制语句:主要由commit、rollback和savepoint三个关键字完成。

SQL语句的关键字不区别大小写,也就是说,create和CREATE的作用完全一样。在上面五种SQL语句中,DCL语句用于为数据库用户授权,或者回收指定用户的权限,通常无须程序员操作,所以本节不打算介绍任何关于DCL的知识。
在SQL命令中也可能需要使用标识符,标识符可用于定义表名、列名,也可用于定义变量等。这些标识的明明规则如下。

  • 标识符通常必须以字母开头。
  • 标识符包括字母、数字和三个特殊字符(#_$)。
  • 不要使用当前数据库系统的关键字、保留字,通常建议使用多个单词连缀而成,单词之间以_分隔。
  • 同一个模式下的对象不应该同名,这里的模式指的是外模式。

掌握了SQL的这些基础知识后,下面将分类介绍各种SQL语句。

truncate是一个特殊的DDL语句mtruncate在很多数据库中都被归类为DDL,它相当于先删除指定的数据表,然后再重建该数据表。如果使用MySQL的普通存储机制,truncate确实是这样的,但如果使用InnoDB存储机制,则比较复杂,在MySQL5.0.3之前,truncate和delete完全一样;在MySQL5.0.3之后,truncate table比delete效率高,但如果该表被外键约束所参照,则依然被映射成delete操作。当使用快速truncate时,该操作会重设自己增长计数器,在MySQL5.0.13之后,快速truncate总是可用,即比delete性能要好,关于truncate的用法,参考文章后面内容。

DDL语句

DDL语句是操作数据库对象的语句,包括创建(create)、删除(drop)和修改(alter)数据库对象。
前面已经介绍过,最基本的数据库对象是数据表,数据表是存储数据的逻辑单元。但数据库里绝对不仅数据表,数据库里可包含如下表所示的几种常见的数据库对象。
第十三章 MySQL数据库与JDBC编程_第13张图片
因为存在上面几种数据库对象,所以在create后可以紧跟不同的关键字。例如,建表应使用create table,建索引应使用 create index,建视图应使用 create view … 在drop和alter后也需要添加类似的关键字来标识删除、修改该哪种数据库对象。
因为函数、存储过程和触发器属于数据库编程内容,而且需要大量使用数据库特性,等下阶段 MySQL的专门学习再介绍这些内容!

1.创建表的语法
标准的创建语句的语法如下:

create table[模式名.]表明
{
	#可以由多个定列定义
	columnName1 datatype [default expr],
	...
}

上面语法中圆括号里可以包含多个列定义,每个列定义之间以英文逗号(,)隔开,最后一个列定义不需要使用英文逗号,而是直接以括号结束。
前面已经讲过,建立数据表只是建立表结构,就是指定该数据表有多少列,每行的数据类型,所以建立表语句的重点就是圆括号里的列定义,列定义由列名、列类型和可选的默认值组成。
列定义有点类似于Java里的变量定义,与变量定义不同的是,列定义时将列名放在前面,列类型放在后面。如果要指定列默认值,则使用 default关键字,而不是使用等于号(=)。

例如下面的建表语句:

create table test{
	#整形通常用 int
	test_id int,
	#小数点数
	test_price decimal,
	#普通长度文本,使用default指定美容唔知
	test_name varchar(255),defalut 'xxx',
	#大文本类型
	test_desc text;
	#图片
	test_img blob,
	test_date datetime
};

建表时需要指定每列的数据类型,不同数据库所支持的列类型不同,这需要查阅不同的数据库相关的文档。MySQL支持如下所示的几种列类型。
第十三章 MySQL数据库与JDBC编程_第14张图片
第十三章 MySQL数据库与JDBC编程_第15张图片
上面是比较常见的建表语句,这种建表语句只是创建一个空表,该表里没有任何数据。如果使用子查询建表语句,则可以在建表的同时插入数据。子查询的建表语句的语法如下:

create table[模式名.]表明[column[,column...]]
as subquery;

上面语法中新表的字段列表必须与子查询中的字段列表数量匹配,创建新表时的字段列表可以省略。如果省略了该字段列表,则信标的列名与选择结果完全相同。下面语句使用子查询来建表。

#创建hehe数据表,该数据表和user_inf完全相同,数据也完全相同
create table hehe
as
select * from user_inf;

因为上面语句是利用子查询来建立数据表,所以执行该SQL语句要求数据库中已存在user_inf数据表,负责程序将出现错误。
当数据表创建成功后,MySQL使用information_schema数据库里的TABLES表来保存该数据库实例中的数据表,用户可以通过查询TABLES表来获取该数据库的表信息。

2.修改表结构的语法
修改表结构使用 alter table,修改表结构包括增加列定义、修改列定义、删除列、重命名列等操作。增加列定义的语法如下:

alter table 表名
add
{
	#可以有多个列定义
	column_namel datatype[default expr],
	...
}

上面的语法格式中圆括号部分与建表语法的圆括号部分完全相同,只是此时圆括号里定义是追加到已有表的列定义后面。还有一点需要指出,如果只是新增一列,则可以省略圆括号,仅在add后紧跟一个列定义即可。为数据表增加字段的SQL语句如下。

#为hehe数据表增加一个hehe_id字段,该字段的类型为int
alter table hehe
add hehe_id int;
#为hehe数据表增加aaa、bbb字段,两个字段的类型都为varchar(255)
alter table hehe
add
{
	aaa varchar(255) default 'xxx',
	bbb varchar(255)
};

上面第二条SQL语句增加aaa字段时,为字段默认值为’xxx’。值得指出的是,SQL语句中的字符串值不是用双引号引起,而是用单引号引起的。
增加字段时需要注意:如果数据表中已有数据记录,除非给新增的列指定了默认值,否则新增的数据不可指定非空约束,因为那些已有的记录在新增列上肯定是空(实际上,修改表结构很容器失败,只要新增的约束与已有的数据冲突,修改就会失败)。
修改列定义的语法如下:

alter table 表名
modify column_name datatype [default expr] [first|after col_name];

上面语法中first或者after col_name指定需要将目标修改到指定位置。
从上面修改语法中可以看出,该修改语句每次只能修改一个列定义,代码如下:

#将hehe表的 hehe_id 列修改成varchar(255)类型
alter table hehe
modify hehe_id varchar(255);
#将hehe表的bbb列修改成int类型
alter table hehe 
modify bbb int;

从上面代码中不难看出,使用SQL修改数据表里列定义的语法和为数据表只增加一个列定义的衣服啊几乎完全一样,关键是增加列定义使用add关键字,而修改列定义使用modify关键字。还有一点需要指出,add新增的列名必须是原表中不存在的,而modify修改的列名必须是原表中已存在的。
虽然MySQL的一个modify命令不支持一次修改多个列定义,但其他数据库如Oracle支持一个modify命令修改多个列定义,一个modify命令修改多个列定义的语法和一个add命令增加多个列定义的语法非常相似,也需要使用圆括号把多个列定义括起来。如果需要让MySQL支持一次修改多个列定义,则可在alter table后使用多个modify命令。
如果数据表里已有数据记录,则修改列定义非常容器失败,因为有可能修改的列定义规则与原有的数据记录不符合。如果修改数据列的默认值,则只会对以后的插入操作有作用,对以前已经存在的数据不会有任何影响。
从数据表中删除列的语法比较简单:

alter table 表名
drop column_name

删除列只要在drop后紧跟需要删除的列名即可。列如:

#删除hehe表中的aaa字段
alter table hehe
drop aaa;

从数据表中删除列定义通常总是可以成功,删除列定义时将从每行中删除该列的数据,并释放该列在数据块中占用的空间。所以删除大表中的字段时需要比较长的时间,因为还需要回收空间。
上卖弄介绍的这些增加列、修改列和删除列的语法是标准的SQL语法,对所有的数据库都通用。除此之外,MySQL还提供了两种特殊的语法:重命名数据表和完全该完全改变列定义。
重命名数据表的语法格式如下:

alter table 表名
rename to 新表名

如下SQL语句用于将hehe表命名为wawa:

#将hehe数据表重命名为wawa
alter table hehe
rename to wawa;

MySQL为alter table 提供了change选项,该选项可以改变列名。change选项的语法如下:

alter table 表名
change old_column_name new_column_name type[default expr][first|after col_name]

对比change和modify两个选项,不难发现:change选项多了一个列名,因为change选项可以改变列明,所以它需要两个列名。一般而言,如果不需要改变列明,使用alter table 的 modify选项即可,只有当需要修改列名时才会使用change选项。语句如下:

#将 wawa数据表的 bbb字段重命名为 ddd
alter table wawa
change bbb ddd int;

3.删除表的语法

删除表的语法格式如下:

drop table 表名;

如下SQL语句将会把数据库中已有的wawa数据表删除:

#删除数据表
drop table wawa;

删除数据表的效果如下:

  • 表结构被删除,表对象不再存在
  • 表里所有数据也被删除
  • 该表所有相关的索引、约束也被删除。

4.truncate表

对于大部分数据库而言,truncate都被当成DDL处理,truncate被称为“截断”某个表——它的作用是删除该表里的全部数据,但保留表结构。相对与DML里的delete命令而言,truncate的速度要快得多,而且truncate不像delete可以删除指定的记录,truncate只能一次性删除整个表的全部记录。truncate命令的语法如下:

truncate 表名

MySQL对truncate的处理比较特殊——如果使用非InnoDB存储机制,truncate比delete速度要快,如果使用InnoDB存储机制,在MySQL5.0.3之前,truncate和delete完全一样,在5.0.3之后,truncate table 比 delete效率高,但如果该表被外键约束所参照,truncate又变为和delete操作。在5.0.13之后,快速truncatee总是可用,即比delete性能要好。

数据库约束

前面创建的数据表仅仅指定了一些列定义,这紧急你是数据表里的基本功能。除此之外,所有的关系数据库都支持对数据库使用约束,通过约束可以更好地保证数据表里的数据的完整形。约束是在表上强制执行的数据校验规则,约束主要用于保证数据库里的数据的完整形。除此之外,当表中数据存在相互依赖性时,可以保护相关的数据不被删除。
大部分数据库支持下面种完整性约束。

  • NOT NULL:非空约束,指定某列不能为空。
  • UNIQUE:唯一约束,指定某列或者几列组合不能重复。
  • PRIMARY KEY:主键,指定该列的只可以唯一地标识该条记录。
  • FOREIGN KEY:外键,指定该行记录从属于主表中的一条记录,主要用于保证参照完整性。
  • CHECK:检查,指定一个布尔表达式,用于指定对应列的值必须满足该表达式。

虽然大部分数据库支持上面5种约束,但MySQL不支持CHECK约束,虽然MySQL的SQL语句也可以使用CHECK约束,但整个CHECK约束不会又任何作用。

虽然约束的作用只是用于保证数据表里的数据的完整性,但约束也是数据对象,并被存储在系统表中,也拥有自己的名字。根据约束对数据列的限制,约束分为如下两类。

  • 单列约束:每个约束只约束一列。
  • 多列约束:每个约束可以约束多个数据列。

为数据表指定的约束有如下两个时机。

  • 键表的同时为相应的数据列指定约束
  • 键表后创建,以修改表的方式来增加约束。

大部分约束都可以采用列级约束语法或者表级约束语法。
MySQL使用information_sechema 数据库里的TABLE_CONSTRAINTS表来保存该穴居库实例中所有的约束信息,用户可以通过查询TABLE_CONSTRAINTS表来获取该数据库的约束信息。

1.NOT NULL约束
非空约束用于确保指定列不允许为空,非空约束是比较特殊的约束,它只能作为列级约束使用,只能使用列及约束语法定义。这里要介绍一下SQL中的null值,SQL中的null不区分大小写。SQL中的null具有如下特征。

  • 所有数据类型的值都可以是null,包括int、float、boolean等数据。
  • 与Java类似的是,空字符串不等于null,0也不等于null。

如果需要在建表时为指定列指定非空约束,只要在列定义后增加 not null即可。建表语句如下:

create table hehe
{
	#建立了非空约束,这意味着hehe_id不可以为null
	hehe_id int not null,
	#MySQL的非空约束不能指定名字
	hehe_name varchar(255) default 'xyz' not null
	#下面列可以为空,默认值就时可以为空
	hehe_gender varchar(2) null
}

除此之外,也可以使用alter table 修改表时增加或删除非空约束,SQL命令如下:

#增加非空约束
alter table hehe
modify hehe_gender varchar(2) not null;
#取消非空约束
alter table hehe
modify hehe_name varchar(2) null;
#取消非空约束,并指定默认值
alter table hehe
modify hehe_name varchar(255) default 'abc' null 

2.UNIQUE约束
唯一约束用于保证指定列或指定列组合不允许出现重复值。虽然唯一约束的列不可以出现重复值,但可以出现多个null值(因为在数据库中null不等于null)。
同一个表内可以创建多个唯一约束,唯一约束也可由多列组合而成。当为某列创建唯一约束时,MySQL会为该列相应地创建唯一索引。如果不给唯一索引约束起名,该唯一约束默认与列名相同。
唯一约束既可以使用列及约束语法建立,也可以使用表级语法建立。如果需要为多列建组合约束,或者需要为唯一约束指定约束名,则只能用表级约束语法。
当建立唯一约束时,MySQL在唯一约束或列组合上建立对应的唯一索引。
使用列及约束语法建立唯一约束非常简单,只要简单地在列后面定义增加unique关键字即可。SQL语句如下:

#建表时创建唯一约束,使用列级约束语法建立约束
create table unique_test
{
	#建立了非空约束,这意味着test_id不可以为null
	test_id int not null,
	#unique就是唯一约束,使用列级约束语法建立唯一约束
	test_name varchar(255) unique
};

如果需要为多列组合建立唯一约束,或者想自行指定约束名,则需要使用表记约束语法。表级约束语法格式如下:

[constraint 约束名]定义约束

上面的表级约束语法格式既可以在create table 语句中与列定义并列,也可以在alter table语句中使用add关键字来添加约束。SQL语句如下:

#建表时创建唯一约束,使用表级语法建立约束
create table unique_test2
{
	#建立了非空约束,这意味着test_id不可以为null
	test_id int not null;
	test_name varchar(255),
	test_name varchar(255),
	test_pass varchar(255),
	#使用表级约束语法建立唯一约束
	unique(test_name),
	#使用表级约束建立唯一约束,而且指定约束名
	constraint test2_uk unique(test_pass)
};

上面的建表语句为test_name、test_pass分别建立了唯一约束,这意味着这两列都不能出现重复值。除此之外,还可以为这两列组合建立唯一约束,SQL语句如下:

#建表时创建唯一约束,使用表级约束语法建立约束
create table unique_test3
{
	#建立了非空约束,这意味着test_id不可以为null
	test_id int not null.
	test_name varchar(255),
	test_pass varchar(255),
	#使用表级约束语法建立唯一约束,指定两列组合不允许重复
	constraint test3_uk unique(test_name,test_pass)
};

对于上面的unique_test2和unique_test3两个表,都是对test_name、test_pass建立唯一约束,其中unique_test2要求test_name、test_pass都不能出现重复值,而unique_test3只要求test_name、test_pass两列的组合不能重复。
也可以在修改表结构时使用add关键字来增加唯一约束,SQL语句如下:

#增加唯一约束
alter table unique_test3
add unique(test_name,test_pass);

还可以在修改表是使用modify关键字,为单列采用列集约束语法来增加唯一约束,代码如下:

#为unique test3 表的 test_name列增加唯一约束
alter table unique_test3
modify test_name varchar(255) unique;

对于大部分数据库而言,删除约束都是在alter table语句后使用 "drop constraint 约束名"语法来完成的,但MySQL并不使用这种方式,二十使用“drop index 约束名”的方式来删除约束。例如如下SQL语句:

#删除unique_test3表上的test3_uk唯一约束
alter table unique_test3
drop index test3_uk;

3.PRIMARY KEY约束
主键约束相当于非空约束和唯一约束,即主键约束的列的列既不允许出现重复值,也不允许出现null值;如果对于多列组合建立主键约束,则多列里包含的每一列都不能为空,但要求这些列组合不能重复。主键列的值可以用于唯一地标识表中的一条记录。
每一个表中最多允许有一个主键,但这个主键约束可由多个数据列组合而成,主键是表中能唯一确定一行记录的字段或字段组合。
建立主表约束时即可使用列几约束语法,也可使用标记约束语法。如果需要对多个字段建立组合主键约束,则只能使用表级约束语法。使用表级约束语法来建立约束时,可以为该约束指定约束名。但不管用户是否为该主键约束指定约束名,MySQL总是将所有的主键约束命名为PRIMARY。
MySQL允许咋建立主键约束时为该约束命名,但这个名字没有任何作用,这是为了保持与标准SQL的兼容性,大部分数据库都允许执行指定主键约束的名字,而且一旦指定了主键约束名,则该约束名就是用户指定的名字。
当创建主键约束时,MySQL在主键约束所在列或列组合上建立对应的唯一索引。
创建主键约束的语法和创建唯一约束的语法非常像,一样允许使用列级约束语法为单独的数据列创建主键,如果需要为多列组合建立主键约束名或者需要为主键约束名,则应该使用表级约束语法来建立主键约束,与建立唯一约束不同的是,建立主键约束使用primary key。

create table primary_test
{
	#建立了主键约束
	test_id int primary key,
	test_name varchar(255)
};

建表时创建主键约束,使用表级约束语法:

create table primary_test2
{
	test_id not null,
	test_name varchar(255),
	test_pass varchar(255),
	#指定主键约束名为test2_pk,对大部分数据库有效,但对MySQL无效
	#MySQL数据库中该主键约束名依然是PRIMARY
	constraint test2_pk primary key(test_id)
};

建表时闯将主键约束,以多列建立组合主键,只能使用表级约束语法:

create table primary_test3
{
	test_name varchar(255),
	test_pass varchar(255),
	#建立多列组合的主键约束
	primary key(test_name,test_pass)
};

如果需要删除指定表的主键约束,既可通过modify修改列定义来增加主键约束,这将是采用列级约束语法来增加主键约束;也可通过add来增加主键约束,这将采用表级约束语法来增加主键约束。SQL语句如下:

#使用表级约束语法来增加主键约束
alter table primary_test3
add primary key(test_name,test_pass);

如果只是为单独的数据列增加主键约束,则可使用modify修改列定义来实现。SQL语句如下:

#使用列级约束语法来增加主键约束
alter table primary_test3
modify test_name varchar(255) primary key;

不要连续执行上面两条SQL语句。因为上面两条SQL语句都是为primary_test3增加主键约束,而同一个表里最多只能有一个主键约束,所以连续执行上面两条SQL语句肯定出现错误,为了避免这个问题,可以在成功执行了第一条增加主键约束的SQL语句之后,先将primary_test3里的主键约束删除或再执行第二条增加主键约束的SQL语句。
很多数据库对主键列都支持一种自增长的特性——如果某个数据列的类型是整形,而且该列作为主键列,则可指定该列具有自增长功能。指定自增长功能通常用于设置逻辑主键列——该列的值没有任何物理意义,仅仅用于标识每行记录。MySQL使用auto_increment来设置自增长,SQL语句如下:

create table primary_test4
{
	#建立主键约束,使用自增长
	test_id int auto_increment primary key,
	test_name varchar(255),
	test_pass varchar(255)
};

一旦指定了某列具有增长特性,则向该表插入记录时可不为该列指定值,该列的值由数据库系统自动生成。
4.FOREIGN KEY约束
外键约束主要用于保证一个或两个数据表之间的参照完整性,外键是构建于一个表的两个字段或者两个表的两个字段之间的参照关系。外键确保了相关的两个字段的参照关系:子(从)表外键列的值必须在主表被参照列的值范围之内,或者为空(也可以通过非空约束来约束外键列不允许为空)。
当主表的记录被从表记录参照时,主表记录不允许删除,必须先把从表里参照该记录的所有记录全部删除或,才可以删除主表的该记录。还有一种方式,删除主表记录时级联删除从表中所有参照该记录的从表记录。
从表外参照的只能是主表主键列或者唯一键列,这样才可保证从表记录可以准确定位到被参照的主表记录。同一个表内可以拥有多个外键。
建立外键约束时MySQL也会为该列建立索引。
外键约束通常用于定义两个实体之间的一对多、一对一的关联关系。对于一对多的关联关系,通常在多的一段增加外键列。例如老师-学生(假设一个老师对应多个学生,但每个学生只有一个老师,这是典型的一对多的关联关系)。为了建立他们之间的关联关系,可以在学生表中增加一个外键列,增加外键列的表被称为从表,只要为外键列增加唯一约束就可表示一对一的关联关系了。对于多对多的关联关系,则需要额外增加一个链接表来记录它们的关联关系。
建立外表键的同样可以采用列级约束语法和表级约束语法。如果仅对单独的数据列建立外键约束,则使用列级约束语法既可;如果需要对多列组合创建外键约束,或者需要为外键约束指定名字,必须使用表级约束语法。
采用列级约束语法建立外键约束直接使用references关键字,references指定了该列参照哪个主表,以及参照主表哪一列。SQL语句如下:

#为了保证从表参照的主表存在,通常应该先建立主表
create table teacher_table
{
	#auto_increment:代表数据库的自动编号策略,通常用作数据表的逻辑主键
	teacher_id int auto_increment,
	teacher_name varchar(255),
	primary key(teacher_id)
};
create table student_table
{
#为本列建立主键约束
	student id int auto_increment primary key,
	student_name varchar(255),
	#指定java_teacher参照到teacher_table的teacher_id列
	java_teacher int references tacher_table(teacher_id)
};

值得指出的是,虽然MySQL支持使用列级约束语法来建立外键约束,但这种列级约束语法建立的外键约束并不会生效,MySQL提供这种列级约束语法仅仅是为了和标准SQL保持良好的兼容性。因此,如果要使用MySQL中的外键约束生效,则应该使用表级约束语法。

#为了保证从表参照的主表存在,通常应该先建立主表
create table techer tablel{
	#auto_increment:代表数据库的自动编号策略,通常用作数据表的逻辑主键
	techer_id int auto_increment,
	teacher_name varchar(255),
	primary key(teacher_id)
};
create table student_tablel{
	#为本表建立主键约束
	student_id int auto_increment primary key,
	student_name varchar(255),
	#指定java_teacher参照到teacher_tablel的teacher_id列
	java_teacher int,
	foreign key(java_teacher) references teacher_tablel(teacher_id)
};

如果使用表级约束语法,则需要使用foreign key来指定本表的外键列,病使用references 来指定参照哪个招标,以及参照到主表的哪个数据列。使用表级约束语法可以为外键约束指定约束名,如果创建外键约束时没有指定约束吗,则MySQL会为该外键约束命名为table_name_ibfk_n,其中table_name是从表的表面,二n是从1开始的整数。
如果需要显示地指定外键约束的名字,则可使用constraint来指定名字。SQL语句如下:

#为了保证从表参照的主表存在,通常应该先建主表
create table teacher_table2
{
	#auto_increment:代表数据库的自动编号策略,通常用作数据表的逻辑主键
	teacher_id int auto_incrment,
	teacher_name varchar(255),
	primary key(teacher_id)
};
create table student_table2{
	#为本表建立主键约束
	student_id int auto_increment primary key,
	student_name varchar(255),
	java_teacher int,
	#使用表级约束语法建立外键约束,指定外键约束的约束名为student_teacher_fk
	constraint student_teacher_fk foreign key(java_teacher) references 
	teacher_table2(teacher_id)
};

如果需要建立多列组合的外键约束,则必须使用表级约束语法,SQL语句如下:

#为了保证从表参照的主表存在,通常应该先建主表
create table teacher_table3{
	teacher_name varchar(255),
	teacher_pass varchar(255),
	#以两列建立组合主键
	primary key(teacher_name,teacher_pass)
};
create table student_table3
{
	#为本表建立主键约束
	student_id int auto_increment primary key,
	student_name varchar(255),
	java_teacher_name varchar(255),
	java_teacher_pass varchar(255),
	#使用表级约束语法建立外键约束,指定两列的联合外键
	foreign key(java_teacher_name,java_teacher_pass)
	references teacher_table3(teacher_name,teacher_pass)
};

删除外键约束的语法很简单,在alter table 后增加“drop foreign key 约束名”子句既可。代码如下:

#删除student_table3表上名为student_table3_ibfk_1的外键约束
alter table studnet_table3
drop foreign key student_table3_ibfk_1;

增加外键约束通常使用add foreign key命令。SQL语句如下;

#修改student_table3数据表,增加外键约束
alter table student_table3
add foreign key(java_teacher_name,java_teacher_pass)
references teacher_table3(teacher_name,teacher_pass);

值得指出的是,外键约束不仅可以参照其他表,而且可以参照自身,这种参照自身的情况通常被称为自关联。例如,使用一个表保存某个公司的所有员工记录,员工之间有部门经理和普通员工之分,部门经理和普通员工之间存在一对多的关联关系,但他们都是保存在一个数据表里的记录,这就是典型的自关联。下面SQL语句用于建立自关联的外键约束。

#使用表级约束语法建立外约束,且直接参照自身
create table foreign_test
{
	foreign_id int auto_increment primary key,
	foreign_name varchar(255),
	#使用该表的refer_id参照到本表的foreign_id列
	refer_id int,
	foreign key(refer_id) references foreign_test(foreign_id)
};

如果想定义当删除主表记录时,从表记录也会随之删除,则需要在建立外表约束后添加 on delete cascade 或添加到 on delete set null,第一种是删除主表记录时,吧参照该主表记录的从表记录全部级联删除;第二种是指定当前删除主表记录时,吧参照主表记录的从表记录的外键设为null。SQL语句如下:

#为了保证从表参照的主表存在,通常应该先建主表
create table teacher_table4
{
	#auto_increment:代表数据库的自动编号策略,通常用作数据表的逻辑主键
	teacher_id int auto_increment,
	teacher_name varchar(255),
	primary key(teacher_id)
};
create table student_table4
{
	#为本表建立主键约束
	student_id int auto_increment primary key,
	student_name varchar(255),
	java_teacher int,
	#使用表级约束语法建立外键约束,定义级联删除
	foreign key(java_teacher) references teacher_table4(teacher_id)
	on delete cascade # 也可用 on delete set null
};

5.CHECK约束
当前版本的MySQL支持建表时指定CHECK约束,但这个CHECK约束不会有任何作用。建立CHECK约束的语法也很简单,只要在建表的列定义后增加 check(逻辑表达式)即可。SQL语句如下;

create table check_test
{
	emp_id int auto_increment,
	emp_name varchar(255),
	emp_salary decimal,
	primary key(emp_id),
	#建立CHEKC约束
	check(emp_salary>0)
};

虽然上面的SQL语句建立的check_test表中有CHECK约束,CHECK约束要求emp_salary大于0,但这个要求实际上病不会起作用。
MySQL作为一个开源、免费的数据库系统,对有些功能支持确实不太好。如果读者确实希望MySQL创建数据表有CHECK约束,甚至有更复杂的完整性约束,则可借助于MySQL的触发器机制。

索引

索引是存放在模式(schema)中的一个数据库对象。虽然索引总是从属于数据表,但它也和数据表一样属于数据库对象。创建索引的唯一作用就是加速对表的查询,索引通过使用快速路劲访问方法来快速定位数据,从而减少了磁盘的I/O。
索引作为数据库对象,在数据字典中独立存放,但不能独立存在,必须属于某个表。
MySQL使用information_schema数据库里的STATISTICS表来保存该数据库实例中的所有索引信息,用户可以通过查询该表来获取该数据库的索引信息。
创建索引有两种方式。

  • 自动:当前表上定义主键约束、唯一约束和外键约束时,系统会为该数据列自动创建对应的索引。
  • 手动:用户可以通过create index…语句来创建索引
    删除索引也有两种方式。
  • 自动:数据表被删除时,该表上的索引自动被删除。
  • 手动:用户可以通过 drop index…语句来删除指定数据表上的指定索引。
    索引的作用类似于书的目录,几乎没有一本书没有目录,因此几乎没有一个表没有索引。一个表中可以有多个索引列,每个索引都可用于加速该列的查询速度。
    正如书的目录总是根据书的知识点来建立一样——因为读者经常要根据知识点来查阅一本书。类似的,通常为经常需要查询的数据列建立索引,可以在一列或者多列上创建索引。创建索引的语法格式如下:
create index index_name
on table_name (column[,column]...);

下面的索引将会提供对employess表基于last_name字段的查询速度。

create index emp_last_name_idx
on employees(last_name);

也可同事对多列建立索引,SQL语句如下:

#下面语句为employees的first_name和last_name两列同时建立索引
create index emp_last_name_idx2
on employees(first_name,last_name);

MySQL中删除索引需要指定表,采用如下语法格式:

drop index 索引名 on 表名

如下SQL语句删除了employees表上的emp_last_name_idx2索引:

drop index emp_last_idx2
on employees

有些数据库删除索引时无须指定表名,因为它们要求建立索引时每个索引都有唯一的名字,所以无须指定表名,例如Oracle就采用这种策略。但MySQL之要求一个表内的索引不能同名,所以删除索引时必须指定表名。
索引的好处就是可以加速查询。但索引也有如下两个坏处。

  • 与书的目录相似,当数据表中的记录被添加、删除、修改时,数据库系统需要维护索引,因此有一定的系统开销。
  • 存储索引信息需要一定的磁盘空间。

视图

视图看上去非常像一个数据表,但它不是数据表,因为它并不能存储数据。视图只是一个或多个数据表中数据的逻辑显示。使用视图有如下几个好处。

  • 可以限制对数据的访问。
  • 可以使复杂的查询变得简单。
  • 提供了数据的独立性。
  • 提供了对相同数据的不同显示。

因为视图只是数据表中数据的逻辑显示——也就是一个查询结果,所以创建视图就是建立视图名和查询语句的关联。创建视图的语法如下:

create or replace view 视图名
as
subquery

从上面的语法可以看出,创建、修改视图都可使用上面语法。上面语法的含义是,如果该视图不存在,则创建视图;如果指定视图名的视图已经存在,则使用新视图替换原有视图。后面的subquery就是一个查询语句,这个查询可以非常复杂。
通过建立视图的语法规则不难看出,所谓视图的本质,其实就是一个被命名的SQL查询语句。
一旦建立了视图后,使用该视图与使用数据表就没有区别了,但通常只是查询视图数据,不会修改视图里的数据,因为视图本身没有存储数据。
如下SQL语句就创建了一个简单的视图:

create or replace view view_test
as
select teacher_name,teacher_pass from teacher_table;

通常不推荐直接改变视图的数据,因为视图并不存储数据,它只是相当于一个命名的查询语句而已。为了强制不允许改变视图的数据,MySQL允许在创建视图时使用 with check option子句,使用该字句创建的视图不允许修改,如下所示。

create or replace view view_test
as
select teacher_name from teacher_table
#指定不允许修改该视图的数据
with check option;

大部分数据库都采用 with check option 来强制不允许修改视图的数据,但Oracle采用 with read only来强制不允许修改视图的数据。
删除视图使用如下语句:

drop view 视图名

如下SQL语句删除了前面刚刚创建的视图:

drop view view_test

DML语句语法

与DDL操作数据对象不同,DML主要操作数据表里的数据,使用DML可以完成如下三个任务。

  • 插入新数据。
  • 修改已有的数据。
  • 删除不需要的数据。

DML语句由 insert into、 update 和 delete from三个命令组成。
1.insert into 语句
insert into 用于向指定数据表中插入记录,对于标准的SQL语句而言,每次只能插入一条记录,insert into语句的语法格式如下:

insert into table_name[(column[,column...)]
values(value[,value...);

执行插入操作时,表名后可以用括号列出所有需要插入值得列名,而values后用括号列出对应需要插入的值。
如果省略了表名后面的括号及括号里的列名列表,默认将为所有的列都插入值,则需要为每一列都指定一个值。如果既不想在表名后列出列明,又不想为所有列都指定值,则可以为那些无法确定值的列分配null。下面的SQL语句师范了如何向数据表中插入记录。
只有在数据库中已经成功创建了数据表之后,才可以向数据表中插入记录,下面的SQL语句以前介绍外键约束时的teacher_table2和student_table2为例来介绍数据插入操作。
在表名后使用括号列出所有需要插入值的例:

insert into teacher_table2(teacher_name)
values('xyz');

如果不想再表后用括号列出所有列,则需要为所有列指定值;如果某列的值不能确定,则为该列分配一个null值。

insert into teacher_table2
#使用null替代主键列的值
values(null,'abc');

经过两条插入语句后,可以看到teacher_table2表中的数据所示。
根据前面介绍的外键约束规则:外键列里的值必须是被参照列里已有的值,所以向从表中插入记录之前,通常应该先向主表中插入记录,
否则从表记录的外键只能为null。现在主表teacher_table2中已有了2条记录,现在可以向从表student_table2中插入记录了,SQL语句如下:

insert into student_table2
#当向外键列里插值时,外键列的值必须是被参照列里已有的值
values(null,'张三',2);

外键约束保证被参照的记录必须存在,但并不保证必须有被参照记录,即外键列可以为null。如果想保证每条从表记录必须存在对应的主表记录,则应该使用非空、外键两个约束。
有一些特殊情况下,可以使用带子查询的插入语句,带子查询的插入语句可以一次插入多条记录,SQL语句如下:

insert into student_table2(student_name)
#使用子查询的值来插入
select teacher_name from teacher_table2;

正如上面的SQL语句所示,带子查询的插入语句甚至不要求查询数据的源表和插入数据的目地表是同一个表,它只要求选择出来的数据列和插入目的表的数据列个数相等、数据类型匹配既可。
MySQL甚至提供了一种扩展的语法,通过这种扩展的语法可以一次插入多条记录。MySQL允许在values后使用多个括号包含多条记录,表示多条记录的多个括号之间以英文逗号(,)隔开,SQL语句如下:

insert into teacher_table2
#同时插入多个值
values(null,"Yeeku"),
(null,"Sharfly");

2.update语句
update语句用于修改数据表的记录,每次可以修改多条记录,通过使用where子句限制修改哪些记录。where子句是一个条件表达式,该条件表达式类似于Java语言的if,只有符合该条件的记录才会被修改。没有where子句意味着where表达式的值总是true,即该表的所有记录都会被修改。update语句的语法格式如下:

update table_name
set column1 = value1[,column2 = value2]...
[WHERE condition];

使用update语句不仅可以一次修改多条记录,也可以一次修改多列。修改多列都是通过在 set关键字后使用 column1 = value1,column2=value2…来实现的,修改多列的值之间以英文逗号(,)隔开。
下面的SQL语句将会吧teacher_table2表中的鄋记录的teacher_name列的值都改为’孙悟空’。

update teacher_table2
set teacher_name = '孙悟空';

也可以通过添加where条件来指定修改特定记录,SQL语句如下:

#只修改teacher_id 大于1的记录
update teacher_table2
set teacher_name = '猪八戒'
where teacher_id >1;

3.delete from 语句
delete from语句用于删除指定数据表的记录。使用delete from语句删除时不需要指定列名,因为总是整行地删除。
也可以使用where条件来限定只删除指定记录,SQL语句如下:

delete from teacher_table2
where teacher_id > 2;

当主表记录被从表记录参照时,主表记录不能被删除,只有先将从表中参照主表记录的所有记录全部删除后,才可删除主表记录。还有一种情况,定义外键约束时定义了主表记录和从表记录之间的级联删除 on delete cascade,或者使用 on delete set null用于指定当主表记录被删除时,从表中参照该记录的从表记录把外键列的值设为null。

单表查询

select语句的功能就是查询数据。select语句也是SQL语句中功能最丰富的语句,select语句不仅可以执行单标查询,而且可以执行多表连接查询,还可以进行子查询,select语句用于从一个或多个数据表中选出特定行、特定列的交集。select语句最简单的功能如图所示。
第十三章 MySQL数据库与JDBC编程_第16张图片
单标查询的语句如下:

select column1,column2...
from 数据源
[where condition]

上面语法格式中的数据源可以是表、视图等。从上面的语法定义中可以看出,select后的列表用于确定选择那些列,where条件用于确定选择那些行,只有满足where条件的记录才会被选择出来;如果没有where条件,则默认选出所有行。如果想选择出所有列,则可使用星号(*)代表所有列。
下面的SQL语句将会选择出teacher_table表中的所有行、所有列的数据。

select *
from teacher_table;

语句都为演示,并无实际效果
如果增加where条件,则只选择出符合where条件的记录。如下SQL语句将选择出student_table表中 java_teacher值大于3的记录的student_name列的值。

select student_name
from student_table
where java_teacher > 3;

当使用select语句进行查询时,还可以在select语句中使用算术运算符(+、-、*、/),从而形成算术表达式。使用算数表达式的规则如下:

  • 对数值型数据列、变量、常量可以使用算术运算符(+、-、*、/)创建表达式。
  • 对日期型数据列、变量、常量可以使用部分算术运算符(+、-)创建表达式,两个日期之间可以进行减法运算,日期和数值之间可以进行加、减运算;
  • 运算符不仅可以在列和常量、变量之间进行运算,也可以在两列之间进行运算。

不论从哪个角度来看,数据列都很像一个变量,只是这个变量的值具有指定的范围——逐行计算表中的每条记录时,数据列的值依次变化。因此能使用变量的地方,基本上都可以使用数据列。
下面的select语句中使用了算术运算符

#数据列实际上可当成一个变量
select teacher_id +5
from teacher_table;
#查询出teacher_table表中teacher_id *3 大于4的记录
select *
from teacher_table
where teacher_id *3>4;

需要指出的是,select后不仅可以是数据列,也可以是表达式,还可以是变量、常量等。例如,如下语句也是正确的。

#在select后直接使用表达式或常量
select 3*5,20
from teacher_table;

SQL语句中算术运算符的优先级与Java语言的运算符优先级完全相同,乘法和除法的优先级高于加法和减法,同级运算的顺序是从左到右,即无法使用加号(+)将字符串常量、字符串变量或字符串列链接起来。MySQL使用concat函数来进行字符串连接运算。
SQL语法如下:

#选择出teacher_name和'xx'字符串连接后的结果
select concat(teacher_name,'xx')
from teacher_table;

对于MySQL而言,如果在算术表达式中使用null,将会导致整个算术表达式的返回值为null;如果在字符串连接运算中出现null,将会导致连接后的结果也是null。如下SQL语句将会返回null。
对某些数据库而言,如果让字符串和null进行连接运算,它会把null当成空字符串处理。
如果不希望直接使用列名作为标题,则可以为数据列或表达式起一个别名,为数据列或表达式起别名时,别名紧跟数据列,中间以空格隔开,或者使用as关键字隔开。SQL语句如下:

select teacher_id + 5 as MY_ID
from teacher_table;

如果列别名中使用特殊字符(例如空格),或者需要强制大小写敏感,都可以通过为别名添加双引号来实现。SQL语句如下:

#可以为选出的列起名,别名中包括单引号字符,所以把别名用双引号引起来
select teacher_id + 5 "MY'id"
from teacher_table;

如果需要选择多列,并未多列起别名,则列与列之间以逗号隔开,但列和列别名之间以空格隔开。SQL语句如下:

select teacher_id + 5 MY_ID,teacher_name 老师名
from teacher_table;

不仅可以为列或表达式起别名,也可以为表起别名,为表起别名的语法和为列或表达式起别名的语法完全一样,SQL语句如下:

select teacher_id +5 MY_ID,teacher_name 老师名
#为teacher_table 起别名t
from teacher_table t;

前面已经提到,列名可以当成变量处理,所以运算符也可以在多列之间进行运算,SQL语句下:

select teacher_id +5 MY_ID,concat(teacher_name,teacher_id)teacher_name
from teacher_table
where teacher_id *2 > 3;

甚至可以在select、where子句中都不出现列名,SQL语句如下:

select 5+4
from teacher_table
where 2<9;

这种情况比较特殊:where语句后的条件表达式都是true,所以会把teacher_table表中每条记录都选择出来——但SQL语句没有选择任何列,仅仅选择了一个常量,所以SQL会把该常量当成一列,teacher_table表中有多少记录,该常量就会出现多少次。
对于选择常量的情形,指定数据表可能没有太大的意义,所以MySQL提供了一种扩展语法,允许select语句后没有from子句,即可写成如下形式:

select 5+4;

上面这种语句并不是标准的SQL语句。例如,Oracle提供了一个名为dual的虚表(最新的MySQL数据库也支持dual虚表),它并没有任何意义,仅仅相当于from后的占位符。如果选择常量,则可使用如下语句:

select 5+4 from dual;

select默认会把符合条件的记录全部选出来,即使两行记录完全一样。如果想去除重复行,则可以使用distinct关键字从查询结果中清除重复行。比较下面两条SQL语句的执行结果:

#选出所有记录,包括重复行
select student_name,java_teacher
from student_table;

#去除重复行
select distinct student_name,java_teacher
from student_table;

使用distinct去除重复行时,distinct紧跟select关键字。它的作用是去除后面字段组合的重复值,而不管对应记录在数据库里是否重复。例如,(1,‘a’,‘b’)和(a.‘a’,‘b’)两条记录在数据库里是不重复的,如果仅选择后面两列,则distinct会认为两条记录重复。
前面已经看到了where子句的作用——可以控制只选择指定的行。因为where子句里包含的是一个条件表达式,所以可以使用>、>=、<、<=、=和<>等基本的比较运算符。SQL中的比较运算符不仅可以比较数值之间的大小,也可以比较字符串、日期之间的大小。
SQL中判断两个值是否相等的比较运算符是单等号,判断不相等的运算符是<>;SQL中的赋值运算符不是等号,而是冒号等号(:=)。
除此之外,SQL还支持如表所示的特殊比较运算符。
第十三章 MySQL数据库与JDBC编程_第17张图片
下面的SQL语句选出student_id大于等于2,且小于等于2,且小于等于4的所有记录。

select*from student_table
where student_id between 2 and 4;

使用between vall and val2必须保证val1小于val2,否则将选不出任何记录。除此之外,between vall and val2 中的两个值不仅可以是常量,也可以是变量,或者是列名也行。如下SQL语句选出java_teacher小于等于2,student_id大于2的所有记录。

select*from student_table
where 2 between java_teacher and student_id;

使用in 比较运算符时,必须在in后的括号里列出一个或多个值,它要求必须指定列必须与in括号里在意一个值相等。SQL语句如下:

#选出student_id为2或4的所有记录
select*from student_table
where student_id in(2,4);

与此类似的是,in括号里的值既可以是常量,也可以是变量或者列名,SQL语句如下:

#选出student_id、java_teacher列的值为2的所有记录
select*from student_table
where 2 in(student_id,java_teacher);

like运算符主要用于进行模仿查询,例如,若要查询名字以“张”开头的所有记录,这就是需要用到模糊查询,在模糊查询中需要使用like关键字。在SQL语句中可以会用两个通配符:下划线(_)和百分号(%),其中下画线可以代表任意一个字符,百分号可以代表任意多个字符。如下SQL语句将查询出所有学生名字中以“张”开头的学生。

select * from student_table
where student_name like '张%';

下面的SQL语句将查询出名字为两个字符的所有学生。

select*from student_table
#下面使用两个下画线代表两个字符
where student_name like '__';

在某些特殊情况下,查询的条件里需要使用下画线或百分号,不希望SQL把下画线和百分号当成通配符使用,这就需要使用转义字符,MySQL使用反斜线(\)作为转义字符,SQL语句如下:

#选出所有名字以下画线开头的学生
select * from student_table
where student_name like '\_%';

标准SQL语句并没有提供反斜线(\)的转义字符,而是使用escape关键字显式进行转义。例如,为了实现上面功能需要使用如下SQL语句:

#在标准SQL中选出所有名字以下画线开头的学生
select*from student_table
where student_name like '\_%' escape '\';

is null用于判断某些值是否为空,判断是否为空不要用=null来判断,因为SQL中null=null返回null。如下SQL语句将选择出student_table表中student_name为null的所有记录。

select*from student_table
where student_name is null;

如果where子句后有多个条件需要组合,SQL提供了and和or逻辑运算符来组合两个条件,并提供了not来对逻辑表达式求否。如下SQL语句将选出学生名字为2个字符,且student_id大于3的所有记录。

select*from student_table
#使用and来组合多个条件
where student_name like '__' and student_id > 3;

下面的SQL语句将选出student_table表中姓名不以下画线开头的所有记录。

select * from student_table
#使用not对where条件去否
where not studnet_name lke '\_%';

当使用比较运算符、逻辑运算符来连接表达式时,必须注意这些运算符的优先级。SQL中比较运算符、逻辑运算符的优先级如下表所示。
第十三章 MySQL数据库与JDBC编程_第18张图片
如果SQL代码需要改变优先级的默认顺序,则可以使用括号,括号的优先级比所有的运算符高。
如下SQL语句使用括号来改变逻辑运算符的优先级。

select * from student_table
#使用括号强制险计算or运算
where(student_id >3 or student_name > '张')
	and java_teacher>1;

执行查询后的查询结果默认按插入顺序排序;如果需要查询结果按某列数值的大小进行排序,则可以是哟n order by 子句。order by 子句的语法格式如下:

order by column_name1 [desc],column_name2 ...

进行排序时默认按升序排序,如果强制降序排序,则需要在列后使用desc关键字(与之对应的是asc关键字,用不用该关键字的效果都完全一样,因为默认是按圣墟排序)。
上面语法中设定排序列时可采用列名、序列号和列别名。如下SQL语句选出student_table表中的所有记录,选出后按java_teacher列的升序排列。

select * from student
order by java_teacher;

如果需要按多列排序,则每列的asc、desc必须单独设定。如果指定了多个排列,只有当第一列中存在多个相同的值时,第二个排序列才会引起作用。如下SQL语句先按java_teacher列的降序排列,当java_teacher列的值相同时按student_name列的升序排列。

select * from student_table
order by java_teacher desc,student_name;

数据库函数

正如前面看到的连接字符串使用的concat函数,每个数据库都会在标准的SQL基础上扩展一些函数,这些函数用于进行数据处理或复杂计算,它们通过对一组数据进行计算,得出最终需要的输出结果。
函数一般都会有一个或者多个输入,这些输入被称为函数的参数,函数内部会对这些参数进行判断和计算,最终只有一个值作为返回值。函数可以出现在SQL语句的各个位置,比较常见的位置就是select之后和where子句中。
根据函数对多行数据的处理方式,函数被分为单行函数和多行函数,单行函数对每行输入值单独计算,每行得到一个计算结果返回给用户;多行函数对多行输入值整体计算,最好只会得到一个结果。单行函数和多行函数如下图所示。
第十三章 MySQL数据库与JDBC编程_第19张图片
SQL中的函数和Java语言的方法有点相似,但SQL中的函数是独立的程序单元,也就是说,调用函数时无须任何类、对象作为调用者,而是直接执行函数。执行函数的语法如下:

function_name(arg1,arg2...)

多行函数也被称为聚集函数、分组函数,主要用于完成一些统计功能,在大部分数据库中基本相同。但不同数据库中的单行函数差别特别大,MySQL中的单行函数具有如下特征。

  • 单行函数的参数可以是变量、常量或数据列。单行函数可以接收多个参数,但只返回一个值。
  • 单行函数会对每行单独起作用,每行(可能包含多个参数)返回一个结果。
  • 使用单行函数可以改变参数的数据类型。单行函数支持嵌套使用,即内层函数的返回值是外层函数的参数。

MySQL的单行函数分类如下图所示。
第十三章 MySQL数据库与JDBC编程_第20张图片

MySQL数据库的数据类型大致分为数值型、字符型和日期时间型,所以MySQL分别提供了对应的函数,转换函数主要负责完成类型转换,其他函数又大致分为如下几类。

  • 位函数
  • 流程控制函数
  • 加密解密函数
  • 信息函数

每个数据库都包含了大量的单行函数,这些函数的用法也存在一些差异,但有一点是相同的——每个数据库都会为一些常用的计算功能提供相应的函数,这些函数的函数名可能不同,用法可能有差异,但所有数据库提供的函数库所能完成的功能大致相似,读者可以参考各数据库系统的参考文档来学习这些函数的用法。下面通过一些例子来介绍MySQL单行函数的用法。

#选出teacher_table表中的teacher_name列的字符长度
select char_length(teacher_name)
from teacher_table;
#计算teacher_name列的字符长度的sin值
select sin(char_length(teacher_name))
from teacher_table;
#计算1.57的sin值,约等于1
select sin(1.57);
#为指定日期添加一定的时间
#在这种用法下 interval是关键字,需要一个数值,还需要一个单位
SELECT DATE_ADD('1998-01-02',interval 2 MONTH);
#这种用法啊更简单
select ADDDATE('1998-01-02',3);
#获取当前日期
select CURDATE();
#获取当前时间
select curtime();
#下面的MD5是MD5加密函数
select MD5('testing');

MySQL提供了如下几个处理null的函数。

  • ifnull(expr1,expr2):如果expr1为null,则返回expr2,否则返回expr1。
  • nullif(expr1,expr2):如果expr1和expr2相当,则返回null,否则返回expr1。
  • if(expr1,expr2,expr2):有点类似于?:三木运算符,如果expr1为true,不等于0,且不等于null,则返回expr2,否则返回expr3.。
  • isnull(expr1):判断expr1是否为null,如果为null则返回true,否则返回false。
#如果student_name列为null,则返回没有名字
select ifnull(student_name,'没有名字')
from student_table;
#如果student_name列等于'张三',则返回null
select nullif(student_name,'张三')
from student_table;
#如果student_name列为null,则返回'没有名字',否则返回有名字
select if(isnull(student_name),'没有名字','有名字')
from student_table;

MySQL还提供了一个case函数,该函数是一个流程控制函数,case函数有两个用法,case函数第一个用法的语法格式如下:

case value
when compare_value1 then result1
when compare_value2 then result2
...
else result
end

case函数用value和后面的compare_value1、compare_value2、…依次进行比较,如果value和指定的compare_value1相等,则返回对应的result1,否则返回else后的result。例如如下SQL语句:

#如果java_teacher为1,则返回'Java老师',为2返回'Ruby老师',否则返回'其他老师'
select student_name,case java_teacher
when 1 then 'Java老师'
when 2 then 'Runby老师'
else '其他老师'
end
from student_table;

case函数第二个用法的语法格式如下:

case 
when condition1 then result1
when condition2 then result2
...
else result
end

在第二个用法中,condition1、condition2都是一个返回boolean值条件表达式,因此这种用法更加灵活。例如如下SQL语句:

#id小于3的为初学班,3~6的为中级班,其他为高级班
select student_name,case
when student_id <= 3 then '初级班'
when sutdneg_id <= 6 then '中级班'
else '高级班'
end
from student_table;

虽然此处介绍了一些MySQL常用函数的简单用法,但通常不推荐在Java程序中使用特定数据库的函数,因为这将导致程序代码与特定数据库耦合;如果需要把该程序移植到其他数据库上时,可能需要打开源程序,重新修改SQL语句。

分组和组函数

组函数也就是前面提到的多行函数,组函数将一组记录作为整体计算,每组记录返回一个结果,而不是每条记录返回一个结果。常用的组函数又如下个。

  • avg([distinct|all]expr):计算多行expr的平均值,其中,expr可以是变量、常量或数据列,但其数据类型必须是数值型。还可以在变量、列前使用distinct或all关键字,如果使用distinct,则表明不计算重复值;all用和不用的效果完全一样,表明需要计算重复值。
  • count({*|[distinct|all]expr}):计算多行expr的总条数,其中expr可以是变量、常量或数据列,其数据类型可以使任意类型;用星号表示统计该表内的记录行数;distinct表示不计算重复值。
  • max(expr):计算多行expr的最大值,其中epr可以是变量、常量或数据列,起数据类型可以是任意类型。
  • min(expr):计算多行expr的最小值,其中expr可以是变量、常量或数据列,起数据类型可以是任意类型。
  • sum([distinct|all]expr):计算多行expr的总和,其中expr可以是变量、常量或数据列,但其类型必须是数值型;distinct表示不计算重复值。
#计算student_talbe表中的记录条数
select count(*)
from studnet_table;
#计算java_teacher列总共有多少个值
select count(distinct java_teacher)
from student_table;
#统计所有student_id的总和
select sum(student_id)
from student_table;
#计算的结果是20*记录的行数
select sum(20)
from student_table;
#选出 student_table表中的student_id最大的值
select max(student_table)
from student_table;
#选出teacher_table表中teacher_id最小的值
select min(teacher_id)
from teacher_table;
#因为sum里的expr是常量34,所以每行的值都相同
#使用distinct强制不计算重复值,所以下面计算结果为34
select sum(distinct 34)
from student_table;
#使用count统计记录行数时,null不会被计算在内
select count(student_name)
from student_table;

对于可能出现null的列,可以使用ifnull函数来处理该列。

#计算java_teacher列所有记录的平均值
select avg(ifnull(java_teacher,0))
from student_table;

值得指出的是,distinct和*不同时使用,如下SQL语句由错误。

select count(distinct*)
from student_table;

在默认情况下,组函数会把所有记录当成一组,为了对记录进行显式分组,可以在select语句后使用group by 子句后通常跟一个或多个列名,表明查询结果根据一列或多列进行分组——当一列或多列组合的值完全相同时,系统会把这些记录当成一组。SQL语句如下:

#count(*)将会对每组得到一个结果
select count(*)
from student_table
#将java_teacher列值相同的记录当成一组
group by java_teacher;

如果对多个列进行分组,则要求多列的值完全相同才会被当成一组。SQL语句如下:

select count(*)
from student_table
#当java_teacher、student_name两列的值完全相同时才会被当成一组
group by java_teacher,student_name;

对于很多数据库而言,分组计算时有严格的规则——如果查询列表中使用了组函数,或者select语句中使用了group by分组子句,则要求出现在select列表中的字段,要么使用组函数包起来,要么必须出现在group by 子句中。这条规矩很容易理解,因为一旦使用了组函数或使用了 group by 子句,都讲导致多条记录只有一条输出,系统无法输出多条记录中的哪一条记录。
对于MySQL来说,并没有上面的规则要求,如果某个数据列既没有出现在group by 之后,也没有使用组函数包起来,则MySQL会输出该列的第一条记录的值。下图显示了MySQL的处理结果。
第十三章 MySQL数据库与JDBC编程_第21张图片如果需要对分组进行过滤,则应该使用having子句,having子句后面也是一个条件表达式,只有满足该条件
表达式的分组才会被选出来。having子句和where子句非常容易混淆,他们都有过滤功能,但它们有如下区别。

  • 不能在where子句中过滤组,where子句仅用于过滤行。过滤组必须使用having子句。
  • 不能在whrer子句中使用组函数,having子句才可使用组函数。

SQL语句如下:

select *
from student_table
group by java_teacher
#对组进行过滤
having count(*)>2;

多表连接查询

很多时候,需要选择的数据并不是来自一个表,而是来自多个数据表,这就需要使用多表连接查询。例如,对于上面的student_table和teacher_table两个数据表,如果希望查询出所有学生以及他的老师名字,这就需要从两个表中取数据,
多表连接查询有两种规范,较早的SQL92规范支持如下几种多表连接查询。

  • 等值连接。
  • 非等值连接。
  • 外连接。
  • 广义笛卡儿积

SQL99规范提供了可读性更好的多表连接语法,并提供了更多类型的连接查询。SQL99支持如下几种表连接查询。

  • 交叉连接。
  • 自然连接。
  • 使用using子句的连接。
  • 使用on子句的连接。
  • 全外连接或者左、右连接。

1.SQL92的连接查询
SQL92的多表连接语法比较简洁,这种语法把多个数据表都放在from之后,多个表之间以逗号隔开;连接条件放在where之后,在查询条件之间用and逻辑运算符连接。如果连接条件要求两列值相等,则称为等值连接,否则称为非等值连接;如果没有任何连接条件,则称为广义笛卡二积。SQL92中多表连接查询的语法格式如下:

select column1,column2...
from table1,table2 ...
[where join_condition]

多表连接查询中可能出现两个或多个数据列具有相同的列名,则需要在这些同名列之间使用表名前缀或表别名前缀作为限制,避免系统混淆。
实际上,所有的列都可以增加表名前缀或表别名。只是进行单表查询时,绝不可能出现同名列,所以系统不可能混淆,因此通常省略表名前缀。
如下SQL语句查询出所有学生的资源以及对应的老师姓名。

select s.*,teacher_name
#指定多个数据表,并指定表别名
from student_table s,teacher_table t
#使用where指定连接条件
where s.java_teacher = t.teacher_id;

上面的查询结果正好满足要求,可以看到每个学生以及他对应的老师的名字。实际上,多表查询的过程可理解成一个嵌套循环,这个嵌套循环的伪码如下:

//依次遍历 teacher_table 表中的每条记录
for t in teacher_table
{
	//遍历student_table表中的每条记录
	for s in teacher_table
	{
		//当满足连接条件时,输出两个表连接后的结果
		if(s.java_teacher = t.teacher_id)
		output s + t
	}
}

理解了上面的伪码之后,接下来即可很轻易地理解多表连接查询的运行机制。如果求广义笛卡儿积,则where子句后没有任何连接条件,相当于没有上面的if语句,广义笛卡儿积的结果会有 nXm条记录。只要把where后的连接条件去掉,就可以得到广义笛卡儿积,SQL语句如下:

#不适用连接条件,得到广义笛卡儿积
select s.*,teacher_name
#指定多个数据表,并指定表别名
from student_table s,teacher_table t;

与此类似的是,非等值连接的执行结果可以使用上面的嵌套循环来计算,SQL语句如下:

select s.*,teacher_name
#指定多个数据表,并指定表别名
from student_table s,teacher_table t
#使用where指定连接条件,非等值连接
where s.java_teacher>t.teacher_id;

上面SQL语句的执行结果相当于if条件换成了s.java_teacher>t.teacher_id。
如果还需要对记录进行过滤,则将过滤条件和连接条件使用and连接起来,SQL语句如下:

select s.*,teacher_name
#指定多个数据表,并指定表别名
from student+table s,teacher_table t;

与此类似的是,非等值连接的执行结果可以使用上面的嵌套循环来计算,SQL语句如下:

select s.*,teacher_name
#指定多个数据表并指定表别名
from student_table s, teacher_table t
#使用where指定连接条件,非等值连接
where s.java_teacher>t.teacher_id;

上面SQL语句的执行结果相当于if条件换成了s.java_teacher>t.teacher_id。
如果还需要对记录进行过滤,则将过滤条件和连接条件使用and连接起来,SQL语句如下:

select s.*,teacher_name
#指定多个数据表,并指定表别名
from student_table s,teacher_table t
#使用where指定连接条件,并指定student_name列不能为null
where s.java_teacher=t.teacher_id and student_name is not null;

虽然MySQL不支持SQL92中的左外连接、右外连接,但本书还是有必要了解一下SQL92的左外连接和右外连接。SQL92中的外连接就是在连接条件的列名后增加括号抱起来的外连接符(+或*,不同的数据库有一定的区别),当外连接符出现在左边时称为左外连接,出现在右边时则称为右外连接。SQL语句如下:

select s.*,teacher_name
from student_table s,teacher_table t
#右外连接
where s.java_teacher = t.teacher_id(*);

外连接就是外连接符所在的表中增加一个“万能行”,这行记录的所有数据都是null,而且该行可以与另一个表中所有不满足条件的记录进行匹配,通过这种方式就可以把另一个表中的所有记录选出来,不管这些记录是否满足连接条件。
除此之外,还有一种自连接,正如前面介绍外键约束时提到的自关联,如果同一个表的不同记录之间存在主、外键约束关联,例如把员工、经理保存在同一个表里,则需要使用自连接查询。
自连接只是连接一种用法,并不是一种连接类型,不管是SQL92还是SQL99栋可以使用自连接查询。自连接的本质就是把一个表当成两个表来用。

create table emp_table
{
	emp_id int auto_increment primary key,
	emp_name varchar(255),
	manager_id int,
	foreign key(manager_id) references emp_table(emp+id)
};
insert into emp_table
values(null,'唐僧',null),
(null,'孙悟空',1),
(null,'猪八戒',1),
(null,'沙僧',1);

如果需要查询该数据表中的所有员工名,以及每个员工对应的经理名,则必须使用自连接查询。所谓自连接就是把一个表当成两个表来用,这就是需要为一个表起两个别名,而且查询中用的所有数据列都要加表别名前缀,因为两个表的数据列完全一样。下面的自连接查询可以查询出所有员工名,以及对应的经理名。

select emp.emp_id,emp.emp_name 员工名,mgr.emp_name 经理名
from emp_table emp,emp_table mgr
where emp.manger_id = mgr.emp_id;

2.SQL99连接查询
SQL99的连接查询与SQL92的连接查询原理基本相似,不同的是SQL99连接后查询的可读性更强——查询用的多个数据显式使用 xxx join连接,而不是直接一次排列在from之后,from后只需要放一个数据表;连接条件不再是放在where之后,而是提供了专门的连接条件子句。

  • 交叉连接(cross join):交叉连接效果就是SQL92的广义笛卡儿积,所以交叉连接无须任何连接条件,SQL语句如下:
select s.*,teacher_name
#SQL99多表连接查询的from后只有一个表名
from student_table s
#cross join交叉连接,相当于广义笛卡儿积
cross join teacher_table t;
  • 自然连接(natural join):自然连接表面上看起来也无须指定连接条件,但自然连接是有连接条件的,自然连接会以两个表中的同名列作为连接条件;如果两个表中没有同列名,则自然连接与交叉连接效果完全一样——因为没有连接条件。SQL语句如下:
select s.*,teacher_name
#SQL 99多表连接查询的from后只有一个表名
from student_table s
#natural join 自然连接使用两个表名后的同名作为连接条件
natural join teacher_table t;
  • using子句连接:using子句可以指定一列或多列,用于显式指定两个表中的同名作为连接条件。假设两个表有超过一列的同名列,如果采用natural join,则会把所有的同名列当成连接条件;使用using子句,就可显式指定使用哪些同名列作为连接条件。SQL语句如下:
select s.*,teacher_name
#SQL 99多表连接查询的from后只有一个表名
from student_table s
#join 连接另一个表
join teacher_table t
using(teacher_id);

运行上面语句将出现一个错误,因为student_table表中并不存在名为teacher_id的列,也就是说,如果使用using子句来指定连接条件,则两个表中必须有同名列,否则就会出现错误。

  • on子句连接:这是最常用的连接方式,SQL99语句的连接条件放在on子句中指定,而且每个on子句只指定一个连接条件。这意味着:如果需要进行N表连接,则需要有N-1个join…on对。

SQL语句如下

select s.*,teacher_name
#SQL 99多表连接查询的from后只有一个表名
from student_table a
#join连接另一个表
join teacher_table t
#使用on来指定连接条件
on s.java_teacher = t.teacher_id;

使用on子句的连接完全可以代替SQL92中的等值连接、非等值连接,因为on子句的连接条件除等值条件之外,也可以是非等值条件。如下SQL语句就是SQL99中的非等值连接。

select s.*,teacher_name
#SQL 99多表连接查询的from后只有一个表名
from student_table s
#join 连接另一个表
join teacher_table t
#使用on来指定连接条件:非等值连接
on s.java_teacher>t.teacher_id;
  • 左、右、全外连接:这三种外连接分别使用 left[outer]join、right[outer]join和full[outer]join,这三种外连接的连接条件一样通过on子句来指定,既可以是等值连接条件,也可以是非等值连接条件。
    下面使用右外连接,连接条件时非等值连接
select s.*,teacher_name
#SQL 99多表连接查询的from后只有一个表名
from student_table s
#ringht join 右外连接另一个表
ringht join teacher_table t
#使用on来指定连接条件,使用费等值连接
on s.java_teacher < t.teacher_id;


下面使用左外连接,连接条件时非等值连接。

select s.*,teacher_name 
#SQL 99多表连接查询的from后只有一个表名
from student_table s
#left join 左外连接另一个
left join teacher_table t
#使用on来指定连接条件,使用非等值连接
on s.java_teacher >t.teacher_id;

运行上面两条外连接语句并查看它们的运行结果,不难发现SQL99外连接与SQL92外连接恰好相反,SQL99左外连接将会把左边表中所有不满足连接条件的记录全部列出;SQL99右外连接将会把右边表中所有不满足连接条件的记录全部列出。
下面的SQL语句使用全外连接,连接条件是等值连接。

select s.*,teacher_name
#SQL 99多表连接查询的from后只有一个表名
from student_table s
#full join 全外连接另一个表
full join teahcer_table t
#使用on来指定连接条件,使用等值连接
on s.java_teacher = t.teacher_id;

运行上面查询语句时会出现错误,这是因为MySQL并不支持全外连接。

子查询

子查询就是指出查询语句中嵌套另一个查询,子查询可以支持多层嵌套。对于一个普通的查询条件而言,子查询可以出现在两个位置。

  • 出现在from语句后当成数据表,这种用法也被称为行内视图,因为该子查询的实质就是一个临时视图。
  • 出现在where条件后作为过滤条件的值。

使用子查询时要注意如下几个点。

  • 子查询要用括号括起来。
  • 把子查询当成数据表时(出现在from之后),可以为该子查询起名,尤其是作为前缀来限定数据列时,必须给子查询起名。
  • 把子查询当成过滤条件时,将子查询放在比较运算符的右边,这样可以增加查询的可读性。
  • 把子查询当成过滤条件时,单行子查询使用单行运算符,多行子查询使用多行运算符。

对于把子查询当成数据表是完全把子查询当做数据表来用,只是把之前的表名变成子查询(也可以为子查询起别名),其他部分与普通查询没有任何区别。下面的SQL语句师范了把子查询当成数据表的用法。

select *
#把子查询当成数据表
from(select * from student_table)t
where t.java_teacher>1;

把子查询当成数据表的用法更准确地说是当成视图,可以把上面的SQL语句理解成在执行查询时创建了一个临时视图,该视图名为t,所以这种临时创建的视图也被称为行内视图。理解了这种子查询的实质后,不难知道这种子查询可以完全代替查询语句中的数据表,包括在多表连接查询中使用这种子查询。
还有一种情形:把子查询当成where条件中的值,如果子查询返回单行、单列值,则被当成一个标量值使用,也就可以使用单行记录比较运算符。例如如下SQL语句:

select*
from student_table
where java_teacher>
#返回单行、单列的子查询可以当成标量值使用
(select teacher_id
from teacher_table
where teacher_name = 'Yeeku'
);

上面查询语句中的子查询将返回一个单行、单列值(该值就是1),如果把上面查询语句的括号部分换成1,那么这条语句就再简单不过了——实际上,这就是这种子查询的实质,单行、单列子查询的返回值被当成标量值处理。
如果子查询返回多个值,则需要使用in、any和all等关键字,in可以单独使用,与前面介绍比较运算符时所讲的in完全一样,此时可以把子查询返回的多个值当成一个值列表。SQL语句如下:

select *
from student_table
where student_id in
(select teahcer_id
from teacher_table);

上面查询语句中的子查询将返回多个值,这多个值将被当成一个值列表,只要student_id与该值列表中任意一个值相等,就可以选出这条记录。
any和all可以与>、>=、<、<=、<>、=等运算符结合使用,与any结合使用分别表示大于、小于、等与、小于、小于等与、不等与、等与其中任意一个值;与all结合使用分别表示大于、大于等与、小于、小于等与、不等于、等与全不知。从上面介绍可以看出,=any的作用于in的作用相同,如下SQL语句使用=any来代替上面的in。

select *
from student_table
where student_id =
any(select teacher_id
from teacher_table);

ANY只要大于值列表中的最小值即可。ALL要求大于值列表中的最大值。
下面的SQL语句选出student_table表中的student_id大于teacher_table表中所有teacher_id的记录。

select *
from student_table
where student_id>
all(select teacher_id
from teacher_table);

还有一种子查询可以返回多行、多列,此时where子句中应该有对应的数据列,并使用圆括号将多个数据列组合起来。SQL语句如下:

select *
from student_table
where (student_id,student_name)
= any(select teacher_id,teacher_name
from teacher_table);

集合运算

select语句查询的结果是一个包含多条数据的结果集,类似于数学里的集合,可以进行交(intersect)、并(union)和差(minus)运算,select查询得到的结果集也可能需要进行这三种运算。
为了对两个记过集进行集合运算,这两个结果必须满足如下条件。

  • 两个结果集所包含的数据列的数量必须相等。
  • 两个结果所包含的数据列的数据类型也必须一一对应。

1.union运算
union运算的语法格式如下:

select语句 union select语句

下面的SQL语句查询出所有教师的信息和主键小于4的学生信息。

#查询结果集包含两列,第一列为int类型,第二列为varchar类型
select*from teacher_table
union
#这个结果集的数据列必须与前一个结果集的数据列一一对应
select student_id,student_name from student_table;

2.minus运算
minus运算的语法格式如下:

select语句 minus select语句

上面的语法格式十分简单,不过很遗憾,MySQL并不支持使用minus运算符,因此只能借助于子查询来“曲线”实现上面的minus运算。
加入想从所有学生记录中“减去”与老师记录的ID相同、姓名相同的记录,则可进行如下的minus运算:

select student_id,student_name from student_table
minus
#两个结果集的数据列的数量相等,数据类型一一对应,可以进行minus运算
select teacher_id,teacher_name from teacher_table;

不过MySQL并不支持这种运算。但可以通过如下子查询来实现上面运算。

select student_id,student_name from student_table
where(student_id,student_name)
not in
(select teacher_id,teacher_name from teacher_table);

3.intersect运算
intersect运算的语法格式如下:

select 语句 intersect select 语句

上面的语法格式也十分简单,不过很遗憾,MySQL并不支持使用intersect运算符,因此只能借助于多表连接查询来“曲线”实现上面的intersect运算。
假如向找出学生记录中与老师记录中的ID相同、姓名相同的记录,则可进行如下的intersect运算:

select student_id,student_name from student_table
intersect
#两个结果集的数据列和数量相同,数据类型一一对应,可以进行intersect运算
select teacher_id,teacher_name from teacher_table;

不过MySQL并不支持这种运算。但可以通过如下多表连接查询来实现上面运算

select student_id,student_name from student_table
join
teacher_table
on(student_id=teacher_id and student_name=teacher_name);

需要指出的是,如果进行ntersect运算的两个select子句中都包含了where条件,那么将intersect运算改成多表连接查询后还需要将两个where条件进行and运算。假如有如下intersect运算的SQL语句:

select student_id,student_name from student_table where student_id <4
intersect
#两个结果集的数据列的数量相等,数据类型一一对应,可以进行intersect运算
select teacher_id,teacher_name from teacher_table where_name like '李%';

上面语句改成如下:

select student_id,student_name from student_table
join
teacher_table
on(student_id = teacher_id and student_name = teacher_name)
where student_id < 4 and teacher_name like '李%';

JDBC的典型用法

掌握了标准的SQL命令语法之后,就可以开始使用JDBC开发数据库应用了。

JDBC4.2常用接口和类简介

Java支持JDBC4.2标准,JDBC4.2在原有JDBC标准上增加了一些新特性。下面介绍这些JDBC API时会提到Java8新增的功能。

  • DriverManager:用于管理JDBC驱动的服务类。程序中使用该类的主要功能是获取Connection对象,该类包含如下方法。
public static synchronized Connection getConnection(String
url,String user,String pass
)throws SQLException:该方法获得url对应的数据库的连接
  • Connection:代表数据库连接对象,每个Connection代表一个物理会话。要想访问数据库必须先获得数据库连接。该接口的常用方法如下。
Statement createStatement()throws SQLException:该方法返回一个Statement对象。
preparedStatement prepareStatement(String sql)thorws
SQLException:该方法返回预编译的Statement对象,
即将SQL语句提交到数据库进行编译。
CallableStatement preareCall(String sql)thorw 
SQLException:该方法返回CallableStatement对象,该对象用于调用存储过程。

上面三个方法都返回用于执行SQL语句的Statement对象,PreparedStatement、CallableStatement是Statement的子类,只有获得了Statement之后的语句才可执行SQL语句。
除此之外,Connection还有如下几个用于控制事务的方法。

  • Savepoint setSavepoint():创建一个保存点。
  • Savepoint setSavepoint(String name):以指定名字来创建一个保存点。
  • void setTransactionIsonlation(int level):设置食物的隔离级别。
  • void rollback():回滚事务。
  • void rollback(Savepoint savepoint):将事务回滚到指定的保存点。
  • void setAutoCommit(boolean autoCommit):关闭自动提交,打开事务。
  • void commit():提交事务。

Java7为Connection新增了setSchema(String schema)、getSchema()两个方法,这两个方法用于控制该Connection访问的数据库Schema。Java7还为Connection新增了setNetworkTimeout(Executor executor,int milliseconds)、getNetworkTimeout()两个方法来控制数据库连接的超时行为。

  • Statement:用于执行SQL语句的工具接口。该对象既可用于执行DDL、DCL语句,也可用于执行DML语句,还可用于执行SQL查询。当执行SQL查询时,返回查询到的结果集。它的常用方法如下。

  • ResultSet executeQuery(String sql)thorws SQLException:该方法用于执行查询语句,并返回查询结果对应的ResultSet对象。该方法只能用于执行查询语句。

  • int executeUpdate(String sql)thorws SQLExcetion:该方法用于执行DML语句,并返回受影响的行数;该方法也可以执行DDL语句,执行DDL语句将返回0。

  • boolean execute(String sql)throws SQLException:该方法可执行任何SQL语句。如果执行后第一个结果为ResultSet对象,则返回true;如果执行后第一个结果为受影响的行数或没有任何结果,则返回false。

Java7为Statement新增了closeOnCompletion()方法,如果Statement执行了该方法,则当所有依赖于该Statement的ResultSet关闭时,该Statement会自动关闭。Java7还为Statement提供了一个isCloseOnCompletion()方法,该方法用于判断该Statement是否打开了"CloseOnCompletion"。
Java8为Statement新增了多个重载的executeLargeUpdate()方法,这些方法相当于增强版的executeUpdate()方法,返回值类型为long——也就是说,当DML语句影响大记录条数据超过Integer.MAX_VALUE时,就应该使用executeLargeUpdate()方法。
考虑到目前应用程序所处理的数据列越来越大,使用executeLargeUpdate()方法具有更好的适用性。但遗憾的是,目前最新的MySQL驱动暂不支持该方法。
-PreparedStatement:预编译的Statement对象。PreparedStatement是Statement的子接口,它允许数据库预编译SQL语句(这些SQL语句通常带有参数),以后每次只能改变SQL命令的参数,避免数据库每次都要编译SQL语句,因此性能更好。相对于Statement而言,使用PreparedStatement执行SQL语句时,无须再传入SQL语句,只要为预编译的SQL语句传入参数值即可。所以它比Statement多了如下方法。

  • void setXxx(int parameterIndex,Xxx value):该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引给SQL语句中指定位置的参数。。

PreparedStatement同样有executeUpdate()、executeQuery()和execute()三个方法,只是这三个方法无须接收SQL字符串,因为PreparedStatement对象已经预编译了SQL命令,只要为这写命令传入参数即可。Java8还为PreparedStatement增加了不带参数的executeLargeUpdate()方法——执行DML语句影响的记录条数可能超过Inter.MAX_VALUE时,就应该使用executeLargeUpdate()方法。

  • ResultSet:结果集对象。该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名获得列数据。它包含了如下常用方法来移动记录指针。
  • void close():释放ResultSet对象。
  • boolean absolute(int row):将结果集的记录指针移动到第row行,如果row是负数,则移动到倒数第row行。如果移动后的记录指针指向一条有效记录,则该方法返回true。
  • void beforeFirst():将ResultSet的记录指针定位到首行之前,这是ResultSet结果集记录指针的初始状态——记录指针的初始位置位于第一行之前。
  • boolean previous():将ResultSet的记录指针定位到上一行。如果移动后的记录指针指向一条有效记录,则该方法返回true。
  • boolean next():将ResultSet的记录指针定位下一行,如果移动后的记录指针向一条有效记录,则该方法返回true。
  • boolean last():将ResultSet的记录指针定位到下一行,如果移动后的记录指针指向一条有效记录,则该方法返回true。
  • void afterLast():将ResultSet的记录指针定位到最后一行之后。
  • 在JDK1.4以前,采用默认方法创建的Statement所查询得到的ResultSet不支持absolute()、previous()等移动记录指针的方法,它只支持next()这个移动记录指针的方法,即ResultSet记录指针只能向下移动,而且每次只能移动一格。从Java5.0以后就避免了这个问题,程序采用默认方法创建的Statement所查询得到的ResultSet也支持absolute()、previous()等方法。

当把记录指针移动到指定行之后,ResultSet可通过getXxx(int columnIndex)或getXxx(String cloumnLabel)方法来获取当前行、指定列的值,前者根据列索引获取值,或者根据列名获取值。Java7新增了

JDBC编程步骤

大致了解了JDBC API的相关接口和类之后,下面就可以进行JDBC编程了,JDBC编程大致按如下步骤进行。

1.加载数据库驱动。通常使用Class类的forName()静态方法来加载驱动。例如如下代码:

//加载驱动
Class.forName(driverClass)

最新的JDBC驱动已经可以通过SPI自动注册驱动类了,在JDBC驱动JAR包的META_INF\services路径下回包含一个java.sql.Driver文件,该文件指定了JDBC驱动类。因此,如果使用这种最新的驱动JAR包,第一步其实可以忽略。
上面代码中的driverClass就是数据库驱动类所对应的字符串。例如,加载MySQL的驱动采用如下代码:

//加载MySQL的驱动
Class.forName("com.mysql.cj.jdbc.Driver");

而加载Oracle的驱动则采用如下代码:

//加载Oracle的驱动
Class.forName("oracle.jdbc.driver.OracleDriver");

从上面代码中可以看出,加载驱动时并不是真正使用数据库的驱动类,只是使用数据库驱动类名的字符串而已。
前面给出的仅仅是MySQL和Oracle两种数据库的驱动,我看不出驱动类字符串有什么规律啊。如果我希望使用其他数据库,那怎么找到其他数据库的驱动类呢?
不同数据库的驱动类确实没有什么规律,也无须记住这些驱动类。因为每个数据库厂商在提供数据库驱动(通常是一个JAR文件)时,总会提供相应的文档,其中会有关于驱动类的介绍。不仅如此,文档还会提供数据库URL写法,以及连接数据库的范例代码。当然作为一个Java程序员,代码写得多了,常见数据库的驱动类、URL写法还是能记住的——无须刻意去记忆,自然而然就记住了。

2.通过DriverManager获取数据库连接。DriverManager提供了如下方法。

//获取数据库连接
DriverManager.getConnection(String url,String user,String pass);

当使用DriverManager获取数据库连接时,通常需要传入三个参数:数据库URL、登入数据库的用户名和密码。这三个参数中用户名和密码通常由DBA(数据库管理员)分配,而且该用户还应该具有相应的权限,才会执行相应的SQL语句。
数据库URL通常遵循如下写法:

jdbc:subprotocol:other stuff

上面URL写法中的jdbc是固定的,而subprotocol指定连接到特定数据库的驱动,而后面的other stuff也是不固定的——也没有较强的规律,不同的数据库的URL写法可能存在较大差异。例如,MySQL数据库的URL写法如下:

jdbc:mysql://hostname:port/databasename

Oracle数据库的URL写法如下:

jdbc:oracle:thin:@hostname:port:databasename

如果想了解特定数据库的URL写法,查询该数据库JDBC驱动的文档。

3.通过Connection对象创建Statement对象

Connection创建Statement的方法有如下三个。

  • createStatement():创建基本的Statement对象。
  • prepareStatement(String sql):根据传入的SQL语句创建预编译的Statement对象。
  • prepareCall(String sql):根据传入的SQL语句创建CallableStatement对象。

4.使用Statement执行SQL语句。所有的Statement都有如下三个方法来执行SQL语句。

  • excute():可执行任何SQL语句,但比较麻烦。
  • executeUpdate():只要用于执行DML和DDL语句。执行DML语句返回受SQL语句影响的行数,执行DDL语句返回0。
  • executeQuery():只能执行查询语句,执行后返回代表查询结果的ResultSet对象。

5.操作结果集

如果执行的SQL语句是查询语句,则执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果。程序可以通过操作该ResultSet对象来去除查询街而过。ResultSet对象主要提供了如下两类方法。

  • next()、previous()、first()、last()、beforeFirst()、afterLast()、absolute()等移动记录指针的方法。
  • getXxx()方法获取记录指针指向行、特定列的值。该方法即可使用列索引作为参数,也可使用列名作为参数。使用列索引作为参数性能更好,使用列名作为参数可读性更好。

6.回收数据库资源

包括关闭ResultSet、Statement和Connection等资源

下面程序简单示范了JDBC编程,并通过ResultSet获取结果集的过程。

import java.sql.*;

public class ConnMySql {
    public static void main(String[] args) throws Exception {
        //1.加载驱动,使用反射知识,现在记住这么写
        Class.forName("com.mysql.cj.jdbc.Driver");
        try(
                //2.使用DriverManager获取数据库连接
                //其中返回的Connection就代表了Java程序和数据库的连接
                //不同数据库的URL写法需要查驱动文档,用户名、密码由DBA分配(数据库管理员)
                Connection conn = DriverManager.getConnection(
                        "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&serverTimezone=UTC",
                        "root","123456789");
                //3.使用Connection来创建一个Statement对象
                Statement stmt = conn.createStatement();
                //4.执行SQL语句
                /*
                Statement有三种执行SQL语句的方法:
                1.execute()可执行任何SQL语句-返回一个boolean值
                如果执行后第一个结果是ResultSet,则返回true,否则返回false
                2.executeQuery()执行select语句-返回查询到的结果集
                3.executeUpdate()用于执行DML语句-返回一个整数
                */
                //这里这行代码很简单,我们可以专门来解读一下
                /*
                这里主要看SQL语句
                @select s.*这里的 s是 student_table的表别名 后面的*代表了所有的列
                @teacher_name 这是双表查询的数据
                @from student_table s,teacher_table t 起别名以及查询来自 student_table和teacher_table
                @where t.teacher_id = s.java_teacher 代表了选择哪行
                也就是说 teacher_name的筛选条件是teacher_id[teacher_table]等于java_teacher[student_table]
                 */
//                ResultSet rs = stmt.executeQuery("select s.*,teacher_name" +
//                        "from student_table s,teacher_table t" +
//                        "where t.teacher_id = s.java_teacher")
                //ResultSet rs = stmt.executeQuery("select s.*,teacher_name+from student_table s,teacher_table t where t.teacher_id = s.java_teacher")
                ResultSet rs = stmt.executeQuery("select s.*,teacher_name "+
                        "from student_table s, teacher_table t "+
                        "where t.teacher_id = s.java_teacher")
        )
        {
            //ResultSet有一系列的getXxx(列索引|列名)方法,用于获取记录指针
            //指向行、特定列的值,不断使用next()将记录指针下移一行

            while(rs.next()){
                System.out.println(rs.getString(1)+"\t"
                        +rs.getString(2)+"\t"
                        +rs.getString(3)+"\t"
                        +rs.getString(4));
            }

        }
    }
}

第十三章 MySQL数据库与JDBC编程_第22张图片
第十三章 MySQL数据库与JDBC编程_第23张图片
上面程序严格按JDBC访问数据库的步骤执行了一条多表连接查询语句,这条连接查询语句就是前面介绍SQL92连接时所讲的连接查询语句。
与前面介绍的步骤略有区别的是,本程序采用了自动关闭资源的try语句来关闭各种数据库资源,Java7改写了Connection、Statement、ResultSet等接口,它们都继承了AutoCloseable接口,因此它们都可以由try语句来关闭。
运行上面程序看到控制台输出的语句结果内容。

执行SQL语句的方式

前面介绍了JDBC执行查询示例程序,实际上,JDBC不仅可以执行查询,也可以执行DDL、DML等SQL语句,从而允许通过JDBC最大限度地控制数据库。

使用executeLargeUpdate方法执行DDL和DML语句

Statement提供了三个方法来执行SQL语句,前面已经介绍了使用executeQuery()来执行查询语句,下面将介绍使用executeLargeUpdate()(或executeUpdate())来执行DDL和DML语句。使用Statement执行DDL和DML语句的步骤与执行普通查询语句的步骤基本相似,区别在于执行了DDL语句后返回值为0,执行了DML语句后返回值为受影响的记录条数。
下面程序师范了使用executeUpdate()方法(此处暂未使用executeLargeUpdate()方法因为MySQL驱动不支持)创建数据表。该示例并没有直接把数据库连接信息写在程序里,而是使用一个mysql.ini文件(就是一个properties文件)来保存数据库连接信息,这是比较成熟的做法——当需要把应用程序从开发环节移植到生产环节时,无须修改源代码,只需要修改mysql.ini配置文件即可。

import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.Properties;

public class ExecuteDML{
    private String driver;
    private String url;
    private String user;
    private String password;
    public void initParam(String paramFile) throws Exception{
        //使用Properties类来加载文件
        var props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
    }
    public int insertData(String sql)throws Exception{
        //加载驱动
        Class.forName(driver);
        try(
                //获取数据库连接
                Connection conn = DriverManager.getConnection(url,user,password);
                //使用Connection来创建一个Statement对象
                Statement stmt = conn.createStatement();
                )
        {
            //执行DML语句,返回受影响的记录条数
            return stmt.executeUpdate(sql);
            //,teacher_table twhere s.java_teacher=t.teacher_id
        }
    }

    public static void main(String[] args) throws  Exception{
        var ed = new ExecuteDML();
        ed.initParam("D:\\DemoProject\\src\\mysql.ini");
        int result = ed.insertData(
               "insert into jdbc_test(jdbc_name,jdbc_desc) " +
                        "select s.student_name,t.teacher_name " +
                        "from student_table s,teacher_table t "+
                        "where s.java_teacher = t.teacher_id;"
        );
//            int result = ed.insertData(
//                    "insert into jdbc_test(jdbc_name,jdbc_desc)select s.student_name,t.teacher_name from student_table s,teacher_table t where s.java_teacher = t.teacher_id;"
//
//            );
        System.out.println("-----系统中共有:"+result+"条记录受影响-----");
    }

}

第十三章 MySQL数据库与JDBC编程_第24张图片

第十三章 MySQL数据库与JDBC编程_第25张图片
这里关于ini文件详细如下:
第十三章 MySQL数据库与JDBC编程_第26张图片
运行上面程序,执行成功后悔看到select_test数据库中添加了一个jdbc_test数据表,这表明JDBC执行DDL语句成功。
使用executeUpdate()执行DML语句与执行DDL语句基本相似,区别是executeUpdate()执行DDL语句后返回0,而执行DML语句后返回受影响的记录行数。下面程序将会执行一条insert语句,这条insert语句会向刚刚建立的jdbc_test数据表中插入几条记录。因为使用了带子查询的insert语句,所以可以一次插入多条语句。
第十三章 MySQL数据库与JDBC编程_第27张图片
第十三章 MySQL数据库与JDBC编程_第28张图片
这里的字符串SQL语句一定要在 +号前带一个空格 否则会提示 SQL Version异常 语句不正常 提示要求查看文档修正语法,其实就是没加空格导致语句代码都连接到了一起。
这里异常会提示到你没有空格的地方 比如:swhere,在连接处的相连异常。

使用execute方法执行SQL语句

Statement的execute()方法几乎可以执行任何SQL语句,但它执行SQL语句时比较麻烦,通常没有必须使用execute()方法来执行SQL语句,使用executeQuery()或executeUpdate()方法更简答。但如果不清楚SQL语句的类型,则只能使用execute()方法来执行该SQL语句了。
使用execute()方法执行SQL语句的返回值只能是boolean值,它表明执行该SQL语句是否返回了ResultSet对象
Statement提供了如下两个方法来获取执行结果。

  • getResultSet():获取该Statement执行查询结余所返回的ResultSet对象。
  • getUpdateCount():获取该Statement()执行DML语句所影响的记录行数。
    下面程序示范了使用Statement的execute()方法来执行任意的SQL语句,执行不同的SQL语句时产生不同的输出。
import java.io.FileInputStream;
import java.sql.*;
import java.util.Properties;

public class ExecuteSQL{
    private String driver;
    private String url;
    private String user;
    private String password;
    public void initParam(String paramFile)throws Exception{
        //使用Properties类来加载属性文件
        var props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
    }
    public void executeSql(String sql)throws Exception{
        //加载驱动
        Class.forName(driver);
        try(
                //获取数据库连接
                Connection conn = DriverManager.getConnection(url,user,password);
                //使用Connection来创建一个Statement对象
                Statement stmt = conn.createStatement();
                ){
            //执行SQL语句,返回boolean值表示是否包含ResultSet
            boolean hasResultSet = stmt.execute(sql);
            //如果执行后有ResultSet结果集
            if(hasResultSet){//在if语句中布尔值如果没有显式指定数值则默认条件为 if(hasResultSet==true) else (hasResultSet==false)
                try(
                        //获取结果集
                        ResultSet rs = stmt.getResultSet();
                        ){
                    //ResultSetMetaData是用于分析结果集的元数据接口
                    ResultSetMetaData rsmd = rs.getMetaData();
                    int columnCount = rsmd.getColumnCount();//用于记录受影响的行数
                    //迭代输出ResultSet对象
                    while(rs.next()){
                        //依次输出每列的值
                        for(var i = 0; i < columnCount; i++){
                            System.out.println(rs.getString(i+1)+"\t");//这里 i+1可能是 数组是从0开始的 但是数据是从1开始的
                        }
                        System.out.println("\n");
                    }
                }
            }else {
                System.out.println("该SQL语句影响的记录有"+stmt.getUpdateCount()+"条");
            }
        }
    }

    public static void main(String[] args)throws Exception {
        var es = new ExecuteSQL();
        es.initParam("D:\\DemoProject\\src\\mysql.ini");
        System.out.println("----------执行删除表的DDL语句");
        es.executeSql("drop table if exists my_test");
        System.out.println("----------执行建表的DDL语句----------");
        es.executeSql("create table my_test "+"(test_id int auto_increment primary key, "+"test_name varchar(255))");
        System.out.println("----------执行插入数据的DML语句----------");
        es.executeSql("insert into my_test(test_name) "+"select student_name from student_table");
        System.out.println("----------执行查询数据的查询语句----------");
        es.executeSql("select*from my_test");
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=50179:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject;D:\DemoProject\lib\mysql-connector-java-8.0.13.jar ExecuteSQL
----------执行删除表的DDL语句
该SQL语句影响的记录有0----------执行建表的DDL语句----------
该SQL语句影响的记录有0----------执行插入数据的DML语句----------
该SQL语句影响的记录有6----------执行查询数据的查询语句----------
1	
吴同学	


2	
周同学	


3	
李同学	


4	
鹏同学	


5	
佘同学	


6	
杜同学	



Process finished with exit code 0

第十三章 MySQL数据库与JDBC编程_第29张图片
这里代码挺长的,没必要专门记住,能看懂就行。
这里允许先插入一段,如果你已经成功理解到了现在包括前面的MySQL入门文章和JDBC文章,还是零基础,那是真的厉害,至少我在写这三个文章的学习途中问题特别多,都很坎坷,极度考验了自己解决问题的能力,这里也希望大家可以多锻炼自己通过搜索 浏览 下载等方式来解决很多问题,比如自己想下载个单机游戏,可是网络上的都是病毒,但是总是有可以下载的无毒的安装包的,没有不存在的,只有找不到的,这就是互联网开放的信息时代!
回到正题。
运行上面程序会看到使用Statement的不同方法执行不同SQL语句的效果。执行DDL语句显示受影响的记录条数为0;执行DML语句插入、修改或删除的记录条数;执行查询语句则可以输出查询结果。
上面程序获得SQL执行结果时没有根据各列的数据类型调用相应的getXxx()方法,而是直接通过getString()方法来取得值,这是可以的。ResultSet的getString()方法几乎可以获取除Blob之外的任意类型列的值,因为所有的数据类型都可以自己转换成字符串类型。

使用PreparedStatement执行SQL语句

如果经常需要返回执行一条结构相似的SQL语句,列如如下两条SQL语句:

insert into student_table valeus(null,'张三',1);
insert into student_table values(null,'李四',2);

对于这两条SQL语句而言,它们的结构基本相似,只是执行插入时的值不同而已。对于这种情况,可以使用带占位符(?)参数的SQL语句来替代它。

insert into student_table values(null,?,?);

但Statement执行语句时不允许使用问号占位符参数,而且这个问号占位符参数必须获得值后才可以执行。为了满足这种功能。JDBC提供了PreparedStatement接口,它是Statement接口的子接口,它可以预编译SQL语句,预编译后的SQL语句被存储在PreparedStatement对象中,然后可以使用该对象多次高效地执行该语句。简而言之,使用PreparedStatement比使用Statement的效率要高。
创建PreparedStatement对象使用Connection的prepareStatement()方法,该方法需要传入一个SQL字符串,该SQL字符串可以包含占位符。代码如下:

//创建一个PreparedStatement对象
pstmt = conn.prepareStatement("insert into student_table values(null,?,1)");

PreparedStatement也提供了execute()、executeUpdate()、executeQuery()三个方法来执行SQL语句,不过这三个方法无须参数,因为PreparedStatement已存储了预编译的SQL语句。
使用PreparedStatement预编译SQL语句时,该SQL语句可以带占位符参数,因此在执行SQL语句之前必须为这些参数传入参数值,PreparedStatement提供了一些列的setXxx(int index,Xxx value)方法来传入参数值。
如果程序很清楚PreparedStatement预编译SQL语句中参数的类型,则使用相应的setXxx()方法传入参数即可;如果程序不清楚预编译SQL语句中的各参数的类型,则可以使用setObject()方法来传入参数,由PreparedStatement来负责类型转换。
下面程序示范了使用Statement和PreparedStatement分别插入100条记录对比。使用Statement需要传入100条SQL语句,但使用PreparedStatement则只需要传入1条预编译的SQL语句,然后100次为该PreparedStatement的参数设置即可。

import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.Properties;

public class PreparedStatementTest{
    private String driver;//加载驱动路劲
    private String url;//网络定位符 连接
    private String user;//用户
    private String password;//密码
    public void initParam(String paramFile) throws Exception{
        //使用Properties类来加载属性文件
        var props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
        //加载驱动
        Class.forName(driver);
    }
    public void insertUseStatement() throws Exception{
        long start = System.currentTimeMillis();
        try(
                //获取数据库连接
                Connection conn = DriverManager.getConnection(url,user,password);
                //使用Connection来创建一个Statement对象
                Statement stmt = conn.createStatement()
                ){
            //需要使用100条SQL语句来插入100条记录
            for(var i=0;i<100;i++){
                stmt.executeUpdate("insert into student_table values("+"null,'姓名"+i+"',1)");
            }
            System.out.println("使用Statement费时:"+
                    (System.currentTimeMillis()-start));
        }
    }
    public void insertUsePrepare() throws Exception{
        long start = System.currentTimeMillis();
        try(
                //获取数据库连接
                Connection conn = DriverManager.getConnection(url,user,password);
                //使用Connection来创建一个PreparedStatement对象
                PreparedStatement pstmt = conn.prepareStatement("insert into student_table values(null,?,1)")
                ){
            //100次为PreparedStatement的参数设置值,就可以插入100条记录
            for(var i =0;i<100;i++){
                pstmt.setString(1,"姓名"+i);
                pstmt.executeUpdate();
            }
            System.out.println("使用PreparedStatement费时:"+
                    (System.currentTimeMillis()-start));
        }
    }

    public static void main(String[] args) throws Exception{
        var pt = new PreparedStatementTest();
        pt.initParam("D:\\DemoProject\\src\\mysql.ini");
        pt.insertUseStatement();
        pt.insertUsePrepare();
    }
}

第十三章 MySQL数据库与JDBC编程_第30张图片
第十三章 MySQL数据库与JDBC编程_第31张图片
多次允许上面程序,可以发现使用PreparedStatement插入100条记录所用的时间比Statement插入100条记录所用的时间少,这表明PreparedStatement的执行效率比Statement的执行效率高。
除此之外,使用PreparedStatement还有一个优势——当SQL语句中要使用参数时,无须“拼接”SQL字符串。而使用Statement则要“拼接”SQL字符串,这是相当容易出现错误的——注意上面的单引号,这是因为SQL的字符串必须使用单引号引起来。尤其当SQLy语句中有多个字符串参数时,“拼接”这条SQL语句就更容易出错了。当使用PreparedStatement则只需要使用问号占位符来替代这些参数即可,降低了编程复杂度。
使用PreparedStatement还有一个很好的作用——用于防止SQL注入。
SQL注入是一个较常见的Cracker入侵方式,它利用SQL语句的漏洞来入侵。
下面以一个简单的登入窗户为例来介绍这种SQL注入的结果。下面登入窗口包含两个文本框每一个用于输入用户名,一个用于输入密码,系统根据用户输入jdbc_test表里的记录进行匹配,如果找到相应记录则提示登入成功。

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

public class LoginFrame{
    private final String PROP_FILE = "D:\\DemoProject\\src\\mysql.ini";
    private String driver;
    //url是数据库的服务地址
    private String url;
    private String user;
    private String password;
    //登入界面的GUI组件
    private JFrame jf = new JFrame("登入");
    private JTextField userField = new JTextField(20);
    private JTextField passwordField = new JTextField(20);
    private JButton loginButton = new JButton("登入");
    public void init() throws Exception{
        var connProp = new Properties();
        connProp.load(new FileInputStream(PROP_FILE));
        driver = connProp.getProperty("driver");
        url = connProp.getProperty("url");
        user = connProp.getProperty("user");
        password = connProp.getProperty("password");
        //加载驱动
        Class.forName(driver);
        //为登入按钮添加事件监听器
        loginButton.addActionListener(e->{
            //登入成功则显示"登入成功"
            if(validate(userField.getText(),passwordField.getText())){
                JOptionPane.showMessageDialog(jf,"登入成功");
            }
            else
            {
                JOptionPane.showMessageDialog(jf,"登入失败");
            }
        });
        jf.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        jf.add(userField, BorderLayout.NORTH);
        jf.add(passwordField);
        jf.add(loginButton,BorderLayout.SOUTH);
        jf.pack();
        jf.setVisible(true);
    }
    private boolean validate(String userName,String userPass){
        //执行查询SQL语句
        var sql = "select*from jdbc_test "+
                "where jdbc_name='"+userName+
                "' and jdbc_desc ='" +userPass+"'";
                System.out.println(sql);
        try(
                Connection conn = DriverManager.getConnection(url,user,password);
                Statement stmt = conn.createStatement();
                ResultSet rs = stmt.executeQuery(sql)
        ){
            //如果查询的ResultSet里有超过一条的记录,则登入成功
            if(rs.next()){
                return true;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }
    public static void main(String[] args) throws Exception{
        new LoginFrame().init();
    }
}

第十三章 MySQL数据库与JDBC编程_第32张图片
第十三章 MySQL数据库与JDBC编程_第33张图片
第十三章 MySQL数据库与JDBC编程_第34张图片
第十三章 MySQL数据库与JDBC编程_第35张图片
可以看到程序运行的后台:

#利用SQL注入后生成的SQL语句
select*from jdbc_test where jdbc_name ='' or true
or '' and jdbc_desc = ''

看到这条SQL语句,读者应该不难明白为什么这样输入也可以显示“正常登入”
第十三章 MySQL数据库与JDBC编程_第36张图片
因为Cracker直接输入了true,而SQL把这个true当成了直接了。
JDBC编程本身并没有提供图形界面功能,它仅仅提供了数据库访问支持。如果希望JDBC程序有较好的图形界面,则需要结合前面介绍的AWT或Swing编程才可以做到。在Web编程中,数据库访问也是非常重要的基础知识。
如果把上面的validate()方法换成使用PreparedStatement来执行验证,而不是直接使用Statement。程序如下:

private boolean validate(String userName,String userPassword){
	try(
		Connection conn = DriverManager.getConnection(url,user,password);
		PreparedStatement pstmt = conn.prepareStatement(
		"select*from jdbc_test where jdbc_name =?
		and jdbc_desc =?"))
	){
		try(
			ResultSet rs = pstmt.executeQuery()
		){
			//如果查询的ResultSet里有超过一条记录,则登入成功
			if(rs.next()){
				return true;
			}
		}
	}catch(Exception e){
		e.printStackTrace();
	}
	return false;
}

将上面的validate()方法改为使用PrepareStatement来执行SQL语句之后
第十三章 MySQL数据库与JDBC编程_第37张图片
下面是未修改并利用注入产生的结果
第十三章 MySQL数据库与JDBC编程_第38张图片
总体来看,使用PreparedStatement比使用Statement多了如下三个好处。

  • PreparedStatement预编译SQL语句,性能更好。
  • PreparedStatement无须“拼接”SQL语句,编程更简单。
  • PreparedStatement可以防止SQL注入,安全性更好。

基于上面三点,通常推荐避免使用Statement来执行SQL语句,改为使用PreparedStatement执行SQL语句。
使用PreparedStatement执行带占位符参数的SQL语句时,SQL语句中的占位符参数只能替代普通纸,不要使用占位符替代表名、列名等数据库对象,更不要用占位符参数来代替SQL语句中insert、select等关键字。

这里原因特殊 我们直接跳过,后续的MySQL直接开启新阶段来学习

你可能感兴趣的:(笔记,java)