Java Database Connectivity (Java语言连接数据库)
JDBC是Sun公司指定的一套接口(Interface)
目的是解耦合,降低程序的耦合度,提高程序的扩展力,多态机制就是非常典型的,面向抽象编程
为什么Sun要指定一套JAVA SQL的接口?
因为每个数据库都存在差异,实现原理不同,Oracle有自己的原理,MySQL有自己的原理,通过这套实现需要厂家进行实现,开发者无需关系底层逻辑
假设现在有个需求,要将Oracle换成MySQL数据库,此时只有接口能做到切换时只需要更换数据库驱动即可无缝切换,代码层完全不用修改(或小改,可能有些语法是特有的)
所谓的数据库驱动就是Jar包,里面就是一堆JDBC的实现类,使用maven时,只需要在依赖项里添加对应依赖即可,或者手动添加jar包
开始实操
既然是操作数据库,就需要有数据库的支持,这里采用Docker安装Mysql并进行数据插入
- 安装启动MySQL:docker run –name testDbContainer -v ~/testDb:/root -e MYSQL_ROOT_PASSWORD=12345678 -e MYSQL_DATABASE=testDb -p 3306:3306 -d mysql:8.0.29-oracle
- 插入数据
drop table if exists t_user;
create table t_user (
id int primary key auto_increment,
username varchar(10) unique,
age int(3),
gender int(1) comment '1表示男,0表示女'
);
insert into t_user (username, age, gender) values('Jack', 15, 1),('Mike', 20, 1),('Susan', 22, 0);
Java配置
使用JDBC进行编程,需要先注册驱动,告诉Java程序要连接什么数据库,Oracle还是MySQL,还是H2等等,需要用到java.sql包下的DriverManager类,有一个静态方法registerDriver
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
registerDriver(driver, null);
}
方法要求传入的是接口(面向接口编程),此时就需要去找数据库厂商提供的驱动类,他们会负责Driver接口的实现类
- 使用maven管理的项目比较简单,只需要引入依赖即可
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
- 如果想手动添加jar包依赖,到mysql的官网下载页dev.mysql.com/downloads/c…
这里要选择与平台无关的选项,下载的压缩包解压后里面就有对应的jar包,自行导入即可
这里有一个注意事项,新版的驱动包名已变更,如果用的还是com.mysql.jdbc包中的Driver类,在加载类时就会输出此类不再推荐使用,正确的包名是com.mysql.cj.jdbc.Driver
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
}
static {
System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
}
}
再看看真正的使用的Driver类
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
在静态代码块中,有一行DriverManager.registerDriver(new Driver());,原本应该开发者去注册的事,加载类的时候就已经做了,所以只需要让JVM去加载类即可实现注册 最简单的便是Class.forName(“”)方法
public class App {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
}
}
第二步:获取连接
表示JVM进程和数据库之间的通道打开,使用完需要关闭通道释放资源,主要方法为DriverManager.getConnection方法,入参为数据库URL,账号,密码,返回值是Connection对象,不同数据库的URL不一样,用到什么搜什么即可
public class App {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/testDb";
String username = "root";
String password = "12345678";
Connection connection = DriverManager.getConnection(url, username, password);
}
}
第三-第四步:获取数据库操作对象,执行SQL
这个对象主要用于执行SQL语句,Connection.createStatement/prepareStatement调用这两个方法,这里先演示createStatement,后面会说到区别
第五步:获取结果集
如果执行的是DQL语句,那么将会返回一个ResultSet对象,表示数据表的映射,如果是DML语句,返回的则是int,表示sql语句所影响的行数。
讲讲ResultSet这个对象的next方法,执行DQL语句调用executeQuery方法,假设执行了某张表的select * 语句,想要获取结果集的数据需要调用ResultSet.next()方法,光标会向下移动一行,如果有数据则返回true,此时操作ResultSet对象就可以获取对应的行数据,再次调用next又会将光标向下移动一行,直到没有数据了返回false
ResultSet.getXXX方法可以获取对应的列,使用方式有
- 索引,如果返回字段顺序为id,name,age,1则代表id,以此类推(JDBC中的所有操作索引从1开始),不推荐,语义不清晰,没有办法通过代码直观到看出预期
- 键名,直接使用字段名作为参数,如果select写了字段的别名,则需要用别名去取值
- 提供get数据类型的方法,比如字段id类型为int,则可以用getInt获取id,如果使用getString,不管数据类型是什么都会按照String的形式取出
public class App { public static void main(String[] args) throws SQLException, ClassNotFoundException { Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/testDb"; String username = "root"; String password = "12345678"; Connection connection = DriverManager.getConnection(url, username, password); Statement statement = connection.createStatement(); String sql = "select * from t_user;"; ResultSet resultSet = statement.executeQuery(sql); // 通过while循环获取全部结果集,返回false则跳出循环 while (resultSet.next()) { System.out.println(resultSet.getString("username")); } }
示例通过列名去除了所有结果集的用户名
第六步:释放资源
当所有操作结束后,需要将ResultSet(如果有),Statement,Connection的close方法从左到右执行,释放掉资源避免不必要的浪费
public class App {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/testDb";
String username = "root";
String password = "12345678";
Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
String sql = "select * from t_user;";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
System.out.println(resultSet.getString("username"));
}
resultSet.close();
statement.close();
connection.close();
}
从JDK7开始,便支持了try with resources,只要资源实现了AutoCloseable接口,作用是在块代码执行后,自动释放资源,也就是自动执行close方法。
查看类源码可以看到,ResultSet,Statement,Connection三个类都实现了AutoCloseable接口,所以可以改写让代码更整洁,多个资源用分号进行隔开
public class App {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/testDb";
String username = "root";
String password = "12345678";
String sql = "select * from t_user;";
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
) {
while (resultSet.next()) {
System.out.println("用户名:" + resultSet.getString("username"));
}
}
}
}
可以不使用,但可以作为一个知识点了解
SQL注入问题
假设这是一个提供给外部的接口服务,会接受用户的传参数据,由于creaetStatement方法是先进行sql语句都拼接,再进行sql的编译,所以可以会将用户输入的恶意关键字一同编译,像下面代码中用户传了字符串’ or ‘1’ = ‘1,仿佛猜透了服务端对sql拼接规则,由于加了必定成功的条件,所以即使传的参数不对也会执行成功
// 其他代码省略
Statment st = connection.createStatment();
// 用户传入的值
String value = "' or '1' = '1";
String sql = "select * from t_user where username = '" + value +"'";
ResultSet rs = st.executeQuery(sql);
if(rs.next()){
System.out.println(rs.getString("username");
// 这里会输出
}
这种欺骗了服务器令其执行了非预期的查询的行为,便称为SQL注入
要解决有两种方案
- 从参数解决,对用户的输入进行特定规则的校验
- 让用户的传参不参与SQL语句的编译
更应该使用第二种解决方案
PreparedStatement对象就可以完美处理注入问题,在使用时需要先将SQL语句进行编译,需要用户传参的地方用占位符替代,编译后再将值传到原本的占位符中
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/testDb";
String username = "root";
String password = "12345678";
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "select * from t_user where username = ?";
PreparedStatement ps = connection.prepareStatement(sql);
// 将原本会导致SQL注入的值通过索引传到占位符
ps.setString(1, "' or '1' = '1");
ResultSet resultSet = ps.executeQuery();
while (resultSet.next()) {
System.out.println("用户名:" + resultSet.getString("username"));
}
resultSet.close();
ps.close();
connection.close();
}
上面这段代码执行后不会有任何输出
PreparedStatement很好,但是有一种情况是无法实现的,比如要求order by字段实现动态的升降序,asc和desc不参与编译就没有办法使用,这时候只能用Statement来主观实现用户的SQL注入
对比总结两种操作对象
- Statement存在SQL注入问题,PreparedStatement解决了注入问题
- Statement是执行一次编译一次,而PreparedStatement是编译一次,执行多次,效率更高
- PreparedStatement拥有静态编译检查,比如占位符要求传入String,就不能传入其他数据类型,编译器会报错,安全性更高
- 能使用PreparedStatement尽量使用,只有当需要主观行为上的SQL注入时再使用Statement
JDBC中的事务
在JDBC中,事务行为默认自动提交,只要执行任意的DML语句,就会自动提交一次。这种行为肯定不符合实际业务开发, 一个业务中可能包含多条DML语句,必须满足同时成功或者同时失败
可以通过Connection对象的setAutoCommit(false)方法,传入false来关闭自动提交事务,在业务程序中,正常逻辑下使用Connection.commit()方法进行事务提交,用try catch代码块捕获异常后使用Connection.rollback()方法进行事务回滚
事务演示(DQL用executeQuery,DML使用executeUpdate,包含增删改)
// 其他代码省略
Connection connection = DriverManager.getConnection(url, username, password);
connection.setAutoCommit(false);
String sql = "delete from t_user where id = 3;";
PreparedStatement ps = connection.prepareStatement(sql);
int i = ps.executeUpdate();
if (i == 1) {
System.out.println("删除成功");
}
ps.close();
connection.close();
尝试删除id为3的用户,代码执行后,控制台会输出删除成功
但是去查看数据库并没有数据删除,说明JDBC的自动提交事务被成功关闭了
正确的处理场景,在所有预期事务执行完毕后进行事务提交,catch异常后进行事务的回滚
// 其他代码省略
Connection connection = DriverManager.getConnection(url, username, password);
connection.setAutoCommit(false);
PreparedStatement ps = null;
try {
String sql = "delete from t_user where id = 3;";
ps = connection.prepareStatement(sql);
int i = ps.executeUpdate();
if (i == 1) {
System.out.println("删除成功");
}
connection.commit();
} catch (Exception e) {
connection.rollback();
}
if (Objects.nonNull(ps)) {
ps.close();
}
connection.close();
在真实的开发场景中,并不会使用原生的JDBC去实现业务处理,主流的有MyBatis和JPA等ORM框架来帮助快速开发,但是底层都是基于JDBC的封装,只有熟悉JDBC才能更加清晰的掌握框架的使用
链接:https://juejin.cn/post/7099066449456529438
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/5325.html