JDBC:Java Database Connectivity,java数据库连接,其实就是用java语言来操作数据库。
JDBC的本质:其实就是官方(sun公司)定义的一套用来操作所有关系型数据库的规则,即接口。然后各个厂商去实现这套接口,提供数据库驱动jar包。我们可以使用这套接口(JDBC)来编程,而真正执行的是驱动jar包中的实现类,即多态的方式。
JDBC的使用步骤
- 导入驱动jar包:mysql-connector-java-5.1.37-bin.jar
- 注册驱动
- 获取数据库连接对象Connection
- 定义sql
- 获取执行sql语句的对象Statement
- 执行sql语句,接收返回结果
- 处理结果
- 释放资源
DriverManager类
注册驱动
java.sql.DriverManager类,管理JDBC的驱动。 通常用来【注册驱动】和【获取数据库连接】。
注册驱动的方法,这是一个静态方法:
通常,我们并不需要使用这个方法来注册驱动,而是通过下方的代码,加载一个类文件:
加载的文件是导入的jar包中定义的一个类,该类中定义了一个静态代码块:
也就是说,jar包中已经有了相关的文件帮我们进行了注册,我们只需加载一下这个类文件就可以了。
获取数据库连接
获取数据库连接的方法getConnection,也是一个静态方法,参数传递指定连接的路径(ip+端口+指定数据库),用户名及密码。返回的是一个Connection对象。
注意:这里传递的连接路径,不同的数据库写法有所差异,mysql的写法为:
如果连接的是本机的mysql服务器,并且mysql服务器默认服务端口是3306,那么可以将IP地址和端口号省略不写。写成jdbc:mysql:///数据库名称
Connection接口
java.sql.Connection接口,用来建立与特定数据库的连接。我的理解:连接数据库和执行sql语句之间的一个桥梁。
获取执行sql语句的对象:
- createStatement方法
- prepareStatement方法
管理事务的方法
- 开启事务的方法,setAutoCommit,参数传递false即为开启事务
- 提交事务,commit
- 回滚事务,rollback
Statement接口
java.sql.Statement接口,用于执行静态sql语句。
执行sql语句的方法
- execute方法,可以执行任意的sql语句,此方法并不常用,了解即可。
- executeUpdate,多用于执行数据的增删改语句,返回一个int值,该值为sql语句影响的行数。我们可以通过这个行数来判断sql语句是否执行成功,如果行数>0,表示执行成功,否则失败。如果传递DDL语句,则会返回一个0。
- executeQuery方法,通常会传递sql查询语句,返回的是个结果集对象。
ResultSet接口
java.sql.ResultSet接口,封装查询结果。
next方法,光标向下移动一行(光标初始位置在表头行,注意查询返回的是一个满足条件的新表格)。如果该方法返回false,则说明已经不指向数据行,也就是此时位于数据行之后了。可以用于判断是否还有数据。
getXxx(参数)方法,用来获取特定类型特定位置的数据,后面的Xxx就是你想获取的数据类型,并且返回获取的数据,比如:int getInt()、String getString()等。此方法的参数有两种传递方式:
- 传递int值,表示想要获取第几列的数据,比如你想获取的某String类型值在第3列,则方法为:getString(3),注意,列数从1开始。
- 传递String值,直接传递要获取的数据所在列的字段名,比如getInt("age")。
结果集的遍历(此处以遍历各个员工姓名、性别和工资为例):
代码实现(行数不确定,用while循环):
注意:结果集只能一行一行地循环,不能一格一格地循环。而且要知道有哪些字段,以及这些字段分别是什么类型。还可以指定获取哪些字段数据。
表和实体类的映射
对比一下表和类就可以发现,可以将整个表映射成一个类,表名对应类名,表字段对应类的成员变量。
将上面的学生表映射成一个类:
然后可以定义一个方法,遍历数据库表为这些成员变量赋值:这样就可以将每行数据都封装成一个对象。再将这些对象存到集合中:
public static List<Student> findAll(){
//定义一个List集合,用于存放Student对象
List<Student> list = new ArrayList<>();
//为了释放资源,需要提升几个对象的作用域
Connection conn = null;
Statement stmt = null;
ResultSet rs =null;
//开始遍历数据库,将字段值赋给对应的成员变量
try {
//注册驱动
Class.forName("com.mysql.jdbc.Driver");
//获取数据库连接对象
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1","root","ROOT");
//获取执行sql语句的对象
stmt = conn.createStatement();
//定义sql语句
String sql = "select * from student";
//开始执行sql语句,返回一个结果集
rs = stmt.executeQuery(sql);
//声明一个学生对象,因为局部变量必须要赋值,所以赋初始值为null
Student s = null;
//遍历结果集
while(rs.next()){
//开始依次获取每一行数据
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
String sex = rs.getString("sex");
String address = rs.getString("address");
int math = rs.getInt("math");
int english = rs.getInt("english");
//用带参构造函数创建对象,传递获取的值
s = new Student(id,name,age,sex,address,math,english);
//将对象存进list集合
list.add(s);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}finally {
//如果对象创建成功,则需要释放资源
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();
}
}
}
return list;
}
然后调用该方法,返回的就是一个存放每行数据的对象集合,再想使用这些数据,就很方便了。
JDBC工具类
上面写的代码重复度很高,我们可以抽取出一个工具类,来简化代码。java并不直接提供此工具类,需要自己根据实际情况来编写。
工具类中的方法一般定义成静态的,方便通过类名直接调用。
通常需要抽取这几个部分,省得每次都写重复代码:
- 注册驱动的部分
- 获取连接对象的部分
- 释放资源的部分
先定义比较长但是比较简单的资源释放部分,要注意的是,当用executeUpdate执行语句时不会产生结果集对象,而用executeQuery执行时会多一个结果集对象,所以释放资源的方法需要定义两种重载形式:
//当用executeUpdate执行sql时,只需要释放两个对象资源
public static void close(Statement stmt, Connection conn){
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//当用executeQuery执行sql时,需要释放三个对象资源
public static void close(ResultSet rs,Statement stmt, Connection conn){
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();
}
}
}
获取连接对象部分的抽取,同样需要定义一个方法,里面写获取连接对象的语句,并返回一个Connection对象。这个方法中需要数据库路径、账号和密码这三个参数。
按理说,肯定不能将这三个值写死,那么就需要给这个方法传参,那调用这个方法时根本就没有得到简化(毕竟原来也只有一条语句),既不能写死,又不能传参,怎么办呢?用读取配置文件的方式来得到这三个值:
因为配置文件只用加载一次,所以将加载配置文件的方法定义在静态代码块中,让他随着类的加载而加载:
注意
- 因为异常抛出要依赖于方法,所以静态代码块中的异常无法抛出,只能try…catch来处理
- 配置文件中要定义路径、用户以及密码的信息,
另外,我们可以将驱动注册也写在配置文件中,然后在静态代码块进行注册。
综合代码如下:
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
private static String url;
private static String user;
private static String password;
private static String driver;
static{
//获取当前类的类加载器
ClassLoader classLoader = JDBCUtils.class.getClassLoader();
//通过加载器找到目标文件
URL resource = classLoader.getResource("jdbc.Properties");
//将url对象转化成路径
String path = resource.getPath();
System.out.println(path);
//通过Properties集合来加载文件
Properties ps = new Properties();
try {
ps.load(new FileReader(path));
} catch (IOException e) {
e.printStackTrace();
}
url = ps.getProperty("url");
user = ps.getProperty("user");
password = ps.getProperty("password");
//同时进行驱动的注册
driver = ps.getProperty("driver");
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//获取连接对象的方法
public static Connection getConnection(){
Connection conn = null;
try {
conn = DriverManager.getConnection(url,user,password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
//当用executeUpdate执行sql时,只需要释放两个对象资源
public static void close(Statement stmt, Connection conn){
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//当用executeQuery执行sql时,需要释放三个对象资源
public static void close(ResultSet rs,Statement stmt, Connection conn){
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();
}
}
}
}
PreparedStatement接口
先看一个登陆案例
public class LoginTest {
public static void main(String[] args) {
//输入用户名和密码
Scanner sc = new Scanner(System.in);
System.out.println("请输入用户名:");
String user = sc.next();
System.out.println("请输入密码:");
String password = sc.next();
//用户登录,并提示登录结果
boolean flag = login(user, password);
if(flag){
System.out.println("登录成功!");
}else{
System.out.println("用户名或密码错误!");
}
}
public static boolean login(String user,String password){
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
boolean result = false;
try {
//注册驱动,并获取数据库连接
conn = JDBCUtils.getConnection();
//创建sql执行对象
stmt = conn.createStatement();
//定义sql语句
String sql = "select * from login where user = '"+user+"' and password = '"+password+"'";
//执行sql语句
rs = stmt.executeQuery(sql);
//返回查询结果,如果存在该用户名和密码,那么就是对哒
result = rs.next();
} catch (SQLException e) {
e.printStackTrace();
}finally{
JDBCUtils.close(rs,stmt,conn);
}
return result;
}
}
以上代码存在一定的问题,当用户名随便输入,密码输入a’ or 'a' = 'a时,会导致sql语句为以下格式,导致能够登陆成功。
像这样的问题被称为【sql注入问题】,即在拼接sql语句时,有一些sql特殊的关键字参与了字符的拼接,会造成安全性问题。
要想解决sql注入问题就要使用java.sql.PreparedStatement接口,他是Statement接口的一个子接口,表示预编译的 SQL 语句的对象。
预编译的SQL:定义的sql语句中,参数使用?作为占位符。
如果使用PreparedStatement接口来处理sql语句,那么使用数据库的步骤就有一些变动:
- 1、通过Connection接口中的prepareStatement方法来获取一个PreparedStatement对象,并且该方法需要传入预编译的sql语句,所以sql语句要在这之前就定义好。
- 2、获取PreparedStatement对象之后,需要用该类中的setXxx方法来为?赋值,Xxx为相应的数据类型,并且该方法有两个参数:
- 第一个参数表示第几个问号,编号从1开始;
- 第二个参数表示要赋的值
- 3、开始执行sql语句,PreparedStatement接口中同样有executeQuery和executeUpdate两个方法,因为已经传递过sql语句了,所以这两个方法没有参数。
使用示例:
注意:后期都会使用PreparedStatement接口来完成增删改查的所有操作,可以有效防止sql注入,提高安全性。
JDBC事务管理
主要使用Connection中的三个方法:开启事务setAutoCommit,提交事务commit,回滚rollback
有几个问题需要注意:
- 在事务开始之前开启事务
- 在所有sql执行完提交事务
- 在多条sql之间出现异常则要回滚,注意几个问题:
- 回滚语句要放在出现异常后跳转到的地方,即catch里面
- 最好给这个回滚语句加个判断条件,如果Connection不为空,再进行回滚,否则会出现空指针异常。
- 因为无论出现什么异常都需要进行回滚,所以catch最好捕获大的异常Exception。
示例如下:
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt1 = null;
PreparedStatement pstmt2 = null;
//创建sql执行对象
try {
//注册驱动,并获取数据库连接
conn = JDBCUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
//定义转账的sql语句
String sqlone = "update account set balance = balance - ? where id = ?";
String sqltwo = "update account set balance = balance + ? where id = ?";
//获取sql执行对象
pstmt1 = conn.prepareStatement(sqlone);
pstmt2 = conn.prepareStatement(sqltwo);
//给sql语句中的?赋值
pstmt1.setDouble(1,500);
pstmt1.setInt(2,3);
pstmt2.setDouble(1,500);
pstmt2.setInt(2,4);
//执行sql语句
pstmt1.executeUpdate();
pstmt1.executeUpdate();
//提交事务
conn.commit();
} catch (Exception e) {
//回滚事务
if(conn != null){
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
e.printStackTrace();
}
}
数据库连接池
为了解决使用数据库时频繁地连接数据库——释放连接带来的系统损耗,实现数据库连接的重复利用,就引入了数据库连接池。
数据库连接池就是一个存放数据库连接的容器,当系统初始化后,容器会被创建好,容器中会申请一些连接对象,当用户访问数据库时,从容器中获取连接对象,用完之后再归还给容器。
这样就大大节约了资源,提高了访问数据库的效率。
如何实现:
在java.sql.DataSource接口下,有一个getConnection方法用来获取数据库连接
如果连接是从连接池里获取的,那么调用Connection中的close方法,则不会再关闭连接了,而是归还连接。
一般我们不去实现该接口,由数据库厂商来实现
1、C3P0:一种比较古老的数据库连接池实现技术
2、Druid:由阿里巴巴提供的一种数据库连接池实现技术。
C3P0连接池
使用步骤:
1、导入连接池jar包(两个):c3p0-0.9.5.2.jar和mchange-commons-java-0.2.12.jar,另外别忘了要导入数据库的驱动jar包。
2、定义配置文件
- 名称:c3p0.properties或者c3p0-config.xml,必须是这两个名字,否则找不到。
- 路径:放在工程的src包下即可。
3、(代码开始)创建核心对象,使用DataSource的实现类ComboPooledDataSource
4、获取连接:getConnection
关于c3p0的配置文件
在new ComboPooledDataSource时如果是空参就调用默认配置,其实在这个配置文件里还可以定义更多的配置信息:
在new ComboPooledDataSource时传入指定配置的名称就能使用了。通常使用默认的即可。
Druid连接池
使用步骤:
1、导入jar包,druid-1.0.9.jar,另外别忘了导入数据库驱动jar包
2、定义配置文件:文件为properties类型的,可以取任意名称,放在任意目录下,所以需要自己手动加载
3、利用Properties集合去手动加载配置文件
4、获取连接池对象:通过工厂来获取 DruidDataSourceFactory.createDataSource(),该方法有一个参数,需要传入一个Properties对象。
5、获取连接对象
使用示例:
Druid工具类
定义一个工具类,里面提供如下功能:
提供静态代码块加载配置文件,初始化连接池对象
提供方法:
- 获取连接的方法:通过数据库连接池获取连接
- 释放资源的方法
- 获取连接池的方法
代码如下:
注:由于截图原因,释放资源和获取连接池的getDs方法略。
JdbcTemplate
Spring JDBC,Spring框架对JDBC的简单封装,提供了一个JDBCTemplate对象简化JDBC的开发。
使用步骤:
- 1、导入jar包
- 2、创建JdbcTemplate对象,依赖于连接池对象DataSource
- 3、调用JdbcTemplate中的方法来完成数据库的CRUD操作:
update方法
update是执行增、删、改语句的方法,参数可以传递一个增删改的sql语句,如果有预编译的sql语句,则按顺序依次传入每个问号的值(不用写编号),比如:
query型方法
queryForMap()方法,查询结果并将结果集封装成Map集合,注意,此方法查询的结果集只能是一行数据,然后将字段名和对应值映射成键值对,封装到Map集合中。
queryForList()方法,查询结果并将结果集封装成List集合,该方法将每行数据封装成Map集合,然后再将多个Map集合存入List集合:
query()方法,查询结果并将结果集封装成javabean对象。之前将数据表映射成类时,进行了一系列的获取值和赋值的过程:
query方法可以将这一过程进行简化,这个方法可以传递两个参数:第一个参数传递sql查询语句,第二个参数可以将查询出的结果通过RomMapper接口封装成一个LIst集合,这个接口我么可以自己实现(一大堆获取值和赋值的过程),也可以用已经有的实现类BeanPropertyRowMapper来将结果集封装成List集合,泛型使用映射的类,参数传递映射类的字节码文件:
注意:如果在映射类中将数据类型定义成基本类型,一旦数据库中出现null,将会出现无法转换的异常,null值只能用引用类型来接收,所以最好将映射类的数据类型定义成包装类。
queryForObject方法,可以将查询到的一行数据封装成一个对象。
如果是静态sql,需要传入两个参数,分别是sql语句和new BeanPropertyRowMapper<映射类>(映射类.class)。
如果是动态sql,在后面紧接着传递实际参数即可。
该方法还可以用来执行聚合函数,参数传递sql语句,以及查询的聚合函数类型的.class文件。返回一个包装类对象。还可以紧接着传递参数。
注意,这些方法中的参数都是可变参数,可以传递数组。