JDBC(Java DataBase Connectivity) :Java数据库连接技术:具体讲就是通过Java连接广泛的数据库,并对表中数据执行增、删、改、查等操作的技术。
学习过SQL后,可以通过DataGrip
、 Navicat
、SQLyog
等图形化客户端发送SQL操作数据库。本质上,JDBC的作用和图形化客户端的作用相同,都是发送SQL操作数据库。差别在图形化界面的操作是图形化、傻瓜化的,而JDBC则需要通过编码(先不要思考JDBC代码怎么写,也不要觉得它有多难)完成图形操作时的效果。
总结:JDBC本质上也是一种发送SQL操作数据库的client技术,只不过需要通过Java编码完成。
为什么要学习JDBC? 既然说JDBC和 Navicat
的作用相同,而 Navicat
图形化操作很简单(我们也很熟练),那么为什么还要学习操作更复杂(需要编码)、更陌生的新的Java客户端技术呢?
Java是可编程的(可定制化),可以将普通用户输入的数据拼接成各种各样的可运行的SQL,从而降低普通用户使用数据库服务的难度。
举个例子:任何系统中都有登录功能,登录时需要从用户表中根据用户名查询然后比对密码,每个人的用户名密码肯定不同,执行的select语句的条件部分就不同,直接让用户通过
Navicat
操作数据库,就得由用户拼接SQL,这对于小白用户要求过高。而JDBC就可以通过Java代码将用户输入的用户名密码拼接到SQL中完成查询,普通小白用户通过JDBC程序操作数据库时,只要会输入用户名密码即可!!!
1.2.1 JDBC涉及的API
JDBC要通过Java代码操作数据库,JDBC中定义了操作数据库的各种接口和类型:
Driver: 驱动接口,定义了Java如何和数据库获取连接
DriverManager: 管理驱动的工具类, 可以管理多种驱动,通过它可以获取和数据库的连接
Connection:连接接口,通过它完成Java和数据库之间的交互
DataSource::数据源接口,用来管理Java和数据库建立的连接,可以复用之前建立的连接
PreparedStatement: 发送SQL的工具接口,该类型对象用于向数据库发送一条SQL
ResultSet: 结果集接口, 该类型的对象表示一条查询SQL返回的结果
注意:学习到这里不需要你掌握上述的任何一个类型(后续会逐步学习到每一个类型),只需要加深理解JDBC需要通过编码操作数据库(也就是JDBC的可编程属性)即可。
1.2.2 JDBC原生编程示例(了解)
之前,我们已经反复强调JDBC是一种和 Navicat
相当的客户端程序,那么JDBC操作数据库的步骤和 Navicat
操作数据库步骤就大同小异,下面我们先回顾下 Navicat
的操作步骤,然后分析出JDBC的步骤。
总结:开发步骤
1. 加载驱动
2. 获取链接:配置url、username和password
3. 准备SQL以及发送SQL的工具
4. 执行SQL
5. 处理结果集
6. 释放资源
需求:从t_person
表中查询数据
-- 数据准备
create table t_person(
person_id int primary key auto_increment,
person_name varchar(20),
age tinyint,
sex varchar(6),
mobile varchar(20),
address varchar(200)
);
insert into t_person values(1,'王月华',18,'F','1563868xxxx','郑州'),(2,'刘天赐',18,'M','1563768xxxx','郑州');
-- 需求 查询t_person表所有数据
select * from t_person;
准备工作(搭建开发环境)
需要在项目中引入数据库驱动jar包(jar文件:针对class文件的压缩格式包含了多个带包的class文件,类似于普通文件打包的zip、rar)
编码(JDBC 6步)
public class JDBCTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1 加载驱动
/*
驱动版本8.0.x com.mysql.cj.jdbc.Driver
驱动版本5.1.x com.mysql.jdbc.Driver
*/
Class.forName("com.mysql.cj.jdbc.Driver");
//2 获取连接
String username = "root";//用户名
String password = "123456";//密码
/*
url参数用来确定连接的数据库信息: 数据库机器ip 端口号port 数据库名db_name 连接的参数,比如编解码集、时区...
url格式:jdbc:mysql://ip:port/db_name?k=v参数 ,只需要了解url的组成,不需要记忆
*/
String url = "jdbc:mysql://localhost:3306/baizhi?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai";
Connection conn = DriverManager.getConnection(url, username, password);//通过DriverManager管理驱动获取连接
//3 准备发送SQL的工具
String sql = "select * from t_person";
PreparedStatement pstm = conn.prepareStatement(sql);
//4 发送执行SQL
ResultSet rs = pstm.executeQuery();
//5 处理结果集(如果有)
while(rs.next()){
/*
rs.getXxx(列顺序从1开始) 或者 rs.getXxx("列名") 获取指定列的数据,Xxx为数据类型
实战中多使用列名,可读性强
*/
int personId1 = rs.getInt("person_id");
String personName1 = rs.getString("person_name");
int age1 = rs.getInt("age");
String sex1 = rs.getString("sex");
String mobile1 = rs.getString("mobile");
String address1 = rs.getString("address");
System.out.println("personId="+personId1+",personName="+personName1
+",age="+age1+",sex="+sex1+",mobile="+mobile1+",address="+address1);
int personId2 = rs.getInt(1);
String personName2 = rs.getString(2);
int age2 = rs.getInt(3);
String sex2 = rs.getString(4);
String mobile2 = rs.getString(5);
String address2 = rs.getString(6);
System.out.println("personId="+personId2+",personName="+personName2
+",age="+age2+",sex="+sex2+",mobile="+mobile2+",address="+address2);
}
//6 释放资源(反序关闭:后出现先关闭)
rs.close();
pstm.close();
conn.close();
}
}
JdbcTemplate是Spring对JDBC的封装,目的是使JDBC更加易于使用。JDBC如同是毛坯房,而JdbcTemplate类似于精装房,使用JdbcTemplate会比原生JDBC更简单舒适一些。
1. 创建DataSource (连接池,管理java和数据库之间所有的连接)
2. 创建JdbcTemplate (封装了各种操作数据库的简化后的方法)
3. 执行SQL (调用jdbcTemplate的方法 update:执行增删改 query:执行查询)
需求:向t_person
表中新增一行数据
create table t_person(
person_id int primary key auto_increment,
person_name varchar(20),
age tinyint,
sex varchar(6),
mobile varchar(20),
address varchar(200)
)char set utf8mb4;
-- 需求 向t_person表中插入一行数据
insert into t_person values(null,'王月华',88,'其他','138383838','硅谷');
准备工作(搭建开发环境)
需要在项目中引入数据库驱动jar包和JdbcTemplate的jar包(jar文件:针对class文件的压缩格式包含了多个带包的class文件,类似于普通文件打包的zip、rar)
编码
public class JdbcTemplateTest {
public static void main(String[] args) {
//1 创建,并配置DataSource
String url = "jdbc:mysql://localhost:3306/baizhi?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "123456";
DataSource dataSource = new DriverManagerDataSource(url,username,password);
//2 建立JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//3 执行sql
String sql = "insert into t_person values(null,'王月华',18,'F','1563868xxxx','郑州')";
int update = jdbcTemplate.update(sql);
System.out.println("本次执行的sql插入"+update+"条数据");
}
}
需求:查询t_person表中的数据
select * from t_person;
使用JdbcTemplate对象的query方法执行查询sql时会返回ResultSet结果集对象,通过它我们可以获取到SQL语句的查询结果。要通过ResultSet获取到查询结果需要解决以下几个问题:
如何获取ResultSet?
String sql = "select * from t_person"; // query方法中,传入ResultSetExtractor接口实现,其extractData方法将会得到ResultSet jdbcTemplate.query(sql, new ResultSetExtractor
如何获取一行中各个字段的值?
两种方式:
rs.getXxx(列编号) ,列编号从1开始
rs.getXxx("字段名")
注意:Xxx为数据类型,比如String、Int、Double...
如何获取ResultSet中的所有行?
rs内部有一个游标,每调用
rs.next()
一次,游标从上一行向后移动到当前行,并返回指向的当前行是否有数据,true:有数据,false:没有数据。
rs游标初始执行第一行的上方
rs不断调用next(),第一次调用游标指向第一行,第二次调用游标指向第二行,直到返回false,即可以遍历所有行。
开发步骤回顾
1. 创建DataSource 2. 创建JdbcTemplate 3. 执行SQL,处理结果集
while循环处理结果集
while(rs.next()){ //不断循环,遍历rs中所有行 rs.getXxx("字段名"); //获取一行中每个字段的值 }示例:
//3 执行sql String sql = "select * from t_person"; jdbcTemplate.query(sql, new ResultSetExtractor
封装结果,并返回
通过rs获取到多行数据后,为了程序后续流程方便使用这个数据,我们会将多行多列的数据保存起来,而不是简单的打印输出。通常,我们会根据表中字段内容,定义一个与之对应的实体类(DO)。比如,与t_person表对应的实体类如下所示:
public class Person implements Serializable { private Integer personId; private String personName; private Integer age; private String sex; private String mobile; private String address; //省略 构造方法和get set方法 ... }
实体类名和表名相关(表名去掉
t_
后,单词首字母大写)一个表对应一个实体类,多个表多个实体类,按照规范实体类所在的包名通常为
entity
、domain
、pojo
中的一种实体类中属性和表中字段一一对应,属性私有提供公开的get和set
实体类必须包含无参构造方法
实体类应该实现Serializable接口
返回结果:
通过rs查询出的每行数据应该封装为一个实体对象
如果有多行,则可以使用List保存多行转换的实体对象
ListpersonList = jdbcTemplate.query(sql, new ResultSetExtractor >() { @Override //extractData会在查询SQL的结果返回后,被自动调用1次。ResultSet表示查询结果集 public List
extractData(ResultSet rs) throws SQLException, DataAccessException { List personList = new ArrayList<>(); while (rs.next()) {//从上一行移动到当前行 //获取当前行数据 int id = rs.getInt("person_id"); String name = rs.getString("person_name"); int age = rs.getInt("age"); String sex = rs.getString("sex"); String mobile = rs.getString("mobile"); String address = rs.getString("address"); Person person = new Person(id, name, age, sex, mobile, address); personList.add(person); } return personList; } }); personList.forEach((p)->{ System.out.println(p); //遍历打印输出 });
通过上面的学习我们会发现,不同表的查询结果集处理步骤除了从当前行获取数据的处理不同外,其它步骤完全相同。都是:
List list = new ArrayList();//2 都需要定义一个List集合
while(rs.next()){ //1 都需要循环
... //处理当前行,一行封装成一个实体对象
list.add(o);//3 将实体对象保存到list中
}
return list;//4 将封装了多行结果的list返回
为了避免频繁编写上述while循环
模板代码,JdbcTemplate中还可以使用RowMapper接口代替ResultSetExtractor接口,简化结果集的处理。
//3 执行sql
String sql = "select * from t_person";
List personList = jdbcTemplate.query(sql, new RowMapper() {
@Override
//mapRow用来获取一行的多列值,mapRow会被自动的循环调用
public Person mapRow(ResultSet rs, int rowNum) throws SQLException {
//获取当前行数据
int id = rs.getInt("person_id");
String name = rs.getString("person_name");
int age = rs.getInt("age");
String sex = rs.getString("sex");
String mobile = rs.getString("mobile");
String address = rs.getString("address");
Person person = new Person(id, name, age, sex, mobile, address);
return person;
}
});
JdbcTemplate还提供了内置的BeanPropertyRowMapper,简化查询结果和实体类对象的转换。注意,BPRM的使用需要满足以下要求:
字段名和属性名由1个单词构成时,2者要相同。
t_person表中有address字段,则Person实体类中有address属性
字段名和属性名由多个单词构成时,字段名多个单词间使用_连接,属性名第一个单词小写其它单词首字母必须大写
t_person表中有person_id字段,则Person实体类中有personId属性
//1. 创建DataSource
// url 起到定位数据库的作用,格式: jdbc:mysql://数据库所在机器的ip:port/数据库名?k=v参数&k=v参数
/*
jdbc:mysql:// 固定写法
数据库所在的ip,如果是自己的机器,可以直接使用127.0.0.1 或者localhost
port MySQL监听3306
数据库名 baizhi
k=v 用来配置连接的 useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
*/
String url ="jdbc:mysql://localhost:3306/baizhi?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai";
String username = "root"; //数据库用户名
String password = "123456"; //数据库密码
DataSource dataSource = new DriverManagerDataSource(url, username, password);
//2. 创建JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//3. 执行sql
String sql = "select * from t_person";
List personList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Person.class));
personList.forEach(p->{
System.out.println("p = " + p);
});
数据绑定:将用户输入的数据,绑定到要执行的SQL语句中。
JDBC执行的SQL中的数据要根据用户的输入发生变化,比如 登录功能背后的查询sql要根据用户名不同,执行不同的条件。这就需要将用户输入的数据绑定到执行的SQL中。
绑定数据的方式有2种:
字符串拼接
?占位符绑定
环境准备:
create table t_admin(
admin_id int primary key auto_increment,
admin_name varchar(20) unique not null,
password varchar(50) not null
);
-- 添加测试数据
insert into t_admin values(null,'xiaohei','123456');
字符串拼接方式,本质上就是通过Java字符串拼接语法构造出正确的可执行的SQL语句。拼接步骤如下:
在需要数据的地方,使用变量名替换
在变量名前添加"+
,变量后添加+"
注意:如果拼接的是字符串值,那么需要在"+
之前添加'
在+"
后添加'
//1 创建DataSource
String url = "jdbc:mysql://localhost:3306/baizhi?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "123456";
DataSource dataSource = new DriverManagerDataSource(url,username,password);
//2 创建JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//3 执行sql,处理结果集
Scanner sc = new Scanner(System.in);
String adminName = sc.nextLine();
String adminPassword = sc.nextLine();
/*
① 在需要绑定数据的地方,使用变量名替换
② 在变量名前追加( "+ ) 在变量名后追加( +" )
③ 注意:如果拼接的数据是字符串,那么需要在"+前和+"后各添加一个'
*/
String sql = "select * from t_admin where admin_name = '"+adminName+"' and password = '"+adminPassword+"'";
System.out.println("sql = " + sql);
List adminList = jdbcTemplate.query(sql, new RowMapper() {
@Override
public Admin mapRow(ResultSet rs, int rowNum) throws SQLException {
int id = rs.getInt("admin_id");
String name = rs.getString("admin_name");
String password = rs.getString("password");
return new Admin(id, name, password);
}
});
if(!adminList.isEmpty()){
System.out.println("登录成功,admin = "+adminList.get(0));
}else {
System.out.println("登录失败!");
}
?占位符是JDBC的一种特殊语法,专用于参数绑定。使用步骤:
在需要使用数据的地方,使用?代替(占位)
String sql = "select * from t_admin where admin_name = ? and password = ?";
update和query都有Object[]参数的版本和可变长参数的版本,用来接收数据按照顺序为?赋值
update(String sql, @Nullable Object... args); query(String sql, @Nullable Object[] args,RowMapperrowMapper); query(String sql, RowMapper rowMapper, @Nullable Object... args);
//1 创建DataSource
String url = "jdbc:mysql://localhost:3306/baizhi?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "123456";
DataSource dataSource = new DriverManagerDataSource(url,username,password);
//2 创建JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//3 执行sql,处理结果集
Scanner sc = new Scanner(System.in);
String adminName = sc.nextLine();
String adminPassword = sc.nextLine();
/*
① 在需要绑定数据的地方,使用变量名替换
② 在变量名前追加( "+ ) 在变量名后追加( +" )
③ 注意:如果拼接的数据是字符串,那么需要在"+前和+"后各添加一个'
*/
String sql = "select * from t_admin where admin_name = ? and password = ?";
System.out.println("sql = " + sql);
Admin admin = jdbcTemplate.queryForObject(sql,new Object[]{adminName,adminPassword}, new RowMapper() {
@Override
public Admin mapRow(ResultSet rs, int rowNum) throws SQLException {
int id = rs.getInt("admin_id");
String name = rs.getString("admin_name");
String password = rs.getString("password");
return new Admin(id, name, password);
}
});
if(admin != null){
System.out.println("登录成功,admin = "+admin);
}else {
System.out.println("登录失败!");
}
方式 | 特点 | 使用场景 | 最佳实践 |
---|---|---|---|
字符串拼接 | 可能会被SQL注入攻击 | 可以拼接表名、列名、SQL关键字 | 动态的查询不同的表以及根据用户选择升序或者降序查询数据 |
?占位符 | 可以防止SQL注入攻击 | 绑定纯数值数据 | 通常情况下使用占位符 |
4.3.1 SQL注入攻击
SQL注入是一种非常常见的数据库攻击手段,技术上就是用户使用包含SQL关键字的数据参与了SQL拼接,拼接出一个绕过验证的恶意的SQL。然后这样被拼接出来的SQL语句被数据库执行,产生了开发者预期之外的动作。
String adminName = "xiaohei'-- ";//真实用户名后添加'-- ,后面的内容会被注释掉 String adminPassword = "1234567";//密码错误,但也能登录成功
4.3.2 拼接关键字
为了更灵活的SQL效果,有时候我们需要拼接表名、字段名和关键字。比如动态的查询不同的表、根据不同的字段排序等。字符串拼接的方式可以,而?占位符则不行。
解决问题的步骤:
定位问题(哪一行出错)
① 添加打印语句
② 根据异常信息定位(找到异常信息中关于自己写的代码部分)
为什么出错
根据异常信息和所学知识分析问题的原因
解决异常
根据问题的原因,解决异常记录异常
分析异常最高效的办法是阅读异常日志,根据异常日志的信息,可以快速定位问题的位置、问题的原因设置是问题的解决方案。举个例子:
org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLException: Access denied for user 'root'@'172.17.0.1' (using password: YES) at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:82) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:612) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:669) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:700) at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:712) at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:783) at com.baizhi.test.JdbcTemplateTest.testDataBindingPreparedStatement(JdbcTemplateTest.java:217) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55) Caused by: java.sql.SQLException: Access denied for user 'root'@'172.17.0.1' (using password: YES)
先看异常类型:
java.sql.SQLException: Access denied for user 'root'@'172.17.0.1' (using password: YES),说明是数据库账密不正确
再看异常位置:
自上而下看出现数字的行,找到自己写的代码。比如当前案例下:自己的代码 JdbcTemplateTest.java第217行有问题,那么错误就在这里发生的
解决方案:
修改为正确的用户名和密码
说明:阅读异常日志挑错是最有效的,但是对于初学者而言难度过高。刚开始不用急于求成,异常信息也非常有规范,在学习过程中慢慢提升异常阅读能力,最终掌握阅读异常信息的能力。
笨但有效的办法:手动添加打印语句
可以在代码的关键位置处添加打印语句,通过语句的输出与否判定异常发生的问题。 打印输出尽量有意义,比如关键代码有返回值,则可以打印返回值。这样通过返回值可以判断程序运行的状态,为调错提供指导。
更为标准的做法:开启日志
各种框架和类库为了更好的反馈程序执行的过程,都会在代码中预埋日志的输出(类似我们的打印语句,但功能更强大),我们可以开启JdbcTemplate中的日志输出。
JDBC的异常其实还是非常固定的,就那么几种:驱动加载问题、数据库用户名密码错误、sql语法错误、绑定数据错误...。有1个办法可以快速的掌握JDBC中的异常:站在上帝视角,人为的造错,观察记录异常信息,日后出现时可以快速调错。
1.搭建项目开发环境
a.打开idea,新建一个项目
b.导入依赖iar包项目日录下新建一个lib目录,然后将jar包复制其中右键选中lib目录,add as library(添加为库)
c.复制配置文件jdbc.properties + JDBCUtils工具类Cs
2.数据库中建表
3.实体类entity包
4.dao
dao包 接口:定义常见的增删改查方法
dao.impl包实现类:使用jdbcTemplate3步实现
5.service
service包 接口:针对一张表的业务功能方法
service.impl包实现类:业务逻辑判断+dao调用+事务控制
6.test
JUnit测试 一个service一个测试类,注意:Junit在idea中默认不能使用扫描器