title: JDBC
JDBC
- 密码的密文存储:在存储用户密码时,为了保证数据的安全性,一般采用密文存储。
此处我们采用的是MD5加密,并且创建了一个MD5Util帮助类
public class MD5Util { public static String getMd5(String s) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } byte bs[] = md5.digest(s.getBytes()); StringBuffer sb2 = new StringBuffer(); //将每个字节转换为2个16进制字符串。 for(byte b:bs) { //字符串格式化函数,将一个对象格式化为字符串展示,x:16进制,2:长度为2 ,0不足两位用0充填 sb2.append( String.format("%02x", b) ); } return sb2.toString(); } }
JDK类库给我们提供了MessageDigest类来实现各种加密算法。
通过MessageDigest.getInstance(“md5”);来获取一个md5算法的加密类实例。
md5.digest(输入),根据输入获取加密后的的散列值,返回的散列值是一个长度为16的字节数组。
一般我们希望输入的是文本,加密后得到的也是问题,所以需要将16为的字节数组转换为固定长度的字符串。
根据二进制和字节的相关知识,一个字节是8个2进制位,而每4个2进制就可以转换为1个16进制。
所以我们可以将1个字节转换为2个16进制字符,而String.format函数就提供了该功能,将整数格式化为字符串。
execute函数与ResultSetMetaData
- execute功能
之前我们使用过executeUpdate语句来执行修改,返回值为整数;
使用executeQuery语句来执行查询,返回值为结果集ResultSet;
这里我们介绍一下execute函数的使用,当我们不确定sql语句到底是什么类型的时候,例如是依赖用户输入的一个sql,
那么我们可以使用execute函数。
无论是查询还是修改,甚至是DLL语句,都可以通过execute函数执行。
- execute返回值
该函数返回一个boolean类型数据,表示执行的sql语句是否有结果集。
例如,如果execute执行的是查询,返回值为true;如果执行的是修改,返回值为false。
当执行的是修改时,我们可以继续调用Statement的getUpdateCount函数来获取影响的行;
当执行的是查询时,我们可以继续调用Statement的getResultSet函数来获取结果集。
- ResultSetMetaData
结果集元数据,用来描述结果集中的信息,如返回的结果有多少列,每列的名字,数据类型等。
但是我们不能直接获取结果集中有多少行数据。
通过结果集ResultSet的getMetaData函数可以得到元数据对象。
- ResultSetMetaData使用场景
当我们的查询不确定,例如依赖用户输入时,那么我们就无法确定查询结果又多少列,
那么通过rs.getXXX(列编号)来获取某一行的数据时,我们就不知道列编号要写到几结束。
那么此时我们就可以通过ResultSetMetaData这个元数据对象来获取列编号有多少。
- 综合案例
public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; Statement st = null; ResultSet rs = null; try { conn = JDBCUtil.getConn(); st = conn.createStatement(); String sql = "select * from student";//此处可以是用户输入的任何sql String sql2 = "update student set age=20"; boolean b = st.execute(sql);//b表示sql的执行结果是否有结果集 //如果execute执行的是查询,b为true;如果执行的是修改,b为false if(!b) { int rows = st.getUpdateCount(); System.out.println("受影响的行数:"+rows); }else { rs = st.getResultSet(); //结果集的元数据metadata,描述结果集中有多少列,每列的名字是什么等信息 ResultSetMetaData md = rs.getMetaData(); int cc = md.getColumnCount();//从元数据中获取列数目 //打印列名 for(int i=1;i<=cc;i++) { System.out.print( md.getColumnName(i) +" "); } System.out.println("\n---------------------------"); //打印查询到的内容 while(rs.next()) { for(int i=1;i<=cc;i++) { System.out.print(rs.getString(i)+" "); } System.out.println(); } } }catch (SQLException e) { e.printStackTrace(); }finally { JDBCUtil.closeAll(rs, st, conn); } }
执行结果
jdbc制作基于控制台的sql交互模式
//基于控制台的SQl语句查询 public class OraToEcli { public static void main(String[] args) { Scanner sc = new Scanner(System.in); Connection conn = null; ResultSet rs = null; PreparedStatement pstm = null; Statement st = null; try { conn = JDBCUtil.getConnection(); //向数据库发送SQL语句 st = conn.createStatement(); System.out.println("成功连接数据库!!!!"); while(true) { System.out.print("SQL>"); //控制台输入的SQl语句 String sql = sc.nextLine(); pstm = conn.prepareStatement(sql); //定义循环出口 if(sql.equals("bye")) { System.out.println("数据库连接断开!"); break; } //若SQl语句是DML返回false //若SQl语句是DQL返回true //boolean flag = st.execute(sql); boolean temp = pstm.execute(); if(temp) {//DQL查询语句 //rs = st.getResultSet(); //获得结果集元数据 rs = pstm.getResultSet(); //调用返回结果的函数 returnResult(rs); }else {//DML操作语句 //得到操作的行数 int rows = pstm.getUpdateCount(); System.out.printf("有%d行操作成功",rows); System.out.println(rows); } } } catch (SQLException e) { System.out.println(e); }finally { JDBCUtil.closeAll(rs, pstm, conn); if(st != null) { try { st.close(); } catch (SQLException e) { // TODO Auto-generated catch block System.out.println(e); } } } } //返回结果集函数 static void returnResult(ResultSet rs) { try { //结果集的元数据metadata,描述结果集中有多少列,每列的名字 ResultSetMetaData md = rs.getMetaData(); //获取元数据中列的数目cc int cc = md.getColumnCount(); //打印列名 for(int i = 1;i<=cc;i++) { System.out.print(md.getColumnName(i)+" / "); } System.out.println(); System.out.println("----------------------------------"); //打印查询的内容 while(rs.next()) { //打印每一行中的每一列 for(int i = 1;i<=cc;i++) { System.out.print(rs.getString(i)+" / "); } System.out.println(); } } catch (SQLException e) { System.out.println(e); } } }
文件读取时路径问题
- 文本文件一般整行读取并处理,所以过程需要:
- 获取字节流InputStream
- 将字节流包装为字符流InputStreamReader
- 将字符流再包装为BufferReader,方便整行读取
- IO流读取时,文件路径的写法
Kt1.class.getResourceAsStream(“backup.txt”)
这种获取输入流的方式很好用,表示获取Kt1类相同包下的backup.txt文件的输入流,采用这种基于类编译路径的方式避免了从物理系统查找文件。
Kt1.class.getClassLoader().getResourceAsStream(文件名)
多了一个getClassLoader(),就表示从src根路径读取文件,而不是从Kt1当前包下。
数据库连接池
- 连接池作用
- 在jdbc操作数据库的一系列步骤中,获取数据库连接是第一步也是最消耗时间的一步。当系统启动时,我们的每个数据库操作都要经历创建连接和释放连接的步骤,这对系统的处理时间有很多影响。
- 解决问题的办法
- 在系统启动时提前创建好一些连接并维护起来(可以想象为将连接资源放入的一个池子中),当需要操作数据库时,就从池子中获取连接;当使用完后,就把连接放回池子,以供其他操作使用。这个过程就避免了连接的不断创建和释放,提升了性能。
- 基础使用
常用的数据库连接池
- DBCP和C3p0
代码
public static void main(String[] args) throws Exception { //加载配置文件 Properties pp = new Properties(); InputStream ins = Kt6.class.getResourceAsStream("jdbc.properties"); pp.load(ins); //创建连接池 DataSource ds = BasicDataSourceFactory.createDataSource(pp); //获取连接 Connection conn = ds.getConnection(); System.out.println(conn.getClass()); //并不是真正的关闭连接,而是将连接放回连接池 conn.close(); }
1 DBCP中数据库连接池对象为DataSource,也称为数据源,我们就可以通过该对象的getConnection方法来获取连接池的连接。
2 数据源的创建依赖配置信息,需要通过Properties对象加载配置文件,第3~5行代码,然后将该对象作为参数传入createDataSource方法来创建数据源
3 通过conn.close来关闭连接,在连接池中这并不真正的关闭连接,而是将连接资源归还连接池。
4 在jdbc.properties文件中指定连接池创建的一些必要信息和配置参数,文件内容示例如下:
//创建连接的必要信息 driverClassName=oracle.jdbc.driver.OracleDriver url=jdbc:oracle:thin:@localhost:1521:orcl username=hr password=hr initialSize=10 //初始连接池的大小 maxActive=20 //最大连接数 maxIdle=8 //最大空闲连接数,多于这个数,连接被释放 minIdle=2 //最小空闲,少于这个数,新建连接 maxWait=60000 //最大等待时间
- 封装数据库连接池工具类(基于DBCP连接池帮助类)
public class DBCPUtil { private static DataSource ds; //连接池只需要自动创建一次即可 static { Properties pp = new Properties(); InputStream ins = DBCPUtil.class.getClassLoader().getResourceAsStream("jdbc.properties"); try { pp.load(ins); ds = BasicDataSourceFactory.createDataSource(pp); } catch (Exception e) { e.printStackTrace(); } } //获取连接 public static Connection getConn() { try { return ds.getConnection(); } catch (SQLException e) { e.printStackTrace(); } return null; } //关闭连接 public static void closeAll(ResultSet rs,Statement st,Connection conn) { try { if(rs!=null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if(st!=null) st.close(); } catch (SQLException e) { e.printStackTrace(); } try { if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
核心编程思想
create table t_person(
id number(3) primary key,
name varchar2(30) ,
sex number(1),
age number(2),
mobile varchar2(11),
address varchar2(300)
)
向表中添加数据
- 数据库客户端操作数据库
- 登陆数据库
- 打开发送SQL的窗口
- 编写SQL语句
- 执行SQL
- 若是DQL,需要接收结果集
- 释放资源,关闭数据库
使用JDBC操作数据库
- 加载驱动
- 根据驱动连接数据库,获取数据库连接对象
- 根据数据库连接对象获取加载器
- 发送执行SQL
- 若是SQL查询语句,需要ResultSet rs结果集
- 释放资源
ResultSet结果集
- ResultSet:结果集,存放查询语句的查询结果(rs中存放的是select语句查询结果的字段名(别名),可能不是表中的原有字段名,调用get方法时,以别名为准)
动态参数
方式1:SQL字符串拼接
- 用法:将SQL需要的数据,通过字符串拼接的方式拼接到字符串SQL中
- 缺点:
- 操作繁琐,拼接麻烦
- SQL注入问题:在拼接SQL字符串时,可能拼接上一下奇奇怪怪的东西,造成数据库不安全的问题(数据丢失,数据删除…)
- 优点:可以拼接表名、字段名、关键字(desc asc)
public static boolean login(String username,String password) throws Exception {
Class.forName("oracle.jdbc.OracleDriver");
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe","hr","hr");
String sql = "select * from t_account where username = '"+username+"' and password = '"+password+"'";
System.out.println(sql);
PreparedStatement pstm = conn.prepareStatement(sql);
ResultSet rs = pstm.executeQuery();
boolean flag = rs.next();
rs.close();
pstm.close();
conn.close();
return flag;
}
请输入用户名:
abcd
请输入密码:
a'or'a'='a
select * from t_account where username = 'abcd' and password = 'a'or'a'='a'
方式2:pstm占位符
- 用法:在编写字符串SQL时,允许使用?对数据进行一个占位
- 特点
- ?只能给数据占位,不能给关键字,表名,字段名等占位
- 可以解决SQL注入问题
- 缺点:不能给关键字,表名,字段名等占位
- 特点
-
操作步骤
-
先写使用占位符的SQL
-
String sql = "select * from t_account where username = ?";
-
-
发送SQL
-
PreparedStatement pstm = conn.prepareStatement(sql);
-
-
绑定参数,将实际参数绑定到相应的占位符上
-
//给?赋值 pstm.setXxx(第几个?,实际的数据) pstm.setString(1,username); pstm.setString(2, password);
-
-
发送参数,并执行SQL
-
pstm.executeQuery();
-
-
总结
- 若做数据的动态处理,使用pstm占位符
- 若做关键字、字段名等动态处理,使用字符串拼接
批处理
- addBatch(),将参数保存在Java本地(JVM内存中)
- execuBatch(),将本地参数一次发送给数据库
class TestBatch {
public static void main(String[] args) throws Exception {
long t1 = System.nanoTime();
Class.forName("oracle.jdbc.OracleDriver");
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe","hr","hr");
//创建使用占位符的SQL
String sql = "insert into t_person values(person_seq.nextval,?,?,?,?,?)";
//发送SQL,并获取执行器
PreparedStatement pstm = conn.prepareStatement(sql);
for (int i = 1; i <=100000; i++) {
//给?绑定参数
pstm.setString(1,"haha");
pstm.setInt(2, 1);
pstm.setInt(3, 18);
pstm.setString(4, "123456");
pstm.setString(5, "硅谷广场14楼");
pstm.addBatch();//将参数保存在java本地
if(i%500==0) {
pstm.executeBatch();//将多组参数一次发送给数据库
}
}
//将本地可能剩余的参数发送给数据库
pstm.executeBatch();
//System.out.println(count);
//释放资源
pstm.close();
conn.close();
long t2 = System.nanoTime();
System.out.println((t2-t1)/1E9);
}
}
JDBCUtil工具类
-
作用:简化代码,减少代码冗余,提高代码可重用性
-
实现:抽取重复的代码,封装到一个方法当中
public class JDBCUtil {
private static Properties prop = new Properties();
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
//静态代码块减少加载配置文件的次数
static {
//获取配置文件的输入流
//获取类加载输入流,后面的文件夹路径是以src为根目录的
try (InputStream is = JDBCUtil.class.getResourceAsStream("/jdbc.properties");){
prop.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//加载驱动
//根据key获取集合中对应的value
Class.forName(prop.getProperty("driverClassName"));
}
//获取数据库连接
public static Connection getConnection() {
//获取线程空间中的数据
Connection conn = tl.get();
//对conn进行判断,若为null,则肯定是业务层在获取conn,新建conn,并将conn放入线程空间中
//若conn不为null,则肯定是Dao层在获取conn,此时可以直接return conn;
if(conn == null) {
try {
//获取数据库连接对象
conn = DriverManager.getConnection(prop.getProperty("url"),prop.getProperty("user"),prop.getProperty("password"));
tl.set(conn);
} catch (Exception e) {
throw new RuntimeException("数据库连接异常",e);
}
}
//返回数据库连接对象
return conn;
}
//释放数据库资源
public static void closeAll(ResultSet rs,PreparedStatement pstm,Connection conn) {
if(rs != null)
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException("释放ResultSet关闭异常",e);
}
if(pstm != null)
try {
pstm.close();
} catch (SQLException e) {
throw new RuntimeException("释放PreparedStatemend关闭异常",e);
}
if(conn != null)
try {
conn.close();
tl.remove();
} catch (SQLException e) {
throw new RuntimeException("释放Connection关闭异常",e);
}
}
public static void closeAll(PreparedStatement pstm,Connection conn) {
closeAll(null,pstm,conn);
}
}
项目实战设计模式
ORM
- 概念:Object Relational Mapping 将数据库中表中的一行数据映射成一个java对象
- 作用:符合java面向对象的编程思想,通过对象更加方便的管理数据
Dao
- 概念:Data Access Object 数据访问对象
- 作用:专门负责对数据库的增、删、改、查操作,里面的方法不是固定的,可以根据具体需求,自行修改
- 注意:为了满足职能单一原则,Dao类中每一个方法,都应该只对应一种操作
JDBC三层结构
1.视图层
-
作用:直接面向用户,负责与用户进行数据的交互(接收(Scanner)、展示(syso))
-
注意:暂时用Test测试类代替
2. 业务层–service
-
作用:程序给用户提供的服务(业务、功能);例如:注册、登陆、搜索、转账等等
-
注意:每一种功能,都应该对应业务层中的一个方法
-
业务层设计:
- 业务层接口:XxxService 例如:PersonService
- 业务层接口实现类:XxxServiceImpl 例如:PersonServiceImpl
-
注意:大部分情况,一个功能(业务方法)中需要调用多个Dao层方法,为了保证业务正常实现,必须在业务层进行事务控制
-
Connection conn数据库连接对象
conn.setAutoCommit(false); 开启事务
conn.commit(); 提交事务
conn.rollback(); 回滚事务
-
注意:为了统一管理,无论是否需要,业务层方法统统都需要加事务控制!!!
3.Dao层–dao
- 作用:唯一可以直接操作数据的模块!!每一个数据库操作(增删改查)都需要对应Dao层的一个方法
- Dao层设计:
- Dao层接口:XxxDao 例如:PersonDao
- Dao层接口实现类:XxxDaoImpl 例如:PersonDaoImpl
存在问题:事务控制失败!!
失败原因:Dao层中的数据库连接conn与业务层中数据库连接conn,不是同一个!!在业务层中的conn上做事务控制,无法控制到Dao层的conn上的SQL操作
解决办法:让Dao层和业务层使用同一个数据库连接对象conn
方法1:将业务层中conn当做参数传递个Dao层中每个需要使用的方法
方法2:业务层和Dao层都执行在主线程中!每个线程中都有一块儿独立的空间(线程空间TreadLocal).无论任何时候,都是业务层先拿到conn,当业务层需要conn时,创建一个新的conn,并放入线程空间中;当执行到Dao层代码,Dao层需要获取conn时,不再新建,而是从线程空间中获取,保证业务层和Dao层使用的是同一个conn;此时,Dao层中不能关闭conn,而应该在业务层中关闭!!!!!
ThreadLocal
-
概念:线程中一块儿独立的空间,可以用来存储一个变量(数据)
-
语法:ThreadLocal t1 = new ThreadLocal();
-
例如:ThreadLocal t1 = new ThreadLocal();
-
方法:
- set(T v); 往线程空间中存放数据
- T get(); 从线程空间中获取数据
- remove(); 删除线程空间中的数据
-
用在JDBC工具类中
总结
1.常见API
- Connection
- prepareStatement(sql) 获取执行器
- setAutoCommit(false); 开启事务
- close(); 释放conn资源
- PreparedStatement
- executeupdate(); 执行DML
- executeQuery(); 执行DQL
- addBatch(); 将多组数据缓存在java本地
- executeBatch(); 将本地缓存数据一次性提交给数据库
- setXxx(); 绑定参数
- close(); 释放资源
- ResultSet
- next() 使游标下移一行;若指向一行数据,则返回true,否则返回false
- getXxx() 获取当前列对应的字段或序列号的值
- close();
- ThreadLocal 线程空间
- set(T v) 将数据存放到线程空间中
- T get() 获取线程空间中的数据
- remove() 移除线程空间中的数据
2.三层结构
- view
- service
- dao
3.项目开发流程
- 建表
- 创建一个项目
- 在项目根目录下创建lib文件夹(和src同级),用来存放jar包
- Build Path
- 在src下导入配置文件
- 创建包结构:公司域名倒置.模块名
- com.baizhi.util 用来存放工具类
- com.baizhi.entity 实体类
- com.baizhi.dao dao接口
- com.baizhi.dao.impl dao接口实现类
- com.baizhi.service 业务层接口
- com.baizhi.service.impl 业务层接口实现类
- com.baizhi.test 测试类
4. 注意事项
- 所有的service方法都需要进行事务控制
- 在Dao层不要关闭conn
- 在service层一定要关闭conn
,则返回true,否则返回false
- getXxx() 获取当前列对应的字段或序列号的值
- close();
- ThreadLocal 线程空间
- set(T v) 将数据存放到线程空间中
- T get() 获取线程空间中的数据
- remove() 移除线程空间中的数据
2.三层结构
- view
- service
- dao
3.项目开发流程
- 建表
- 创建一个项目
- 在项目根目录下创建lib文件夹(和src同级),用来存放jar包
- Build Path
- 在src下导入配置文件
- 创建包结构:公司域名倒置.模块名
- com.baizhi.util 用来存放工具类
- com.baizhi.entity 实体类
- com.baizhi.dao dao接口
- com.baizhi.dao.impl dao接口实现类
- com.baizhi.service 业务层接口
- com.baizhi.service.impl 业务层接口实现类
- com.baizhi.test 测试类
4. 注意事项
- 所有的service方法都需要进行事务控制
- 在Dao层不要关闭conn
- 在service层一定要关闭conn
- 对包名、类名、接口名的定义尽量规范