1)JDBC是一个独立于特定数据库管理系统,通用的SQL数据库存取和操作的公共接口(一组API),定义用来访问数据库的标准Java库,(java.sql,javax.sql)使用这些类库可以以一种标准的方法、方便地访问数据库资源。
2)JDBC体系结构
① JDBC接口(API)包括两个层次:
面向应用的API:
java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)。
面向数据库的API
java Driver API,供开发商开发数据库驱动程序用
① 特点:直接获取Driver(驱动)对象
// 方式一:
@Test
public void testConnection1() throws SQLException {
//1、获取Driver(驱动)对象
Driver driver =new com.mysql.jdbc.Driver();
//2、提供要连接的数据库
/*
url:http://lovalhost:8080/gmall/keyboard.jpg
jdbc:mysql: 协议
localhost: ip地址
3306: 默认端口号
test: 名test的数据库
*/
String url ="jdbc:mysql://localhost:3306/bjpowernode";
//3、提供连接需要的用户名和密码
//将用户名和密码封装在Properties中
Properties info =new Properties();
info.setProperty("user","root");
info.setProperty("password","4680123");
//4、获取连接
java.sql.Connection conn = driver.connect(url,info);
System.out.println(conn);
}
② 特点:对方式一的迭代—如下程序不出现第三方的api,使程序具有更好的可移植性。
//方式二:对方式一的迭代---如下程序不出现第三方的api,使程序具有更好的可移植性。
@Test
public void testConnection2() throws Exception {
//1、获取Driver实现类对象:---使用反射
Class clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver=(Driver) clazz.getDeclaredConstructor().newInstance();
//2、提供要连接的数据库
String url ="jdbc:mysql://localhost:3306/bjpowernode";
//3、提供连接需要的用户名和密码
Properties info =new Properties();
info.setProperty("user","root");
info.setProperty("password","4680123");
//4、获取连接
java.sql.Connection conn = driver.connect(url,info);
System.out.println(conn);
}
③ 特点:在方式二的基础上,使用DriverManager替换Driver
// 方式三:使用DriverManager替换Driver
@Test
public void testConnection3() throws Exception{
//1、提供三个连接的基本信息
String url ="jdbc:mysql://localhost:3306/bjpowernode";
String user ="root";
String password ="4680123";
//2、获取Driver的实现类对象
Class clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver=(Driver) clazz.getDeclaredConstructor().newInstance();
//注册驱动
DriverManager.registerDriver(driver);
//获取连接
java.sql.Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
}
④ 特点:优化方式三—可以只是加载驱动,不用显示的注册驱动了
// 方式四:优化方式三---可以只是加载驱动,不用显示的注册驱动了
@Test
public void testConnection4() throws Exception{
//1、提供三个连接的基本信息
String url ="jdbc:mysql://localhost:3306/bjpowernode";
String user ="root";
String password ="4680123";
//2、加载Driver
Class.forName("com.mysql.jdbc.Driver");
//相较于方式三,可以省略如下的操作
//Driver driver=(Driver) clazz.getDeclaredConstructor().newInstance();
//注册驱动
//DriverManager.registerDriver(driver);
// 为什么可以省略上述操作
// 在mysql的Driver实现类中,存在一个静态代码块,
// 随着类的加载,内存实现了上述省略的操作
//获取连接
java.sql.Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
}
⑤- - -最终版 特点:将数据库连接需要的4个基本信息声明在配置文件中,读取配置文件jdbc.properties,获取连接
此方法的好处:
* 1、实现了数据与代码的分离;实现了解耦
* 2、如果需要修改配置文件信息,可以避免程序重新打包
@Test
public void testConnection5() throws Exception {
//1、读取配置文件中的4个基本信息 系统类的加载器
InputStream is = Connection.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties(); //Hashtable子类,key和value都是String类型
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//2、加载驱动
Class.forName(driverClass);
//3、获取链接
java.sql.Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
}
代表创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete)操作。
1)数据库连接的含义
数据库连接被用于向数据库服务端发送命令和SQL语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。
2)java.sql包中不同接口对数据库的调用
① Statement:用于执行静态SQL语句并返回它所生成结果的对象
② PrepatedStatement:SQL语句被预编译并存储在此对象中,可以使用此对象多次高效的执行该语句。
③ CallableStatement:用于执行SQL存储过程
① 存在拼串操作,繁琐
② 存在SQL注入问题
SQL注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令(如:select user,password from user_table where user=‘a’ or 1= ’ and password = ’ or ‘1’ = ‘1’),从而利用系统的SQL引擎完成恶意行为的做法。
对Java而言,要防范SQL注入,只要用PreparedStatement(从Statement扩展而来)取代Statement就可以了。
4.1.1增删改操作
例如:向customers表中添加一条记录
注意:
要操作的表是在读取配置文件(jdbc.properties)中对应的数据库里的
操作步骤如下:
1、读取配置文件中的4个基本信息 系统类的加载器
2、加载驱动
3、获取连接
4、预编译sql语句,返回PreparedStatement的实例
5、填充占位符
6、执行操作
7、资源关闭
代码如下:
@Test
public void testInsert() {
java.sql.Connection conn = null;
PreparedStatement ps = null;
try {
//1、读取配置文件中的4个基本信息 系统类的加载器
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties(); //Hashtable子类,key和value都是String类型
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//2、加载驱动
Class.forName(driverClass);
//3、获取链接
conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
//4、预编译sql语句,返回PreparedStatement的实例
String sql="insert into customers(name,email,birth) values(?,?,?)"; // ?表示占位符
ps = conn.prepareStatement(sql);
//5、填充占位符
ps.setString(1,"哪吒");
ps.setString(2,"[email protected]");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("1000-01-01");
ps.setDate(3,new java.sql.Date(date.getTime()));
//6、执行操作
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//7、资源关闭
try {
if (ps!=null)
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (conn!=null)
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
4.1.2 将共有操作(获取数据库连接、关闭资源)写进数据库工具类中
1) 获取数据库连接包含:① 读取配置文件中的4个基本信息 系统类的加载器、② 加载驱动、③ 获取连接
2)关闭资源–① 关闭Statement的操作、② 关闭连接
代码如下:
public class JDBCUtils {
// 获取数据库连接
public static Connection getConnection() throws Exception {
//1、读取配置文件中的4个基本信息 系统类的加载器
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties(); //Hashtable子类,key和value都是String类型
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//2、加载驱动
Class.forName(driverClass);
//3、获取连接
java.sql.Connection conn = DriverManager.getConnection(url, user, password);
return conn;
}
// 关闭资源--连接和Statement的操作
public static void closeResource(Statement ps,Connection conn){
try {
if (ps!=null)
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (conn!=null)
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
4.1.3 利用数据库工具类修改一条记录
操作步骤如下:
1、获取连接
2、预编译sql语句,返回PreparedStatement的实例
3、填充占位符
4、执行操作
5、资源关闭
代码如下:
//修改customers表中一条记录
@Test
public void testUpdate(){
Connection conn = null;
PreparedStatement ps = null;
try {
//1、获取数据库连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回PreparedStatement的实例
String sql ="update customers set name = ? where id =?";
ps = conn.prepareStatement(sql);
//3、填充占位符
ps.setObject(1,"莫扎特");
ps.setObject(2,18);
//4、执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//5、资源的关闭
JDBCUtils.closeResource(ps,conn);
}
}
4.1.4 将增删改sql语句作为变量参数,占位符作为可变参数- - -写出通用增删改操作 (同时使用数据库工具类)
① String sql 存储增删改sql语句、Object…args存储占位符, 写出通用增删改操作
代码如下:
//通用的增删改操作
public void update(String sql,Object...args){ //用可变参数存占位符
Connection conn = null;
PreparedStatement ps = null;
try {
//1、获取数据库连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//3、填充占位符
for (int i=0 ;i<args.length;i++){
ps.setObject(i+1,args[i]); // 小心参数声明错误
}
//4、执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//5、资源的关闭
JDBCUtils.closeResource(ps,conn);
}
}
② 测试通用增删改,实现对customers表、order表的数据进行修改
注意:order是与关键字相同的表名,在对此表进行操作时要使用 `` 符号括起来。
// 测试通用增删改操作
@Test
public void testCommonUpdate(){
//String sql="delete from customers where id =?";
//update(sql,3);
String sql ="update `order` set order_name =? where order_id =?";
update(sql,"DD","2");
}
4.2.1对Customers表查询操作
注意:
1) select与增删改不同的是执行操作后需要显示出来,故执行操作时采用executeQuery方法,返回一个结果集resultSet。
2)if (resultSet.next()),next指针在结果集初始的上方,故用来判断结果集下一条是否有数据
3) 三种方式输出select查询到的结果
推荐方式三(将数据封装为一个对象)
此方法体现了 ORM编程思想(object relational mapping)
① 一个数据表对应一个java类
② 表中的一条记录对应java类的一个对象
③ 表中的一个字段对象java类的一个属性
代码如下:
public class Customer {
private int id;
private String name;
private String email;
private Date birth;
public Customer() {
}
public Customer(int id, String name, String email, Date birth) {
this.id = id;
this.name = name;
this.email = email;
this.birth = birth;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", birth=" + birth +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
}
4)当在关闭资源时,也要将结果集resultSet资源进行关闭,故需要在*数据库工具类JDBCUtils中重载closeResource,增加结果集的关闭 - 代码省略。
5)综上1-4分析写出查询Customer表的代码如下:
@Test
public void testQuery1(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet resultSet = null;
try {
//1、获取数据库连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回PreparedStatement的实例
String sql="select customers.* from customers where id=?";
ps = conn.prepareStatement(sql);
//3、填充占位符
ps.setObject(1,1);
//4、执行,并返回结果集
resultSet = ps.executeQuery();
//5、处理结果集
if (resultSet.next()){ //判断结果集下一条是否有数据,
// 如果有数据返回true,指针下移,如果没有数据返回flase
// 获取当前这条数据的各个字段值
int id = resultSet.getInt(1);
String name = resultSet.getString(2);
String email = resultSet.getString(3);
Date birth = resultSet.getDate(4);
//方式一:
//System.out.println("id = "+ id +",name ="+ name +",email ="+ email +",birth ="+ birth);
//方式二:
//Object[] data = {id, name, email, birth};
//方式三:将数据封装为一个对象(推荐)
Customer customer = new Customer(id, name, email, birth);
System.out.println(customer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//6、关闭资源
JDBCUtils.closeResource(ps,conn,resultSet);
}
}
4.2.2对customers表的通用查询操作
① 注意:
1) String sql 存储增删改sql语句、Object…args存储占位符, 写出通用查询操作
2)重点在于结果集的处理上,
① 根据结果集获取它的元数据ResultSetMetaData
② 利用元数据获取结果集的列数
③ if (rs.next()),next指针在结果集初始的上方,故用来判断结果集下一条是否有数据,创建数据封装的对象Customer cust = new Customer();
④ 获取结果集每一行对应的列的值
⑤ 获取每个列的列名
⑥ 给cust对象指定的columnName属性,赋值为columValue,通过反射
代码如下:
/*
* 针对customers表的通用的查询操作
* */
public Customer queryForCustomers(String sql,Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、获取数据库连接
conn = JDBCUtils.getConnection();
//2、返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//3、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//4、执行,并返回结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
// 通过ResultSetMetaData获取结果集的列数
int columnCount = rsmd.getColumnCount();
if (rs.next()){ //if (rs.next()),next指针在结果集初始的上方,故用来判断结果集下一条是否有数据
Customer cust = new Customer();
//处理结果集一行数据中的每一列
for (int i=0;i<columnCount;i++){
//获取每列对应的值
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名
String columnName = rsmd.getColumnName(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = Customer.class.getDeclaredField(columnName);
field.setAccessible(true);
field.set(cust,columnValue);
}
return cust;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCUtils.closeResource(ps,conn,rs);
}
return null;
}
② 测试customers表的通用的查询操作
注意:没被查询的字段会被默认赋值,因为在处理结果集时, Customer cust = new Customer();**
@Test
public void testQueryForCustomers(){
String sql="select id,name,birth,email from customers where id=?";
Customer customer = queryForCustomers(sql, 13);
System.out.println(customer);
// 没被查询的字段会被默认赋值,因为在处理结果集时, Customer cust = new Customer();
sql="select name,email from customers where name= ?";
Customer customer1 = queryForCustomers(sql, "周杰伦");
System.out.println(customer1);
}
4.3.1Order表的查询操作(与Customer不同)
不同点、带来的问题以及处理方法如下:
① 在Order类中的属性名与Order数据表中的字段名不同
② 不同点带来的问题:
当反射机制去用字段名查找类属性时,就会报错,
③处理方法:
需要在反射机制中用rsmd.getColumnLabel方法获取别名(没有别名,默认获取字段名),,并在sql语句中对查询到的字段起别名,使结果集中的字段名与Order类属性名相同
Order类代码如下: 属性orderId与字段名order_id不同
public class Order {
private int orderId;
private String orderName;
private Date orderDate;
public Order(){
super();
}
public Order(int orderId, String orderName, Date orderDate) {
this.orderId = orderId;
this.orderName = orderName;
this.orderDate = orderDate;
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", orderName='" + orderName + '\'' +
", orderDate=" + orderDate +
'}';
}
public int getOrderId() {
return orderId;
}
public void setOrderId(int orderId) {
this.orderId = orderId;
}
public String getOrderName() {
return orderName;
}
public void setOrderName(String orderName) {
this.orderName = orderName;
}
public Date getOrderDate() {
return orderDate;
}
public void setOrderDate(Date orderDate) {
this.orderDate = orderDate;
}
}
普通的一条sql语句的操作同Customer表一样
代码如下:
@Test
public void tesetQuery1() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、获取连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回prepareStatement实例
String sql="select order_id,order_name,order_date from `order` where order_id =?";
ps = conn.prepareStatement(sql);
//3、填充占位符
ps.setObject(1,1);
//4、执行,并获取结果集
rs = ps.executeQuery();
//5、处理结果集
if(rs.next()){
// 获取当前这条数据的各个字段值
int id = (int) rs.getObject(1);
String name = (String) rs.getObject(2);
Date date = (Date) rs.getObject(3);
// 将数据封装为一个对象
Order order = new Order(id, name, date);
System.out.println(order);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//6、关闭资源
JDBCUtils.closeResource(ps,conn,rs);
}
}
4.3.2对order表的通用查询操作
① 注意
在Order类中的属性名与Order数据表中的字段名不同
当反射机制去用字段名查找类属性时,就会报错,故需要在反射机制中用rsmd.getColumnLabel方法获取别名(没有别名,默认获取字段名),,并在sql语句中对查询到的字段起别名,使结果集中的字段名与Order类属性名相同
代码如下:
/*
* 针对order表的通用的查询操作
* */
public Order orderForQuery(String sql,Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、获取连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//3、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//4、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
if (rs.next()){
Order order = new Order();
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//获取列的列名getColumnName 不推荐使用
//获取列的别名getColumnLabel 没有别名,默认列名
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = Order.class.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(order,columnValue);
}
return order;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,conn,rs);
}
return null;
}
② 测试order表的通用的查询操作
注意:没被查询的字段会被默认赋值,因为在处理结果集时, Order order = new Order();
//测试通用order查询方法
@Test
public void testOrderForQuery(){
// 在sql语句中对查询到的字段起别名,使结果集中的字段名与Order类属性名相同
String sql="select order_id orderId,order_name orderName,order_date orderDate from `order` where order_id =?";
Order order = orderForQuery(sql, 1);
System.out.println(order);
}
③ 总结:
针对表的字段名与类的属性名不相同的情况:
① 必须声明sql时,使用类的属性名来命名字段的别名
② 使用ResultSetMetaData时,需要使用getColumnLabel()来替换getColumnName()来获取列的别名。
注意:如果sql中没有给字段起别名,getColumnLabel()获取的就是字段名(列名)。
查询过程的流程:
4.4.1对不同表的通用查询操作 - - -返回表中的一条记录
不知道是哪个表,故需要此表的类作为参数传入进来。
其中要通过反射创建运行时类的对象
代码如下:
// 针对不同表的通用的查询操作,返回表中的一条记录
public <T> T getInstance(Class<T> clazz,String sql,Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、获取连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//3、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//4、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
if (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,conn,rs);
}
return null;
}
4.4.2对不同表的通用查询操作 - - -返回表中的多条记录(sql语句中查询到多条记录,如where id<12)
① 实现不同表通用查询多条记录
原来是返回一个对象,此时需要利用循环,把每条记录(每行)创建的对象,添加到一个集合中,返回对象集合。
代码如下:
public <T> List<T> getForList(Class<T> clazz,String sql,Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、获取连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//3、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//4、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
// 创建集合对象
ArrayList<T> list = new ArrayList<>();
while (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
//处理结果集每一行数据中的每一列,给t对象指定的属性赋值
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,conn,rs);
}
return null;
}
② 测试不同表通用查询多条记录
由于返回的是个集合,因此需要遍历输出多条记录(利用forEach方法)
代码如下:
@Test
public void testGetForLit(){
String sql ="select id,name,email from customers where id < ?";
List<Customer> list = getForList(Customer.class, sql, 12);
list.forEach(System.out::println);
String sql1="select order_id orderId,order_name orderName,order_date orderDate from `order` where order_id < ?";
List<Order> orderList = getForList(Order.class, sql1, 5);
orderList.forEach(System.out::println);
}
③ 总结:
PreparedStatement替换Statement
除了解决Statement的拼串、sql注入问题之外,PreparedStatement还有好处如下:
1)PreparedStatement操作Blob的数据,而Statement做不到。
2)PreparedStatement可以实现更高效的批量操作。
PreparedStatement操作Blob的数据,而Statement做不到
举例说明
FileInputStream is = new FileInputStream(new File("ss.jpg"));
ps.setObject(4,is);
代码如下:
//向数据表custpmers中插入Blob类型的字段
@Test
public void testInsert() throws Exception{
Connection conn = JDBCUtils.getConnection();
String sql="insert into customers(name,email,birth,photo) values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1,"赵武");
ps.setObject(2,"[email protected]");
ps.setObject(3,"1992-08-07");
FileInputStream is = new FileInputStream(new File("ss.jpg"));
ps.setObject(4,is);
ps.execute();
JDBCUtils.closeResource(ps,conn);
}
举例:
实现查询Blob类型字段,下载下来,以文件的方式保存在本地。
实现方法:
当查询到Blob类型的字段photo后,调用方法getBinaryStream()返回此文件的输入流,再创建一个输出流,来完成文件的读取和下载(关闭资源时,也要关闭输入流和输出流)。
代码如下:
//查询数据表customers中Blob类型的字段
@Test
public void testQuery(){
Connection conn = null;
PreparedStatement ps = null;
InputStream is= null;
FileOutputStream fos = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
String sql = "select id,name,email,birth,photo from customers where id=?";
ps = conn.prepareStatement(sql);
ps.setInt(1,21);
rs = ps.executeQuery();
if (rs.next()){
// 方式一:索引
/*int id = rs.getInt(1);
String name = rs.getString(2);
String email = rs.getString(3);
Date birth = rs.getDate(4);
*/
// 方式二:列的别名
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
Date birth = rs.getDate("birth");
Customer cust = new Customer(id, name, email, birth);
System.out.println(cust);
//查询Blob类型字段,下载下来,以文件的方式保存在本地
Blob photo = rs.getBlob("photo");
is = photo.getBinaryStream(); //调用方法获取输入流
fos = new FileOutputStream("kakaxi.jpg");
byte[] buffer = new byte[1024];
int len;
while ((len=is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is!=null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos!=null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
JDBCUtils.closeResource(ps,conn,rs);
}
}
update、deltete本身就具有批量操作的效果(不加条件where…)
PreparedStatement可以实现更高效的批量操作。
题目:向goods表中插入20000条数据
CREATE TABLE goods(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(25)
);
缺点:由于没有预编译sql语句,导致sql在循环中生成,即每循环一次,生成一个sql语句变量,执行一句。浪费时间。
Connection conn = JDBCUtils.getConnection();
Statement st = conn.createStatement();
for(int i=0;i<=20000;i++){
String sql = "insert into goods(name)values('name_" + i + "')";
st.execute(sql);
}
优点:存在预编译,sql语句变量只生成一次,在循环中给占位符传值即可。
缺点:依然是见一条sql语句,执行一条,并且提交一次,浪费时间。
代码如下:
//批量插入方式二:PreparedStatement
@Test
public void testInsert1(){
Connection conn = null;
PreparedStatement ps = null;
try {
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
String sql ="insert into goods(name)values(?)"; //预编译缓存
ps = conn.prepareStatement(sql);
for (int i=1;i<=20000;i++){
ps.setObject(1,"name_"+i);
ps.execute();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: "+(end-start));//51290
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(ps,conn);
}
}
1、使用addBatch()、executeBatch()、clearBatch() “攒sql语句”,到一定数量之后,一并执行,节约时间。 - - - 要使用此方法
2、mysql服务器默认是关闭批处理的,我们需要通过一个参数,让mysql开启批处理的支持。
url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&rewriteBatchedStatements=true
driverClass = com.mysql.cj.jdbc.Driver
3、使用更新的mysql驱动:mysql-connector-java-8.0.11-bin.jar
优点:使用addBatch()、executeBatch()、clearBatch() “攒sql语句”,到一定数量之后,一并执行,节约时间。 - - -例如:“攒”500个sql语句后,再执行一次
缺点:每执行一条sql语句,就会提交(写入数据库) 一次,在提交过程中也浪费时间。
代码如下:
@Test
public void testInsert2() {
Connection conn = null;
PreparedStatement ps = null;
try {
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)values(?)"; //预编译缓存
ps = conn.prepareStatement(sql);
for (int i = 1; i <= 1000000; i++) {
ps.setObject(1, "name_" + i);
// 1、”攒“sql
ps.addBatch();
if (i % 500 == 0) { // 每500次执行一次(与数据库交互一次),减少调度
//2、执行batch
ps.executeBatch();
//3、清空batch
ps.clearBatch();
}
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start));// 20000用时579
// 1000000用时14160
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(ps, conn);
}
}
在方式三的基础上,设置不允许自动提交数据(每执行一次不会提交数据,最后同一提交)
优点:“攒sql语句”、设置不允许自动提交数据,减少了执行次数和提交次数,从而节约了时间。
代码如下:
//批量插入方式四:PreparedStatement优化方式三
@Test
public void testInsert3() {
Connection conn = null;
PreparedStatement ps = null;
try {
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
// 设置不允许自动提交数据
conn.setAutoCommit(false);
String sql = "insert into goods(name)values(?)"; //预编译缓存
ps = conn.prepareStatement(sql);
for (int i = 1; i <= 1000000; i++) {
ps.setObject(1, "name_" + i);
// 1、”攒“sql
ps.addBatch();
if (i % 500 == 0) { // 每500次执行一次(与数据库交互一次),减少交互时间
//2、执行batch
ps.executeBatch(); //每执行一次就会提交一次,也就写死到数据库中一次,浪费了时间
// 设置不允许自动提交,可以节约时间
//3、清空batch
ps.clearBatch();
}
}
//最后统一的提交数据
conn.commit();
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start));// 1000000用时7297
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(ps, conn);
}
}
事务:一组逻辑操作单元,是数据从一种状态变换到另一种状态。
一组逻辑操作单元:一个或多个DML操作
保证所有事务都作为一个工作单元来执行,即出现了故障,都不能改变这种执行方式。
当一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所做的所有修改,整个事务回滚(rollback)到最初的状态。
注意:数据一旦提交,就不可回滚。
① DDL操作一旦执行,都会自动提交。
set autocommit = false 对DDL操作失效
② DML默认情况下,一旦执行们就会自动提交。
我们可以通过set autocommit = false方式取消DML操作的自动提交。
③ 默认在关闭连接的时,会自动提交数据
在未考虑数据库事务情况下的转账操作
当出现异常时,会导致出现异常之前的DML操作已经提交,不能在回滚,导致事务在没有完成的条件下,改变了数据。错误!!!
代码如下:
//***************未考虑数据库事务情况下的转账操作******************
@Test
public void testUpdate(){
String sql1="update user_table set balance=balance - 100 where user=?";
update(sql1,"AA");
//模拟网络异常
System.out.println(10/0); //此时就会出现只有AA减了100
String sql2="update user_table set balance=balance + 100 where user=?";
update(sql1,"BB");
System.out.println("转账成功");
}
//通用的增删改操作 - - -version 1.0
public void update(String sql, Object... args) { //用可变参数存占位符
Connection conn = null;
PreparedStatement ps = null;
try {
//1、获取数据库连接
conn = JDBCUtils.getConnection();
//2、预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//3、填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]); // 小心参数声明错误
}
//4、执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//5、资源的关闭
JDBCUtils.closeResource(ps, conn);
}
}
考虑数据库事务情况下的转账操作
即:
① 通过set autocommit = false方式取消DML操作的自动提交,执行完所有sql语句后一并提交。
② 在执行完事务的所有sql语句后,一并关闭资源(即连接conn在调用函数的外部创建和关闭)
代码如下:
//***************考虑数据库事务情况下的转账操作******************
@Test
public void testUpdateWithTx(){
Connection conn = null;
try {
//获取连接
conn = JDBCUtils.getConnection();
//1、取消数据的自动提交
conn.setAutoCommit(false);
String sql1="update user_table set balance=balance - 100 where user=?";
update(conn,sql1,"AA");
//模拟网络异常
//System.out.println(10/0); //此时就会出现回滚数据
String sql2="update user_table set balance=balance + 100 where user=?";
update(conn,sql2,"BB");
System.out.println("转账成功");
//2、提交数据
conn.commit();
} catch (Exception e) {
e.printStackTrace();
// 3、出现异常,回滚数据
try {
conn.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
} finally {
//关闭资源
JDBCUtils.closeResource(null,conn); //ps已经在内部关闭
}
}
//重载- - - 通用的增删改操作 - - -version 2.0(考虑上事务)
public void update(Connection conn,String sql, Object... args) { //用可变参数存占位符
PreparedStatement ps = null;
try {
//1、预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]); // 小心参数声明错误
}
//3、执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//4、资源的关闭
JDBCUtils.closeResource(ps, null); //因为是外部传的连接,这里也不要关闭
}
}
JDBC程序中为了让多个SQL语句作为一个事务执行
① 调用Connection对象的setAutoCommit(false); 以取消自动提交事务;
② 在所有的SQL语句都执行成功后,调用commit(); 方法提交事务;
③ 在出现异常时,调用rollback(); 方法回滚事务。
注意:
若Connection没有被关闭,还可能被重复使用,则需要恢复其自动提交状态,setAutoCommit(true)。尤其是在使用数据库连接池技术时,执行close()方法前,建议恢复自动提交状态。
A:原子性(Atomicity)
说明事务是最小的工作单元。不可再分。
C:一致性(Consistency)
所有事务要求,在同一个事务当中,所有操作必须同时成功,或者同时失败,以保证数据的一致性。
I:隔离性(Isolation)
A事务和B事务之间具有一定的隔离。
教室A和教室B之间有一道墙,这道墙就是隔离性。
A事务在操作一张表的时候,另一个事务B也操作这张表会那样???
D:持久性(Durability)
事务最终结束的一个保障。事务提交,就相当于将没有保存到硬盘上的数据
保存到硬盘上!
读未提交:read uncommitted(最低的隔离级别)《没有提交就读到了》- - -脏读现象!(Dirty Read)
什么是读未提交?
事务A可以读取到事务B未提交的数据。
这种隔离级别存在的问题就是:
脏读现象!(Dirty Read)
我们称读到了脏数据。
这种隔离级别一般都是理论上的,大多数的数据库隔离级别都是二档起步!
读已提交:read committed《提交之后才能读到》- - -不可重复读取数据,但解决了脏读的现象。
什么是读已提交?
事务A只能读取到事务B提交之后的数据。
这种隔离级别解决了什么问题?
解决了脏读的现象。
这种隔离级别存在什么问题?
不可重复读取数据。
什么是不可重复读取数据呢?
在事务开启之后,第一次读到的数据是3条,当前事务还没有
结束,可能第二次再读取的时候,读到的数据是4条,3不等于4
称为不可重复读取。
这种隔离级别是比较真实的数据,每一次读到的数据是绝对的真实。
oracle数据库默认的隔离级别是:read committed
可重复读:repeatable read《提交之后也读不到,永远读取的都是刚开启事务时的数据》 - - - 解决不可重复读取数据,但会出现幻读
什么是可重复读取?
事务A开启之后,不管是多久,每一次在事务A中读取到的数据
都是一致的。即使事务B将数据已经修改,并且提交了,事务A
读取到的数据还是没有发生改变,这就是可重复读。
可重复读解决了什么问题?
解决了不可重复读取数据。
可重复读存在的问题是什么?
可以会出现幻读。
每一次读取到的数据都是幻象。不真实!
早晨9点开始开启了事务,只要事务不结束,到晚上9点,读到的数据还是那样!
读到的是假象。不够绝对的真实。
mysql中默认的事务隔离级别就是这个!!!!!!!!!!!
序列化/串行化:serializable(最高的隔离级别)
这是最高隔离级别,效率最低。解决了所有的问题。
这种隔离级别表示事务排队,不能并发!
synchronized,线程同步(事务同步)
每一次读取到的数据都是最真实的,并且效率是最低的。
考虑数据库事务情况下的查询操作
在外部实现连接,且在外部conn.getTransactionIsolation()获取当前连接的隔离级别,conn.setTransactionIsolation(Connection.*)设置数据库的隔离级别。
@Test
public void testTransactionSelect() throws Exception{
Connection conn = JDBCUtils.getConnection();
//获取当前连接的隔离级别
System.out.println(conn.getTransactionIsolation());
//设置数据库的隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
//取消自动提交数据
conn.setAutoCommit(false);
String sql="select user,password,balance from user_table where user=?";
List<User> userList = getForList(conn, User.class, sql, "CC");
userList.forEach(System.out::println);
}
@Test
public void testTransactionUpdate() throws Exception{
Connection conn = JDBCUtils.getConnection();
//取消自动提交数据
conn.setAutoCommit(false);
String sql="update user_table set balance=? where user=?";
update(conn,sql,5000,"CC");
Thread.sleep(15000);
System.out.println("修改结束");
}
// 对任意对象的通用查询操作 - - -version 2.0(考虑上事务)
public <T> List<T> getForList(Connection conn,Class<T> clazz, String sql, Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//3、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
// 创建集合对象
ArrayList<T> list = new ArrayList<>();
while (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
//处理结果集每一行数据中的每一列,给t对象指定的属性赋值
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,null,rs);// 不需要在内部关conn
}
return null;
}
DAO:data(base) accsee object 是一个数据访问接口
四部分:
① BaseDao类:包装针对于数据表通用操作(增删改、查询一条记录/多条记录/特殊值) - - -考虑到事务
代码如下:
* DAO:data(base) access object
* 封装了针对于数据表的通用的操作
*/
public abstract class BaseDao { //表示是一个抽象类,不能实例化
//--- 通用的增删改操作 ---version 2.0(考虑上事务,即连接从外部传入)
public void update(Connection conn, String sql, Object... args) { //用可变参数存占位符
PreparedStatement ps = null;
try {
//1、预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]); // 小心参数声明错误
}
//3、执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//4、资源的关闭
JDBCUtils.closeResource(ps, null); //因为是外部传的连接,这里也不要关闭
}
}
//---通用查询操作,返回任意表的一条记录---version 2.0(考虑上事务,即连接从外部传入)
public <T> T getInstance(Connection conn,Class<T> clazz,String sql,Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//3、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
if (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,null,rs);
}
return null;
}
//---通用查询操作,返回任意表的多条记录的集合---version 2.0(考虑上事务,即连接从外部传入)
public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//3、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
// 创建集合对象
ArrayList<T> list = new ArrayList<>();
while (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
//处理结果集每一行数据中的每一列,给t对象指定的属性赋值
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,null,rs);// 不需要在内部关conn
}
return null;
}
//用于查询特殊值的通用方法 考虑到了事务
public <E> E getValue(Connection conn,String sql,Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(sql);
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
rs = ps.executeQuery();
if (rs.next()){
return (E) rs.getObject(1);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
JDBCUtils.closeResource(ps,conn,rs);
}
return null;
}
}
② 创建Customer类:
ORM编程思想(object relational mapping)
代码如下:
public class Customer {
private int id;
private String name;
private String email;
private Date birth;
public Customer() {
}
public Customer(int id, String name, String email, Date birth) {
this.id = id;
this.name = name;
this.email = email;
this.birth = birth;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", birth=" + birth +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
}
③ 创建CustomerDAO接口:
此接口用于规范针对于customers表的常用操作
内容回顾:
① 接口中所有方法都是抽象方法;
② 无论加不加abstract,所有实现指定接口的类都必须重写并实现该接口的所有方法,否则这个类就是抽象类。
代码如下:
public interface CustomerDAO {
//将cust对象添加到数据库中
void insert(Connection conn, Customer cust);
//根据指定的id,删除表中的一条记录
void deleteById(Connection conn, int id);
//针对内存中的cust对象,去修改数据表中的指定的记录
void update(Connection conn,Customer cust);
// 针对指定的id查询得到对象的Customer对象
Customer getCustomerById(Connection conn,int id);
//查询表中的所有记录构成的集合
List<Customer> getAll(Connection conn);
//返回数据表中的数据的条目数
Long getCount(Connection conn);
//返回数据表中最大的生日
Date getMaxBirth(Connection conn);
}
④ 创建 继承对表操作的BaseDao类,以及实现CustomerDAO接口的类CustomerDAOImpl:
类CustomerDAOImpl要重写CustomerDAO接口的所有方法,在重写方法中对表进行操作故要调用从BaseDao类中继承来的方法。
public class CustomerDAOImpl extends BaseDao implements CustomerDAO{
@Override
public void insert(Connection conn, Customer cust) {
String sql="insert into customers(name,email,birth) values(?,?,?)";
update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth());
}
@Override
public void deleteById(Connection conn, int id) {
String sql="delete from customers where id=?";
update(conn,sql,id);
}
@Override
public void update(Connection conn, Customer cust) {
String sql="update customers set name=?,email=?,birth=? where id=?";
update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth(),cust.getId());
}
@Override
public Customer getCustomerById(Connection conn, int id) {
String sql ="select id,name,email,birth from customers where id=?";
Customer customer = getInstance(conn, Customer.class, sql, id);
return customer;
}
@Override
public List<Customer> getAll(Connection conn) {
String sql ="select id,name,email,birth from customers ";
List<Customer> customerList = getForList(conn, Customer.class, sql);
return customerList;
}
@Override
public Date getMaxBirth(Connection conn) {
String sql="select max(birth) from customers";
return getValue(conn,sql);
}
@Override
public Long getCount(Connection conn) {
String sql="select count(*) from customers";
return getValue(conn,sql);
}
}
对于已经知道对customers表的操作,当子类继承父类时包含泛型即可。故对BaseDao类、CustomerDAOImpl类进行简单修改。
BaseDao类修改后代码如下:
public abstract class BaseDao<T> { //表示是一个抽象类,不能实例化
private Class<T> clazz=null; // clazz的类取决于CustomerDAOImpl继承BaseDao泛型中的类
//显式赋值 定义时、构造器中、代码块中
{ //获取当前BaseDao的子类继承的父类的泛型 this.getClass()是CustomerDAOImpl类
// 对象.getClass返回的是创建这个对象的类(即运行时类 CustomerDAOImpl)
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType paramType =(ParameterizedType) genericSuperclass;
Type[] typeArguments = paramType.getActualTypeArguments();//获取了父类的泛型参数
clazz=(Class<T>) typeArguments[0];//泛型的第一个参数Customer
}
//--- 通用的增删改操作 ---version 2.0(考虑上事务,即连接从外部传入)
public void update(Connection conn, String sql, Object... args) { //用可变参数存占位符
PreparedStatement ps = null;
try {
//1、预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]); // 小心参数声明错误
}
//3、执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//4、资源的关闭
JDBCUtils.closeResource(ps, null); //因为是外部传的连接,这里也不要关闭
}
}
//---通用查询操作,返回任意表的一条记录---version 2.0(考虑上事务,即连接从外部传入)
public T getInstance(Connection conn,String sql,Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//3、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
if (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,null,rs);
}
return null;
}
//---通用查询操作,返回任意表的多条记录的集合---version 2.0(考虑上事务,即连接从外部传入)
public List<T> getForList(Connection conn, String sql, Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1、预编译sql语句,返回prepareStatement实例
ps = conn.prepareStatement(sql);
//2、填充占位符
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
//3、执行,并获取结果集
rs = ps.executeQuery();
// 获取结果集的元数据 ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数,通过元数据 ResultSetMetaData获得
int columnCount = rsmd.getColumnCount();
// 创建集合对象
ArrayList<T> list = new ArrayList<>();
while (rs.next()){
//创建运行时类的对象
T t = clazz.getDeclaredConstructor().newInstance();
//处理结果集每一行数据中的每一列,给t对象指定的属性赋值
for (int i=0;i<columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//获取每个列的列名:通过ResultSetMetaData
//String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//给cust对象指定的columnName属性,赋值为columValue,通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t,columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
JDBCUtils.closeResource(ps,null,rs);// 不需要在内部关conn
}
return null;
}
//用于查询特殊值的通用方法 考虑到了事务
public <E> E getValue(Connection conn,String sql,Object...args){
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(sql);
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
rs = ps.executeQuery();
if (rs.next()){
return (E) rs.getObject(1);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
JDBCUtils.closeResource(ps,conn,rs);
}
return null;
}
}
CustomerDAOImpl类修改后代码如下:
public class CustomerDAOImpl extends BaseDao<Customer> implements CustomerDAO {
//获取当前对象的父类的泛型
/*{
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType paramType =(ParameterizedType) genericSuperclass;
Type[] typeArguments = paramType.getActualTypeArguments();//获取了父类的泛型参数
clazz=(Class) typeArguments[0];//泛型的第一个参数Customer
}*/
@Override
public void insert(Connection conn, Customer cust) {
String sql="insert into customers(name,email,birth) values(?,?,?)";
update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth());
}
@Override
public void deleteById(Connection conn, int id) {
String sql="delete from customers where id=?";
update(conn,sql,id);
}
@Override
public void update(Connection conn, Customer cust) {
String sql="update customers set name=?,email=?,birth=? where id=?";
update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth(),cust.getId());
}
@Override
public Customer getCustomerById(Connection conn, int id) {
String sql ="select id,name,email,birth from customers where id=?";
Customer customer = getInstance(conn,sql, id);
return customer;
}
@Override
public List<Customer> getAll(Connection conn) {
String sql ="select id,name,email,birth from customers ";
List<Customer> customerList = getForList(conn,sql);
return customerList;
}
@Override
public Date getMaxBirth(Connection conn) {
String sql="select max(birth) from customers";
return getValue(conn,sql);
}
@Override
public Long getCount(Connection conn) {
String sql="select count(*) from customers";
return getValue(conn,sql);
}
}
在使用开发基于数据库的web程序时,传统的模式基本是按以下步骤:
① 在主程序(如servlet、beans)中建立数据库连接
② 进行sql操作
③ 断开数据库连接
这种模式开发,存在的问题:
① 普通的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接的时候都要将Connection加载到内存中,再验证用户名和密码(得花费0.05s-1s时间)。导致数据库的连接资源并没有得到很好的重复利用。
② 对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄露,最终将导致重启数据库。
③ 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾忌地分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。
数据库连接池的基本思想:
就是为数据库连接建立一个“缓冲池”。与现在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要冲“缓冲池”中取一个,使用完毕之后再放回去。
两种方式使用此连接池
1)方式一:硬编码
//方式一:硬编码
@Test
public void testGetConnection() throws Exception{
//获取C3P0数据库连接池
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass( "com.mysql.cj.jdbc.Driver" ); //loads the jdbc driver
cpds.setJdbcUrl( "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&rewriteBatchedStatements=true" );
cpds.setUser("root");
cpds.setPassword("4680123");
//通过设置相关的参数,对数据库连接池进行管理
//设置初始的数据库连接池中的连接数
cpds.setInitialPoolSize(10);
Connection conn = cpds.getConnection(); //获取其中一个连接
System.out.println(conn);
//销毁c3p0数据库连接池 一般不用
//DataSources.destroy(cpds);
}
2)方式二:使用配置文件
// 方式二:使用配置文件
@Test
public void testGetConnection1() throws Exception{
ComboPooledDataSource cpds = new ComboPooledDataSource("helloC3P0");
Connection conn = cpds.getConnection();
System.out.println(conn);
}
其配置文件写在XML文件中
代码如下:
<c3p0-config>
<named-config name="helloC3P0">
<property name="driverClass">com.mysql.cj.jdbc.Driverproperty>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&rewriteBatchedStatements=trueproperty>
<property name="user">rootproperty>
<property name="password">4680123property>
<property name="acquireIncrement">5property>
<property name="initialPoolSize">10property>
<property name="minPoolSize">10property>
<property name="maxPoolSize">1000property>
<property name="maxStatements">50property>
<property name="maxStatementsPerConnection">2property>
named-config>
c3p0-config>
两种方式使用此连接池
1)方式一:硬编码
//方式一:硬编码,不推荐
@Test
public void testGetConnection() throws Exception{
//创建了DBCP的数据库连接池
BasicDataSource source = new BasicDataSource();
//设置基本信息
source.setDriverClassName("com.mysql.cj.jdbc.Driver");
source.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&rewriteBatchedStatements=true");
source.setUsername("root");
source.setPassword("4680123");
//还可以设置其他涉及数据库连接池管理的相关属性:
source.setInitialSize(10);
source.setMaxActive(10);
//...
Connection conn = source.getConnection();
System.out.println(conn);
}
2)方式二:使用配置文件
@Test
//方式二:推荐- - -使用配置文件
public void testGetConnection1() throws Exception{
Properties pros =new Properties();
//获取流的方式一:
//InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("DBCP.properties");
//方式二:
FileInputStream is = new FileInputStream(new File("src/DBCP.properties"));
//读取配置文件
pros.load(is);
//创建一个DBCP数据库连接池
DataSource source = BasicDataSourceFactory.createDataSource(pros);
Connection conn = source.getConnection();
System.out.println(conn);
}
其配置文件写在File文件中
代码如下:
username=root
password=4680123
url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&rewriteBatchedStatements=true
driverClassName = com.mysql.cj.jdbc.Driver
initialSize=10
两种方式使用此连接池
1)方式一:同DBCP方式一类似,硬编码,不再写出
2)方式二:使用配置文件,也同DBCP类似
@Test
public void testGetConnection() throws Exception{
Properties pros = new Properties();
//获取流
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("Druid.properties");
//读取配置文件
pros.load(is);
//创建一个DBCP数据库连接池
DataSource source = DruidDataSourceFactory.createDataSource(pros);
Connection conn = source.getConnection();
System.out.println(conn);
}
其配置文件写在File文件中
代码如下:
url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&rewriteBatchedStatements=true
username=root
password=4680123
driverClassName=com.mysql.cj.jdbc.Driver
initialSize=10
maxActive=10
注意事项:
① 要把创建连接池对象这行代码放到获取连接方法getConnection()的外部
② 当把DBCP、Druid连接池对象放到外部过程中,由于两者的配置文件在File文件中,当执行pros.load(is);读取配置文件时,在外部不能实现,故要放入静态代码块执行。
三种连接池在工具类中的代码如下:
public class JDBCUtils2 {
// 使用C3P0的数据库连接池技术
// cpsd放里面每次创建连接就会创建一个,放在外面是只有一个连接池对象,只需要一个连接池对象
private static ComboPooledDataSource cpds = new ComboPooledDataSource("helloC3P0");
public static Connection getConnection() throws Exception{
Connection conn = cpds.getConnection();
return conn;
}
//使用DBCP数据库连接池技术获取数据库连接
//把连接池对象的创建也放在外部
//由于在外部pros.load(is);不能实现,故放入静态代码块中执行
private static DataSource source; //source要在下面方法中使用,故定义为静态
static {
try {
Properties pros =new Properties();
//获取流的方式一:
//InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("DBCP.properties");
//方式二:
FileInputStream is = new FileInputStream(new File("src/DBCP.properties"));
pros.load(is);
source = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection1() throws Exception{
Connection conn = source.getConnection();
return conn;
}
//使用Druid数据库连接池技术获取数据库连接
//把连接池对象的创建也放在外部
//由于在外部pros.load(is);不能实现,故放入静态代码块中执行
private static DataSource source1; //source1要在下面方法中使用,故定义为静态
static {
try {
Properties pros =new Properties();
//获取流的方式一:
//InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("Druid.properties");
//方式二:
FileInputStream is = new FileInputStream(new File("src/Druid.properties"));
pros.load(is);
source1 = DruidDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection2() throws Exception{
Connection conn = source1.getConnection();
return conn;
}
}
commons-dbutils是Apache组织提供的一个开源JDBC工具类库,它是对JDBC的简单封装。
1) QreryRunner类
它包含以下几个方法:
① query(Connection conn, String sql, Object[] params, ResultSetHandler rsh):执行选择查询,在查询中,对象阵列的值被用来作为查询的置换参数。
② query(String sql, Object[] params, ResultSetHandler rsh):方法本身不提供数据库连接,执行选择查询,在查询中,对象阵列的值被用来作为查询的置换参数。
③ query(Connection conn, String sql, ResultSetHandler rsh):执行无需参数的选择查询。
④ update(Connection conn, String sql, Object[] params):被用来执行插入、更新或删除(DML)操作。
示例代码如下:
//测试插入 ---删除和修改不一一测试了
@Test
public void testInsert(){
Connection conn = null; //创建Druid连接池连接
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="insert into customers(name,email,birth)values(?,?,?)";
int insertCount = runner.update(conn, sql, "鸡你太美", "ji@126,com", "1997-03-30");
System.out.println("添加了" +insertCount + "条记录");
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
2) ResultSetHandler接口
ResultSetHandler接口执行处理一个结果集对象,将数据转变并处理为任何一种形式,供其他应用使用。
ResultSetHandler是接口,负责处理两件事:
1)处理Statement执行后产生的结果集,生成结果列表
2)处理存储过程执行后的输出参数
提供了两个函数分别用来处理普通操作和存储过程的结果集
ArrayHandler:把结果集中的第一行数据转成对象数组。
ArrayListHandler:把结果集中的每一行数据都转成一个对象数组,再存放到List中。
BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。
BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。//重点
MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。//重点
MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List
ColumnListHandler:将结果集中某一列的数据存放到List中。
KeyedHandler(name):将结果集中的每一行数据都封装到一个Map里(List
示例代码如下:
//测试查询操作
//BeanHander:是ResultSetHandler接口的实现类,用于封装表中的一条记录
@Test
public void testQuery1(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select id,name,email,birth from customers where id=?";
//查询返回一个对象一条记录
BeanHandler<Customer> handler = new BeanHandler<>(Customer.class);
Customer customer = runner.query(conn, sql, handler, 23);
System.out.println(customer);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
//BeanListHander:是ResultSetHandler接口的实现类,用于封装表中的多条记录的集合
@Test
public void testQuery2(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select id,name,email,birth from customers where id";
//查询返回一个对象的多条记录
BeanListHandler<Customer> handler = new BeanListHandler<>(Customer.class);
List<Customer> customerList = runner.query(conn, sql, handler, 12);
customerList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
//MapHander:是ResultSetHandler接口的实现类,对应表中的一条记录
// 将字段及相应字段的值作为map中的key和value
@Test
public void testQuery3(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select id,name,email,birth from customers where id=?";
//查询返回一个以Map(键值对存储)形式的对象的一条记录
MapHandler handler = new MapHandler();
Map<String, Object> map = runner.query(conn, sql, handler, 23);
System.out.println(map);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
//MapListHander:是ResultSetHandler接口的实现类,对应表中的多条记录
// 将字段及相应字段的值作为map中的key和value,将这些map添加到list中
@Test
public void testQuery4(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select id,name,email,birth from customers where id";
//查询返回一个以Map(键值对存储)形式的对象的多条记录
MapListHandler handler = new MapListHandler();
List<Map<String, Object>> mapList = runner.query(conn, sql, handler, 12);
mapList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
//查询特殊特殊值的方法 - - -统计记录数
// ScalarHandler是ResultSetHandler接口的实现类
@Test
public void testQuery5(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select count(*) from customers";
ScalarHandler<Object> handler = new ScalarHandler<>();
Long count = (Long) runner.query(conn, sql, handler);
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
//查询特殊特殊值的方法 - - -查询最大生日
// ScalarHandler是ResultSetHandler接口的实现类
@Test
public void testQuery6(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select max(birth) from customers";
ScalarHandler<Object> handler = new ScalarHandler<>();
Date maxbirth = (Date) runner.query(conn, sql, handler);
System.out.println(maxbirth);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
注意:
可以自定义ResultSetHandler的实现类
通过匿名实现类去实现ResultSetHandler接口,重写内部的handle()方法,重写后方法的返回值就作为runner.query()方法的返回值。
示例代码如下:
//自定义ResultSetHandler的实现类
@Test
public void testQuery7(){
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils2.getConnection2();
String sql="select id,name,email,birth from customers where id=?";
//匿名实现类 ResultSetHandler是接口,以上的方法也是此接口的实现类
ResultSetHandler<Customer> handler=new ResultSetHandler<Customer>() {
@Override
public Customer handle(ResultSet rs) throws SQLException {
if (rs.next()){
int id=rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
Date birth = rs.getDate("birth");
Customer customer = new Customer(id, name, email, birth);
return customer;//其返回值就作为runner.query()方法的返回值
}
return null;
}
};
Customer customer = runner.query(conn, sql, handler, 23);
System.out.println(customer);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null,conn);
}
}
DbUtils.close()方法,会报异常,要自己try-catch。
DbUtils.closeQuietly()方法,已经在其方法内把异常处理了。
把代码写入JDBCUtils工具类中:
//使用dbutils.jar中提供的DbUtils工具类,实现资源的关闭
public static void closeResource1(Statement ps, Connection conn, ResultSet rs){
//方式一:close()报异常,自己try-catch
/* try {
DbUtils.close(conn);
DbUtils.close(ps);
DbUtils.close(rs);
} catch (SQLException throwables) {
throwables.printStackTrace();
}*/
//方式二:closeQuietly()已经在其方法内把异常处理了
DbUtils.closeQuietly(conn);
DbUtils.closeQuietly(ps);
DbUtils.closeQuietly(rs);
}