四JDBC学习笔记
JDBC学习笔记
Java相关课程系列笔记之四
笔记内容说明
JDBC(范传奇老师主讲,占笔记内容100%);
目 录
一、 JDBC概述 1
1.1 什么是JDBC 1
1.2什么是驱动 1
1.3 SQL lite 1
1.4如何使用Java连接某种数据库 1
1.5连接数据库并操作 1
1.6连接数据库时常见的错误 1
二、 JDBC核心API 2
2.1 Connection 2
2.2 Statement 2
2.3 ResultSet 2
2.4 DriverManager 2
2.5 UUID 3
2.6案例:使用JDBC连接数据库,并操作SQL语句 3
2.7案例:通过JDBC创建表 4
2.8案例:使用JDBC向表中插入数据 4
2.9遍历Student_chang表 5
三、 JDBC核心API:PreparedStatement 6
3.1 Statement的缺点 6
3.2 PreparedStatement的优点 6
3.3 PreparedStatement的常用方法 6
3.4案例详见第五章StudentDAO类 6
四、 Connection封装 7
五、 DAO 8
5.1持久类封装 8
5.2 DAO层 8
5.3 Properties类 8
5.4案例:注册系统 8
六、 批处理 12
6.1批处理的优点 12
6.2 JDBC批处理API 12
6.3案例:详见8.4案例step7 12
七、 事务处理 13
7.1事务特性ACID 13
7.2 JDBC中对事务的支持(API) 13
八、 DAO事务封装 14
8.1 ThreadLocal原理 14
8.2原理图 14
8.3 ThreadLocal核心API 14
8.4案例:登录系统(使用ThreadLocal实现连接共享) 14
九、 分页查询 17
9.1分页查询的基本原理 17
9.2为何使用分页查询 17
9.3 Oracle分页查询SQL语句 17
9.4 MySQL分页查询SQL语句 17
9.5“假”分页 17
9.6案例:分页查询 18
一、JDBC概述
1.1 什么是JDBC
1)Java的设计者希望使用相同的方式访问不同的数据库。
2)JDBC是Java用于统一连接数据库并操作数据库的一组通用接口定义(即通过一系列接口定义了访问数据库的通用API)。
3)JDBC是连接数据库的规范,不同的数据库厂商若想让Java语言可以对其操作,就需要实现一组类,这组类需要实现Java提供的这组用于连接数据库的接口,并实现其中定义的相关方法。那么不同的数据库厂商根据各自数据库的特点,去提供对JDBC的实现(实现类包),那么这组类就是该数据库的驱动包了。
4)原理图:
1.2什么是驱动
简单的说就是让软件知道如何去操作硬件。
1.3 SQL lite
是轻量级的数据库,常用于嵌入式。
1.4如何使用Java连接某种数据库
需要两个部分:1)使用JDBC连接数据库(导入某数据库的.jar包)。
2)提供对该数据库的驱动包(使用静态方法Class.forName注册驱动)。
1.5连接数据库并操作
1)打开与数据库的连接(使用DriverManager.getConnection获取连接)。
2)执行SQL语句(使用Statement或者PreparedStatement)。
3)得到结果。
1.6连接数据库时常见的错误
1)报错ClassNotFoundException则有两种情况:
①驱动包没导入。
②Class.forName()中的字符串拼写有误。
2)报错port number,应注意:
①连接数据库时输入数据库路径时没有添加端口号。
②Oracle数据库的完整写法应为:jdbc:oracle:thin:@IP地址:端口号:数据库名
u 注意事项:Oracle数据库默认端口号1521。MySql数据库默认端口号为3306。
二、
JDBC核心API
2.1 Connection
接口,需导入java.sql.Connnection包,与特定数据库进行连接(会话)。
2.2 Statement
接口,需导入java.sql.Statement包,用于执行静态SQL语句并返回它所生成结果的对象。
1)ResultSet executeQuery(String sql) throws SQLException方法:执行给定的SQL语句(通常为静态SQL SELECT语句),该语句返回单个ResultSet对象。
2)boolean execute(String sql) throws SQLException方法:执行给定的SQL语句,该语句可能返回多个结果。如果第一个结果为ResultSet对象,则返回true;如果其为更新计数或者不存在任何结果,则返回false。详细介绍请看2.6案例注释。
3)int executeUpdate(String sql) throws SQLException方法:执行给定SQL语句,该语句可能为INSERT、UPDATE、DELETE(DML语句),或者不返回任何内容的DDL语句。返回值:①对于数据操作语句(DML语句),返回行计数。②对于DDL语句,返回0。
4)boolean execute(String sql)方法:返回结果为true、false,常用与执行表级操作的SQL语句,如建表、删表等,创建表若失败实际上是会直接抛出异常的。false:为建表成功的标志。
5)exectue()方法:原则上可以执行任意SQL语句。返回true:若执行结果为一个结果集(ResultSet)。返回false:为其他信息(如影响表数据总条数等)。所以我们通常不会使用execute去执行查询语句。
6)int executeUpdate(String sql) throws SQLException方法:返回值int,返回值为当前执行的SQL语句影响了数据库数据的总条数;该方法常用与执行insert、update、delete语句。
7)在底层一定会用到网络Socket和流,但我们不用关心使用字符还是字节接收,都由Statement做了。
2.3 ResultSet
接口,表示数据库结果集的数据表(很像一个集合),通常通过执行查询数据库的语句生成。
1)ResultSet特点:按行遍历,按字段取值。
2)它的next()方法包含了是否有下一条记录的hasnext()方法。
3)按字段取值时,getString(int)方法中的int,代表结果集的第几列,
u 注意事项:这里的int从1开始,和Java对索引的习惯不同。
2.4 DriverManager
它是管理一组JDBC驱动程序的类。
1)Connection getConnection(String url,String user,String password)方法:静态方法,建立与给定数据库URL的连接(DriverManager试图从已注册的JDBC驱动程序集中选择一个适当的驱动程序)。
2)DriverManager如何知道某种数据库已注册的?
例如:oracle.jdbc.driver.OracleDriver类在Class.forName()的时候被载入JVM;
而OracleDriver是JDBC中Driver的子类,它被要求在静态初始化的时候要将自身驱动的信息通过DriverManager的静态方法注册进去,这样DriverManager就知道应该如何通过OracleDriver去连接该数据库了。所以之后就可以通过DrvierManager的另一个静态方法:getConnection()来根据之前注册的驱动信息获取连接了:
Connection conn=DriverManager.getConnetion("","","");
2.5 UUID
UUID为通用唯一标识码(Universally Unique Indentifier)对于大数据量的表来说,UUID是存放ID最好的方式。
1)Java提供的支持(用法详见2.8案例)
UUID类:UUID.randomUUID().toString():获得一个36位不重复的字符串。
2)Oracle提供的支持(用法详见8.4案例step7)
函数sys_guid():获取一个32位不重复的字符串。
2.6案例:使用JDBC连接数据库,并操作SQL语句
/** 连接数据库一定要捕获异常的 */
Connection conn=null;//定义在try外面是用于在finally块中关闭它,同时局部变量在使用前,一定要初始化!!
try{ /** 与数据库进行连接分为两步:1)注册驱动:不同的数据库实现不尽相同,所以要使用不同数据库厂商提供的驱动包。连接不同数据库,传入的字符串不尽相同,但是目的相同,都是注册驱动。而对于驱动包路径,名字是固定的,基本上不会变的!2)根据数据库的位置(路径)以及用户名和密码进行连接 */
Class.forName("oracle.jdbc.driver.OracleDriver");
/** 路径:不同数据库连接的路径写法不尽相同,Oracle的写法: jdbc:oracle:thin:@HOST:DB_NAME
其中HOST包含两部分:IP地址和端口号;本机则使用localhost或127.0.0.1 */
conn=DriverManager.getConnection("jdbc:oracle:thin:@192.168.0.20:1521:tarena",
"jsd1304","jsd1304");
/** 使用SQL语句来操作数据库,若想执行SQL语句,我们需要使用一个专门处理SQL语句的类,这个类叫做Statement */
Statement state=conn.createStatement();
/** user_tables是Oracle用于存储当前用户创建的所有表的信息,其中一个字段叫做table_name用户保存的表名 */
String sql="SELECT table_name FROM user_tables";
/** 通过Statement执行查询语句,当查询完毕后,数据库会将查询结果返回,Statement会将查询结果存储到ResultSet中 */
ResultSet rs=state.executeQuery(sql);
while(rs.next()){ //按行遍历,包含了是否有下一条记录的方法hasnext()
/** 按字段取值;整数参数:结果集的第几列。注意:这里从1开始,和Java对索引的习惯不同 */
String tableName=rs.getString(1);
System.out.println(tableName);
}
/** 底层一定会用到网络socket和流,但我们不用关心使用字符还是字节接收,都由Statement做了 */
rs.close(); state.close();
}catch(Exception e){ e.printStackTrace();
}finally{ if(conn!=null){ try { conn.close(); } catch (SQLException e) {
e.printStackTrace(); } } }
u 注意事项: 养成良好的编码习惯:所有SQL关键字用纯大写,其他内容用纯小写 。
2.7案例:通过JDBC创建表
Connection conn=null;
try{ //1 注册驱动
Class.forName("oracle.jdbc.driver.OracleDriver");
//2 打开连接,支持import java.sql.*,但全导入较耗费性能
conn=DriverManager.getConnection(
"jdbc:oracle:thin:@192.168.0.20:1521:tarena","jsd1304","jsd1304");
//3 创建用于执行SQL语句的Statement
Statement state=conn.createStatement();
//创建建表语句
String sql="CREATE TABLE Student_chang(" +
"id varchar2(36) PRIMARY KEY," +"name varchar2(30)," +
"age number(2)," + "sex varchar2(2)" + ")";
//execute()方法详见2.2节
if(!state.execute(sql)){ System.out.println("创建表成功!");
}else{ System.out.println("创建失败!"); }
state.close();
}catch (Exception e){ e.printStackTrace();
}finally{ if(conn!=null){ try { conn.close(); } catch (SQLException e) {
e.printStackTrace(); } } }
2.8案例:使用JDBC向表中插入数据
Connection conn=null;
try{ Class.forName("oracle.jdbc.driver.OracleDriver");
conn=DriverManager.getConnection(
"jdbc:oracle:thin:@192.168.0.20:1521:tarena","jsd1304","jsd1304");
Statement state=conn.createStatement();
//UUID详见2.5
String uuid=UUID.randomUUID().toString(); System.out.println(uuid);
String sql="INSERT INTO Student_chang VALUES('"+uuid +"','Chang',22,'1')";
//或String sql="INSERT INTO Student_chang VALUES(sys_guid(),'chang',23,'1')";
//判断insert语句是否成功,看返回值是否大于0,executeUpdate方法详见2.2
if(state.executeUpdate(sql)>0){ System.out.println("插入数据成功"); }
state.close();
}catch(Exception e){ e.printStackTrace();
}finally{ if(conn!=null){ try { conn.close(); } catch (SQLException e) {
e.printStackTrace(); } } }
2.9遍历Student_chang表
Connection conn=null;
try{ Class.forName("oracle.jdbc.driver.OracleDriver");
conn=DriverManager.getConnection(
"jdbc:oracle:thin:@192.168.0.20:1521:tarena","jsd1304","jsd1304");
Statement state=conn.createStatement();
String sql="SELECT * FROM Student_chang";
ResultSet rs=state.executeQuery(sql);
while(rs.next()){ String id=rs.getString(1);
String name=rs.getString("name");//不知第几列也可写列名
int age=rs.getInt("age");
String sex=rs.getString(4).equals("1")?"男":"女";
System.out.println(id+","+name+","+age+","+sex); }
rs.close(); state.close();
}catch(Exception e){ e.printStackTrace();
}finally{ if(conn!=null){ try { conn.close(); } catch (SQLException e) {
e.printStackTrace(); } } }
三、
JDBC核心API:PreparedStatement
3.1 Statement的缺点
1)用Statement操作时代码的可读性和可维护性差,编写SQL语句复杂。
2)Statement操作SQL语句,每执行一次都要对传入的语句编译一次,效率比较差。
3)不安全可能出现SQL注入攻击,详见9.6案例step3。
4)扩展:XSS攻击、html代码注入攻击、struts2 OGNL存在可以远程执行底层操作系统命令的漏洞。
3.2 PreparedStatement的优点
1)PreparedStatement实例包含已编译的SQL语句。包含于PreparedStatement对象中的SQL语句可具有一个或多个IN参数。IN参数的值在SQL语句创建时未被指定。该语句为每个IN参数保留一个问号(“?”)作为占位符,不考虑类型。每个问号的值必须在该语句执行之前,通过适当的setString、setInt、setDouble……等方法来提供。
2)由于PreparedStatement对象已预编译过,所以其执行速度要快于Statement对象。因此,多次执行的SQL语句经常创建为PreparedStatement对象,以提高效率。
3)PreparedStatement继承于Statement,其中三种方法:execute、executeQuery、executeUpdate都已被更改为不再需要参数了。因为我们在获取PreparedStatement时已经将SQL语句传入了。所以执行就可以,不需要再传入SQL。
4)PreparedStatement可以进行批量处理。
5)可以防止SQL注入攻击。
u 注意事项:
v 使用预编译语句,你传入的任何内容就不会和原来的语句发生任何匹配的关系,只要全使用预编译语句,你就不用对传入的数据作任何的过滤。
v 对一个表只作一个操作用PreparedStatement,效率高、方便
v 对表进行2种及以上的操作用Statement。
3.3 PreparedStatement的常用方法
1)boolean execute():可以是任何种类的SQL语句。一些预处理过的语句返回多个结果,execute 方法处理这些复杂的语句,executeQuery 和 executeUpdate 处理形式更简单的语句。如果第一个结果是ResultSet对象,则返回true;如果第一个结果是更新计数或者没有结果,则返回false。
2)ResultSet executeQuery():在此PreparedStatement对象中执行SQL查询,并返回该查询生成的ResultSet对象。
3)int executeUpdate():在此PreparedStatement对象中执行SQL语句,该语句必须是一个DML语句,比如INSERT、UPDATE或DELETE语句;或者是无返回内容的语句,比如DDL语句。
4)void addBatch():将一组参数添加到此PreparedStatement对象的批处理命令中。其他批处理方法,见第六章。
5)void setObject(int parameterIndex, Object x):第一个参数用于设置“?”中的值,且从1开始!该方法可将任意给定的参数类型转换为所对应的SQL类型。类似setInt、setString等。
3.4案例详见第五章StudentDAO类
四、
Connection封装
可建立一个DBUtils类,用于对数据库连接的封装。
private static String driver; private static String url;
private static String user; private static String password;
static {//此处详细说明见5.4案例step2
try { Properties props = new Properties();//Properties类详见5.3
props.load(DBUtils.class.getClassLoader().getResourceAsStream(
"db.properties"));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
Class.forName(driver);//注意这里没有引号!
} catch (IOException e) { e.printStackTrace();
} catch (ClassNotFoundException e) { e.printStackTrace(); }
}
/**封装连接操作,供其他类继承,再次使用数据库连接的时候,可以直接调用openConnection获取数据连接,其中url、user、pwd通过上面的.properties文件加载 */
protected static Connection getConnection() throws SQLException{
return DriverManager.getConnection(url,user,pwd);
}
/** 封装关闭操作,供其他类继承,关闭连接时调用该方法 */
protected static void closeConnection(Connection conn){
if(conn!=null){
try{ conn.close(); } catch (SQLExcption e) { }
}//catch块内容可不写,即所谓的“安静”的关闭连接
}
五、
DAO
5.1持久类封装
对象关系映射(ORM)使用描述对象和数据库之间映射的元数据,将Java程序中的对象自动持久化到关系数据库中。1)表和类对应。2)表中的字段和类的属性对应。3)记录和对象对应。
5.2 DAO层
1)DAO:数据连接对象(DataAccessObjects)
2)作用:将数据库中的数据转化为Java的对象并返回(即读数据),将Java的对象转化为数据库中表的一条数据(即写数据)。
3)Java对象在这里就是所谓的实体entity,DAO要达到的目的:对数据库数据的操作面向对象化。
4)实体:用Java中的对象去描述数据库中的某表中的某一条记录。
比如:Student表有字段id、name、age、sex,则对应的Java类中有Student类,属性有id、name、age、sex
5)实体类:用于对应数据库中的表。通常实体类的名字和数据库中表的名字一致。
u 注意事项:实体类代表表,属性代表字段,对象代表一条数据。
5.3 Properties类
用于读取“.properties”文本文件的类,导入java.util.Properties包。
1)“.properties”文件是一个纯文本文件,里面定义的内容格式有要求,必须是key=value的形式,并且以行为单位。一行只记录一条数据!
2)Properties类可以方便的读取properties文件,并将内容以类似HashMap的形式进行读取。
3)db.properties文件里的内容如下:
jdbc.driver=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@192.168.0.20:1521:tarena
jdbc.user=jsd1304
jdbc.pwd=jsd1304
u 注意事项:读取的都是字符串!不用写双引号,无空格!
4)getProperty(String key)方法:该方法可以从properties文件中获取数据,如:jdbc.driver=oracle.jdbc.driver.OracleDriver。获取方式是将jdbc.driver以key作为参数调用方法。返回的就是等号右面的值oracle.jdbc.driver.OracleDriver了。
5.4案例:注册系统
step1:Student实体类
public class Student implements Serializable{
private static final long seriaLversionUID=1L;
private String id; private String name; private int age; private String sex;
……各自的get/set方法 }
u 注意事项:该类描述数据库中Student表,其每一个实例都可以代表Student表的一行数据。通常情况下实体都是可以序列化的!
step2:BaseDAO父类(基础类,提供所有DAO都需要具备的特性)
private static Properties properties=new Properties();
private static String driver="";//"oracle.jdbc.driver.OracleDriver";
private static String url="";//"jdbc:oracle:thin:@192.168.0.20:1521:tarena";
private static String user="";//"jsd1304";
private static String pwd="";//"jsd1304";
/** 在静态初始化中注册驱动,驱动不需要重复注册,所以静态初始化最合适注册驱动 */
static{ try { /** 加载配置文件,读取配置信息 db.properties中的内容见5.3 */
properties.load(BaseDAO.class.getClassLoader()
.getResourceAsStream("day01pm/dao/db.properties"));
System.out.println(properties.getProperty("jdbc.driver"));//可输出检查一下
/** 获取值 */
driver=properties.getProperty("jdbc.driver");
url=properties.getProperty("jdbc.url");
user=properties.getProperty("jdbc.user");
pwd=properties.getProperty("jdbc.pwd");
//加载驱动,并注册到DriverManager,详见2.4
Class.forName(driver);//反射机制
} catch (Exception e) { e.printStackTrace();
throw new RuntimeException(e);//若注册失败,我们要通知调用者 }
}
/** 获取数据库连接对象Connection 让外界去捕获异常,连接不上数据库该怎么办?*/
protected static Connection getConnection() throws SQLException{
return DriverManager.getConnection(url,user,pwd); }
/** 将给定的数据库连接关闭 */
protected static void closeConnection(Connection conn){
if(conn!=null){ try { conn.close(); } catch (SQLException e) {//catch块可不写内容
e.printStackTrace(); } } }
step3:StudentDAO类(用于操作数据库Student表)并继承BaseDAO
public Student findStudentByName(String name){
Connection conn=null;
try{// Class.forName("oracle.jdbc.driver.OracleDriver");
// conn=DriverManager.getConnection(
// "jdbc:oracle:thin:@192.168.0.20:1521:tarena","jsd1304","jsd1304");
//通过父类BaseDAO的getConnection()方法获取数据库连接
conn=getConnection();
// Statement state=conn.createStatement();//使用Statement
// String sql="SELECT * FROM student_chang WHERE name='"+name+"'";
String sql="SELECT * FROM student_chang WHERE name=?";
PreparedStatement state=conn.prepareStatement(sql);//使用PreparedStatement
state.setString(1, name);//给第一个问号赋值
/** 根据用户名查询该用户信息,并将这条数据转化为一个Student对象并返回 */
// ResultSet rs=state.executeQuery(sql);//使用Statement时
ResultSet rs=state.executeQuery();//使用PreparedStatement时,不需要参数了
if(rs.next()){ Student student=new Student(); student.setId(rs.getString("id"));
student.setName(rs.getString("name")); student.setAge(rs.getInt("age"));
student.setSex(rs.getString("sex")); return student; }
}catch(Exception e){ e.printStackTrace();
}finally{
// if(conn!=null){ try { conn.close(); } catch (SQLException e) {
// e.printStackTrace(); } }
//调用父类的关闭连接方法
closeConnection(conn); }
return null; }
/** 持久化Student对象,即将Student对象的数据保存到数据库中 */
public boolean saveStudent(Student student){
Connection conn=null;
try{ // Class.forName("oracle.jdbc.driver.OracleDriver");
// conn=DriverManager.getConnection(
// "jdbc:oracle:thin:@192.168.0.20:1521:tarena","jsd1304","jsd1304");
//通过父类BaseDAO的getConnection()方法获取数据库连接
conn=getConnection();
// Statement state=conn.createStatement();
// String sql="INSERT INTO student_chang VALUES(" +
// "sys_guid()," +
// "'"+student.getName()+"',"+
// student.getAge()+","+
// "'"+student.getSex()+"'"+
// ")";
/** 预编译的SQL,将SQL中变化的内容用“?”代替,然后通过PreparedStatement执行该SQL时用给定的参数代替?,来达到插入不同数据的目的,预编译SQL更像是一个格式或者模版 */
String sql="INSERT INTO student VALUES(sys_guid(),?,?,?)";//问号上不考虑类型,就是占位
PreparedStatement state=conn.prepareStatement(sql);
state.setString(1, student.getName());//将第一个问号替换为学生的姓名
state.setInt(2, student.getAge());
state.setString(3, student.getSex());
if(state.executeUpdate()>0){ return true; }//使用PreparedStatement
// if(state.executeUpdate(sql)>0){ return true; }//使用Statement
}catch(Exception e){ e.printStackTrace();
}finally{
// if(conn!=null){ try { conn.close(); } catch (SQLException e) {
// e.printStackTrace(); } }
//调用父类的关闭连接方法
closeConnection(conn); }
return false; }
step4:StudentService类(业务逻辑类)
private StudentDAO studentDAO=new StudentDAO();
public void reg(String name,int age,String sex){
/** 必要的验证 */
if(name==null||"".equals(name)){ System.out.println("名字不能为空!");
}else if(age<1||age>99){ System.out.println("年龄只能在1-99之间!");
}else if(!"1".equals(sex)&&!"0".equals(sex)){System.out.println("性别只能为0或1!");
}else if(studentDAO.findStudentByName(name)!=null){
System.out.println("该用户已存在!");
}else{//1 将用户输入的信息转化为一个Student对象
Student student=new Student(); student.setName(name);
student.setAge(age); student.setSex(sex);
//2 将该对象交给DAO进行持久化 3根据保存结果通知用户
if(studentDAO.saveStudent(student)){ System.out.println("注册成功!");
}else{ System.out.println("注册失败!"); } } }
public void findStudentByName(String name){
//对用户输入的信息进行必要的判断,null和空字符串
if(name!=null && !"".equals(name)){
//向DAO获取学生信息
Student student=studentDAO.findStudentByName(name);
if(student!=null){
System.out.println("学生:"+student.getName()+"年龄:"+student.getAge());
}else{ System.out.println("查无此人!"); } } }
step5:测试类
String studentName="chang";
StudentService service=new StudentService();
service.findStudentByName(studentName);
String studentName1="changyb";
int age=16;
String sex="1";
StudentService service1=new StudentService();
service1.reg(studentName1, age, sex);
六、
批处理
6.1批处理的优点
一个批处理是被发送到数据库以作为单个单元执行的一组更新语句。这降低了应用程序和数据库之间的网络调用。相比单个SQL语句的处理,处理一个批处理中的多个SQL语句是一种更为有效的方式。
6.2 JDBC批处理API
1)addBatch(String sql):Statement类的方法,“多次调用”该方法可以将给定的“多条”SQL语句添加到Statement对象的命令列表中缓存(每调用一次缓存一条SQL)。
2)addBatch():PreparedStatement类的方法,“多次调用”该方法可以将“多条”预编译的SQL语句添加到PreparedStatement对象的命令列表中缓存(每调用一次缓存一条SQL)。
3)executeBatch():把Statement对象或PreparedStatement对象命令列表中缓存的所有SQL语句一次性提交给数据库进行处理。这样可以有效的减少网络通信带来的性能消耗。
4)clearBatch():清空当前SQL命令列表中缓存的所有SQL语句。
6.3案例:详见8.4案例step7
七、
事务处理
7.1事务特性ACID
1)原子性(atomicity):事务必须是原子工作单元;对其数据修改,要么全都执行,要么全都不执行。
2)一致性(consistency):事务在完成时,必须使所有的数据都保持一致状态。
3)隔离性(isolation):由并发事务所作的修改必须与任何其他并发事务所作的修改隔离。
4)持久性(durability):事务完成之后,它对于系统的影响是永久性的。
7.2 JDBC中对事务的支持(API)
1)JDBC默认是每执行一条SQL语句都会提交事务。这对于批量处理数据来说性能开效果大。而且不满足事务本该管理的原则。
2)conn.setAutoCommit(false);//不自动提交
3)conn.commit();//提交事务
4)conn.rollback();//回滚事务
八、
DAO事务封装
8.1 ThreadLocal原理
1)Java提供了一个类ThreadLocal:用于实现线程内的数据共享,即对于相同的程序代码,多个模块(方法)在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
2)数据共享是根据线程区分的。不同线程间不共享数据。
3)原理分析:
ThreadLocal内部很简单:就是维护者一个HashMap,其中key存放的是每一个线程,value存放这个线程在不同模块间要共享的数据。每一个ThreadLocal实例,只能保存一个线程的一个共享数据,不同线程看到的应该是同一个ThreadLocal实例,但是获取的是不同的数据,因为线程间不共享。每个线程在不同模块中共享数据。比如:BaseDAO中,getConnection()、commin()、rollback()、closeConnection()这些方法,都共享同一个Connection实例。
HashMap hashMap = new HashMap();
void set(Object obj){ hashMap.put(Thread.currentThread(),obj); }
Object get(){ return hashMap.get(Thread.currentThread()); }
4)多线程共享数据:可用单例模式,但要加锁。
8.2原理图
8.3 ThreadLocal核心API
1)public T get():用于获取线程共享的数据。
2)public void set(T value):用于保存线程共享的数据。
3)public void remove():移除线程中保存的数据。
u 注意事项:使用remove()则key也没了,使用set(null)则key还保留。
8.4案例:登录系统(使用ThreadLocal实现连接共享)
step1:添加实体类UserInfo,并设置相应的get/set方法
private static final long serialVersionUID=1L;
private String id; private String name; private String password;
private int age; private String sex; private String email;
step2:修改5.2案例中的BaseDAO中的getConnection()方法
protected static Connection getConnection() throws SQLException{
/** 当一个线程调用该方法要获取连接时,我们先检查之前这个线程是否已经获取过一个连接了,若有就不再创建了 */
Connection conn=localConn.get();//get用于获取线程共享的数据
if(conn==null){//若是空的,说明这个线程第一次获取连接
//创建和数据库的连接
conn=DriverManager.getConnection(url,user,pwd);
//将创建出来的连接放入线程共享中,set方法用于保存线程共享的数据
localConn.set(conn); }
return conn; }
step3:修改5.2案例中的BaseDAO中的closeConnection()方法
protected static void closeConnection(){
try{ Connection conn=localConn.get();
if(conn!=null){ conn.close();//关闭连接
//连接关闭后,这个连接就没有存在的意义了,应该从线程共享中将其删除!
localConn.remove();//key也没了,或者localConn.set(null);key还保留
}
}catch(Exception e){ e.printStackTrace(); } }
step4:添加begin方法:开始事务
protected static void begin(){
try{ Connection conn=localConn.get();
if(conn!=null){ conn.setAutoCommit(false);//取消事务的自动提交 }
}catch(Exception e){ e.printStackTrace(); } }
step5:添加commit方法:提交数据到数据库。
protected static void commit(){
/** 先去线程共享中看看当前调用该方法的线程是否有共享过Connection,若有,就提交事务。不需要UserInfoDAO再告诉我提交哪个conn,即不用再传参数 */
Connection conn=localConn.get();
if(conn!=null){ try { conn.commit(); } catch (SQLException e) {
e.printStackTrace(); } } }
step6:添加rollback方法,回滚事务。
protected static void rollback(){
/** 不需要UserInfoDAO再告诉我回滚哪个conn,即不用再传参数 */
Connection conn=localConn.get();
if(conn!=null){ try { conn.rollback(); } catch (SQLException e) {
e.printStackTrace(); } } }
step7:添加UserInfoDAO类,并继承BaseDAO
private static final String INSERT="INSERT INTO userinfo_chang(" +
"id,name,password,age,sex,email) VALUES(sys_guid(),?,?,?,?,?)";
/** 保存给定的所有用户信息 */
public boolean save(List<UserInfo> userInfos){
Connection conn=null;
try{ conn=getConnection(); conn.setAutoCommit(false);//禁止自动提交
PreparedStatement state=conn.prepareStatement(INSERT);
long start=System.currentTimeMillis();
// for(int i=0;i<50;i++){//可快速插入50条记录
// state.setString(1,"test"+ i); state.setString(2,"12345"+ i);
// state.setInt(3,22); state.setString(4,"1");
// state.setString(5,"test"+ i +"@mail.com");
// //state.executeUpdate();//每执行一次都提交一次事务,涉及到硬件的写操作
// state.addBatch();//而Statement需要把sql语句写入参数列表
// }
for(UserInfo userinfo:userInfos){//读取集合中的记录,并存入数据库,集合不能为空,为空则不执行了。
state.setString(1,userinfo.getName()); state.setString(2,userinfo.getPassword());
state.setInt(3,userinfo.getAge()); state.setString(4,userinfo.getSex());
state.setString(5,userinfo.getEmail());
//state.executeUpdate();//每执行一次都提交一次事务,涉及到硬件的写操作
state.addBatch();//而Statement需要把sql语句写入参数列表
}
state.executeBatch();//批处理执行之前缓存的所有SQL
//conn.commit();
commit(); long end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒");
return true;//返回true,告知调用者保存成功
}catch(Exception e){
e.printStackTrace();
// if(conn!=null){//若执行保存过程中出错,要回滚事务
// //conn.rollback(); }
rollback();
}finally{ //closeConnection(conn);
closeConnection(); }
return false; }
/** 创建表 */
public void createTable(){
Connection conn=null;
try{ conn=getConnection();
Statement state=conn.createStatement();
String sql="CREATE TABLE userinfo_chang(" +
"id VARCHAR2(36) PRIMARY KEY,name VARCHAR2(30)," +
"password VARCHAR2(50),age NUMBER(2)," +
"sex VARCHAR2(2),email VARCHAR2(50))";
if(!state.execute(sql)){ System.out.println("创建完毕"); }
} catch(Exception e){ e.printStackTrace();
}finally{ if(conn!=null){ closeConnection(conn); } } }
九、
分页查询
9.1分页查询的基本原理
Connection conn = getConnection();
PreparedStatement state= conn.prepareStatement(sql);
int start = rowsPerPage * (page -1)+1;//每页的开始
int end = start + rowsPerPage;//每页的结束
state.setInt(1,end); state.setInt(2,start);
ResultSet rs =state.executeQuery();
List<Service> list = new ArrayList<Service>();
while(rs.next()){ list.add(toService(rs)); }//把每条数据转成对象后添加
9.2为何使用分页查询
1)每次只向数据库要求一页的数据量,频繁访问数据库,内存压力小,适合大数据量。
2)数据库中的表可能会存储若干数据,若我们一次性将所有数据获取显然是不理智的。这可能产生很多坏处,比如占用内存过大,而对于用户而言,数据过于多也不利于查看等。为此,我们可以将数据分批次的检索出来。既可以节省资源,也利于用户查看。不同数据库对分页支持的SQL语句不尽相同。所以,使用不同的数据库,我们要适应当前数据库对分页语句的定义。当然,hibernate屏蔽了数据库分页的差异。可以让我们很方便的使用统一方式进行分页查询。
9.3 Oracle分页查询SQL语句
1)Oracle中的分页使用了一个字段rownum,使用该字段对查询的行数进行限制,从而达到获取某一区间的数据。实现分页查询。
2)rownum它是oracle系统顺序分配的查询返回的行的编号,查询到第一条数据rownum返回第一行分配的行号1,查询到第二条数据rownum返回第二行分配的行号2,依此类推。也就是说行号是查询到数据后才产生的。
3)起始、结束位置计算:
每页的起始位置start=每页显示的记录数rows ×(当前要请求的页数page-1)+1
每页的结束位置end=每页的起始位置start + 每页显示的记录数rows(或end=page * rows 含头不含尾 java的一种习惯,不是必须的算法)。比如:
select
id,account_id,host,user_name,login_password,status,create_date,pause_date,close_date,cost_id
from (select id,account_id,host,user_name,login_password,status,create_date,pause_date,
close_date,cost_id,rownum r from service where rownum < ?) where r >=?
u 注意事项:子查询不能写 * ,写了不会有rownum,记得给rownum起列别名!
9.4 MySQL分页查询SQL语句
select * from table limit start,pageSize
u 注意事项:start从0开始,pageSize为每页的条数。
9.5“假”分页
1)一次性把数据全部取出来放在缓存中,根据用户要看的页数(page)和每页记录数(pageSize),计算把哪些数据输出显示。
2)只访问数据库一次,第一次取数比较慢,以后每页都从缓存中取,比较快。
3)比较适合小数据量,如果数据量大,对内存压力比较大。
4)一次性将数据库数据读入结果集,每次查看指定的页时,要求结果集的指针能够跳到指定的行,即指针能够跳到整个结果集的任一位置。
9.6案例:分页查询
step1:在8.4案例中step7的UserInfoDAO类添加分页查询方法和根据用户名、密码获取用户信息方法
/** 分页查询用户信息。page:第几页。rows:每页显示的条数 */
public List<UserInfo> findPaging(int page,int rows){
try{/** 根据页数和每页显示的条数,计算起始行号和结束行号 */
int start=(page-1)*rows+1;
int end=page*rows;//含头不含尾 java的一种习惯,不是必须的算法
/** 执行分页查询的Oracle的SQL语句,这里我们要进行两次查询,第一次先将后区间定位,第二次再将前区间定位。从而获取区间中的数据。 */
//子查询不能写 * ,写了不会有rownum,注意 r 后有一个空格
String sql="SELECT * FROM " +
"(SELECT id,name,password,age,sex,email,rownum r " +
"FROM userinfo_chang WHERE rownum < ?) WHERE r >=? ";
//获取预编译Statement
//Connection conn=getConnection();
//PreparedStatement state=conn.prepareStatement(sql);
//现在不需要关conn了,由BaseDAO去关(统一线程数据共享)
PreparedStatement state=getConnection().prepareStatement(sql);//上面二合一
state.setInt(1, end);//设置后区间(第一个问号)
state.setInt(2, start);//设置前区间(第二个问号)
ResultSet rs=state.executeQuery();//获取结果集
ArrayList<UserInfo> userinfos=new ArrayList<UserInfo>();
/** 这里我们可以在循环外面创建一个引用变量,从而节省循环带来的不必要的内存开销。但是绝对不能在循环外面创建一个对象,在循环里重复设置内容,否则看到的效果就是虽然查询出来了若干数据,但是集合中保存的却是最后一条数据,而且保存了若干次而已(所有引用指向最后一个对象,所以内容相同且为最后一条数据的)。 */
UserInfo userinfo=null;
while(rs.next()){//引用变量可同一个,但对象不能同一个(即定义在循环外)
userinfo=new UserInfo(); userinfo.setId(rs.getString("id"));
userinfo.setName(rs.getString("name"));
userinfo.setPassword(rs.getString("password"));
userinfo.setAge(rs.getInt("age")); userinfo.setSex(rs.getString("sex"));
userinfo.setEmail(rs.getString("email")); userinfos.add(userinfo); }
rs.close(); state.close(); return userinfos;
}catch(Exception e){ e.printStackTrace(); throw new RuntimeException(e);
}finally{ closeConnection(); }
//return null; }
//当catch块写了throw new RuntimeException(e);就不用写return语句了,因为执行不到
/** 根据用户名、密码获取用户信息 */
public UserInfo findUserInfoByNameAndPwd(String name,String password){
try{ String sql="SELECT * FROM userinfo_chang " +
"WHERE name='"+name+"' AND password='"+password+"' ";
Statement state=getConnection().createStatement();
ResultSet rs=state.executeQuery(sql); UserInfo userinfo=null;
if(rs.next()){ userinfo =new UserInfo(); userinfo.setId(rs.getString("id"));
userinfo.setName(rs.getString("name"));
userinfo.setPassword(rs.getString("password"));
userinfo.setAge(rs.getInt("age")); userinfo.setSex(rs.getString("sex"));
userinfo.setEmail(rs.getString("email")); }
rs.close(); state.close(); return userinfo;
}catch(Exception e){ e.printStackTrace(); throw new RuntimeException(e);
}finally{ closeConnection(); } }
step2:main方法测试
public static void main(String[] args){
UserInfoDAO dao=new UserInfoDAO();
// dao.createTable();//建表
// dao.save(new ArrayList());//快速插入数据
List<UserInfo> list=dao.findPaging(2, 10);
for(UserInfo user:list){ System.out.println(user.getName()); } }
step3:main方法添加如下语句,测试SQL注入
/** SQL注入攻击:对于SQL语句"SELECT * FROM userinfo WHERE name='chang' AND password='123456'"
把密码修改为当前形式:AND password='1234' OR '1'='1' 即拼写部分为:
1234' OR '1'='1 呢?
(第2个1最后无单引号,拼的字符串里有,第一个密码同理)则发现也可登录成功! */
// UserInfo userinfo=new UserInfo();//添加用户
// userinfo.setName("chang"); userinfo.setPassword("123456");
// List<UserInfo> list=new ArrayList<UserInfo>();
// list.add(userinfo); dao.save(list);
UserInfo user=dao.findUserInfoByNameAndPwd("chang","123456");
//dao.findUserInfoByNameAndPwd("chang", "1234' OR '1'='1");//SQL注入攻击,则发现也可登录成功!
if(user!=null){ System.out.println("欢迎你:"+user.getName());
}else{ System.out.println("登录失败!"); }

浙公网安备 33010602011771号