JDBC详解
1、JDBC是什么?
JDBC是Java DataBase Connectivity,是Java访问数据库的标准规范,其本质上是SUN公司制定的一套接口,其位于JDK的java.sql包中。
JDBC接口的具体实现类由各大数据库厂商来实现,这些实现类叫做数据库驱动。实际运用时,需要根据项目中使用的DBMS来导入对应的驱动jar包。比如项目中使用MySQL数据库,则需要导入MySQL的驱动jar包。
1.1 使用JDBC的好处
面向接口编程,程序员只需要会调用JDBC 接口中的方法,无需关注类的实现。
1.2 为什么SUN公司制定一套JDBC接口呢?
因为每一个数据库的底层实现原理都不一样。
2、JDBC开发前的一些准备工作
2.1、使用文本编译器开发
先去数据库的官网下载对应的驱动jar包,然后将其配置到环境变量classpath当中。
例如,项目中使用的是MySQL数据库,则去官网下载mysql驱动,然后配置环境变量如下:
classpath=.;D:\course\06-JDBC\resources\MySql Connector Java 5.1.23\mysql-connector-java-5.1.23-bin.jar
2.2 使用IDEA开发
使用IDEA的时候,不需要配置以上的环境变量。
3、JDBC编程六步简介
第一步:注册驱动(作用:告诉Java程序,即将要连接的是哪个品牌的数据库)
第二步:获取连接(表示JVM的进程和数据库进程之间的通道打开了,这属于进程之间的通信,重量级的,使用完之后一定要关闭通道。)
第三步:获取数据库操作对象(专门执行sql语句的对象)
第四步:执行SQL语句(DQL DML….)
第五步:处理查询结果集(只有当第四步执行的是select语句的时候,才有这第五步处理查询结果集。)
第六步:释放资源(使用完资源之后一定要关闭资源。Java和数据库属于进程间的通信,开启之后一定要关闭。)
4、JDBC编程六步详解
4.1、注册驱动
4.1.1、第一种方式
Driver driver = new com.mysql.jdbc.Driver();//多态,父类型引用指向子类型对象(MySQL的驱动)
//Driver driver = new oracle.jdbc.driver.OracleDriver();//Oracle的驱动
DriverManager.registerDriver(driver);
//两行代码可以合并为:
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
其中,Driver是JDK中java.sql包下的一个JDBC接口;com.mysql.jdbc.Driver类(MySQL驱动)实现了Driver接口,可在MySQL官网中下载对应的jar包,并将其配置到IDEA模块中。
4.1.2、第二种方式(常用)
Class.forName("com.mysql.jdbc.Driver");//使用反射机制
那么为什么第二种方式更加常用呢?
我们可以从com.mysql.jdbc.Driver的源码分析,其源码如下:
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static { //静态代码块,类加载时完成初始化,而Class.forName("完整类名")刚好可以完成类加载动作
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
可以看出,注册驱动的动作在静态代码块中,因此只需要使用其类加载时机。另外Class.forName()方法的参数是一个字符串,该字符串可以写到properties属性配置文件中,同时我们不需要该方法的返回值。因此使用这种方式可以利用类加载动作完成静态代码块的初始化,从而实现注册驱动。
4.2、获取连接
获取连接需要URL,数据库用户名和密码。
conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/db","root","123456");
4.2.1、URL
URL是统一资源定位符(网络中某个资源的绝对路径)。例如: https://www.baidu.com/ 这就是一个URL。
URL包括四个部分:协议,IP,端口号,资源名
4.2.2、数据库用户名和密码
数据库的用户名和密码可以在相应的DBMS中进行设置。
4.3、获取数据库操作对象
数据库操作对象Statement是专门执行sql语句的对象。
可以通过数据库连接对象connection来获取数据库操作对象statement。
stmt = conn.createStatement();
4.4、执行SQL语句
4.4.1、执行DQL语句
String sql = "select empno as a,ename,sal from emp";
rs = stmt.executeQuery(sql);
注意:Statement的executeQuery(select语句)方法源码为:
ResultSet executeQuery(String sql) throws SQLException;
其中ResultSet是JDK中java.sql包下的一个JDBC接口。
4.4.2、执行DML语句
使用Statement的executeUpdate方法执行DML语句。
String sql = "insert into dept(deptno,dname,loc) values(50,'开发部','广州')";
int count = stmt.executeUpdate(sql);
注意:Statement的executeUpdate(insert/delete/update)方法为:
int executeUpdate(String sql) throws SQLException;
该方法的返回值是”影响数据库中的记录条数”
4.5、处理查询结果集
只有当第四步中执行的是DQL语句时,才需要处理查询结果集。
主要有四种方式:下标获取,列名获取,指定类型的下标获取,指定类型的列名获取
while(rs.next()){
//第一种,下标获取
/* String empno = rs.getString(1);//JDBC中所有下标从1开始,不是从0开始的。
String ename = rs.getString(2);
String sal = rs.getString(3);
System.out.println(empno + "," + ename + "," + sal);*/、
//第二种,这个不是以列的下标获取,以列的名字获取
/* String empno = rs.getString("a");
String ename = rs.getString("ename");//重点注意:列名称不是表中的列名称,是查询结果集的列名称
String sal = rs.getString("sal");
System.out.println(empno + "," + ename + "," + sal);*/
//第三种,除了可以以String类型取出之外,还可以以特定的类型取出
/* int empno = rs.getInt(1);
String ename = rs.getString(2);
double sal = rs.getDouble(3);
System.out.println(empno + "," + ename + "," + (sal + 100));*/
//第四种
int empno = rs.getInt("a");
String ename = rs.getString("ename");
double sal = rs.getDouble("sal");
System.out.println(empno + "," + ename + "," + (sal + 200));
}
4.6、释放资源
使用完资源之后一定要关闭资源。Java和数据库属于进程间的通信,开启之后一定要关闭。
为了保证资源一定释放,在finally语句块中关闭资源,并且要遵循从小到大一次关闭,分别对其try..catch。
4.6.1、执行DML语句时如何释放资源?
当执行的是DML语句时,先关闭Statement数据库操作对象,最后关闭Connection数据库连接对象。
finally {
if (stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4.6.2、执行DQL语句时如何释放资源?
当执行的是DQL语句时,先关闭ResultSet查询结果集对象,再关闭Statement数据库操作对象,最后关闭Connection数据库连接对象。
finally {
//6、释放资源
if (rs != null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
5、什么是SQL注入?
比如在一个网站的用户登录界面,输入如下用户名和密码:
用户名:feng
密码:feng' or '1'='1
如果此时会显示登录成功,则发生了SQL注入
5.1、SQL注入的根本原因
用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的原意被扭曲,进而达到sql注入。(可通过debug程序看出)
5.2、如何解决SQL注入
使用PreparedStatement可以解决SQL注入。
//3、获取预编译的数据库操作对象
//SQL语句的框子。其中?表示一个占位符,一个?来接收一个“值”
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
//程序执行到此处,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译。
ps = conn.prepareStatement(sql);
//给占位符?传值(第1个问号下标是1,第2个问号下标是2,JDBC中所有下标从1开始。)
ps.setString(1,loginName);
ps.setString(2,loginPwd);
5.3、PreparedStatement的原理
只要用户提供的信息不参与SQL语句的编译过程,SQL注入问题就可以解决。或者用户提供的信息中含有SQL语句的关键字,但是没有参与编译,则关键字不起作用,SQL注入也可以得到解决。 要想用户信息不参与SQL语句的编译,那么必须使用java.sql.PreparedStatement,PreparedStatement接口继承了java.sql.Statement,PreparedStatement是属于预编译的数据库操作对象。PreparedStatement的原理是预先对SQL语句的框架进行编译,然后再给SQL语句传“值”。
5.4、Statement和PreparedStatement的区别
1、Statement存在sql注入问题,PreparedStatement解决了sql注入问题。
2、Statement是编译一次执行一次。PreparedStatement是编译一次,可执行N次,PreparedStatement效率较高一些。
3、PreparedStatement会在编译阶段做类型的安全检查。
综上所述,PreparedStatement使用较多,只有极少数的情况下需要使用Statement。如果业务方面要求支持SQL注入,需要进行SQL语句拼接,则使用Statement。
6、JDBC的事务
默认是自动提交。自动提交:只要执行任意一条DML语句,则自动提交一次。
但是在实际的业务当中,通常都是N条DML语句共同联合才能完成的,必须保证他们这些DML语句在同一个事务中同时成功或者同时失败。此时需要将JDBC的事务修改为手动提交。
6.1、将JDBC的事务修改为手动提交
使用conn.setAutoCommit(false)可以修改为手动提交。
例如:
public class JDBCTest11 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","root","123456");
//将自动提交机制修改为手动提交
conn.setAutoCommit(false);//开启事务
//3、获取预编译的数据库操作对象
String sql = "update t_act set balance = ? where actno = ?";
ps = conn.prepareStatement(sql);
//给?传值
ps.setDouble(1,10000);
ps.setInt(2,111);
int count = ps.executeUpdate();//执行第一条UPDATE语句
System.out.println(count);
String s = null;
s.toString();
//给?传值
ps.setDouble(1,10000);
ps.setInt(2,222);
count += ps.executeUpdate();
System.out.println(count == 2 ? "转账成功" : "转账失败");
//程序能够走到这里说明以上程序没有异常,事务结束,手动提交数据
conn.commit();//提交事务
} catch (Exception e) {
//回滚事务
if (conn != null){
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
}finally{
//6、释放资源
if (ps != null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
7、模糊查询
//正确的写法(模糊查询)
String sql = "select ename from emp where ename like ?";
ps = conn.prepareStatement(sql);
ps.setString(1,"_A%");
rs = ps.executeQuery();
while(rs.next()){
System.out.println(rs.getString("ename"));
}
8、悲观锁和乐观锁
8.1、悲观锁(行级锁)
事务必须排队执行。数据锁住了,不允许并发。
使用方式:select后面添加 for update
8.2、乐观锁
支持并发,事务也不需要排队,只不过需要一个版本号。