jdbc
一、jdbc的简介:
Sun公司参考了ODBC方案,制定了一组专门为java语言连接数据库的通用接口JDBC。方便了java开发人员,
开发人员不需要考虑特定的数据库的DBMS。JDBC不直接依赖于DBMS,而是通过驱动程序将sql语句转发给
DBMS,由DBMS进行解析并执行,处理结果返回。
jdbc的原理
jdbc中常用的接口和方法
名称 | 类型 | 作用 | 方法(主要使用) |
---|---|---|---|
DriverManager | 方法 | 连接数据库,返回Connection对象 | getConnection(String url,String user,String password) |
Connection | 接口 | 获取Statement对象 | createStatement() |
Statement | 接口 | 发送SQL语句 | execute(String sql) :通常用于DDL executeUpdate(String sql):通常用于DML executeQuery(String sql):通常用于DQL |
ResultSet | 接口 | 封装了DQL的返回值 | geXXX(int index) XXX表示需要的属性类型如:int,string getXXX(String columnLabel) |
二、jdbc的入门案例:
案例一、查询操作
public class Test1 {
public static void main(String[] args) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
// 一、利用反射加载驱动类
Class.forName("com.mysql.jdbc.Driver");
// 二、获取;连接对象
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/abdc","root","123456");
// 三、获取SQL执行的Statement对象
stat = conn.createStatement();
// 四、发送SQL语句,执行DQL语句,调用execueQuery方法
rs = stat.executeQuery("select * from emp");
while(rs.next()) {
// 获取查询的数据
int empno = rs.getInt(1);
String ename = rs.getString("ename");
// 打印查询到的语句
System.out.println(empno + "\t" + ename);
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
// 关闭连接
rs.close();
stat.close();
conn.close();
}
}
}
案例二、删除操作
public class Test2 {
public static void main(String[] args) {
try {
// 第一步:利用反射机制,加载mysql的驱动类型 com.mysql.jdbc.Driver到内存中
Class.forName("com.mysql.jdbc.Driver");
// 第二步:获取连接对象
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/abdc","root","123456");
// 第三步:获取SQL语句的执行对象Statement
Statement stat = conn.createStatement();
// 第四步:发送SQL语句
String sql = "delete from emp where empno = 1000";
// 调用执行DML操作的方法 executeUpdate(String sql)
int num = stat.executeUpdate(sql);
// 打印一下几条数据受到影响
System.out.println("受影响的条数为:" + num);
stat.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
批处理问题(一般只用于添加数据操作)
每一次的sql操作都会占用数据库的资源。如果将N条操作先存储到缓存区中,然后再一次性冲刷到数据库中,这就减少了与数据库的交互次数。因此可以提高效率。
//批处理中使用的方法
addBatch(String sql):将sql语句存储到缓冲中
executeBatch():将缓冲中的sql语句全部冲刷到数据库中
/**
* 批处理案例
*/
public void testBatch(){
try {
for (int i = 0; i < 100000; i++) {
int num = (int) (Math.random() * 2);
String gender = null;
if (num == 0) {
gender = "f";
} else {
gender = "m";
}
String sql = "insert into testbatch values ("+i+",'zs" + i + "','" + gender + "')";
// 将sql语句存储到缓冲中
stat.addBatch(sql);
// 冲刷缓冲
if(i%1000==0){
stat.executeBatch();
}
}
// for循环结束后,缓冲里可能还有数据,再次冲刷
stat.executeBatch();
} catch (SQLException e) {
e.printStackTrace();
}
}
三、jdbc的注入问题:
案例:APP登录案例
/**
* 客户账户信息类
* 为表bank_account设计一个实体类型,也就是表的字段与java类型的属性的映射关系
*/
public class Account {
// 提供属性
private int id;
private String account_id;
private double balance;
private String realName;
private String password;
private String idcard;
private Timestamp timestamp;
private String gender;
public Account() {
}
public Account(int id, String account_id, double balance, String realName, String password, String idcard, Timestamp timestamp, String gender) {
this.id = id;
this.account_id = account_id;
this.balance = balance;
this.realName = realName;
this.password = password;
this.idcard = idcard;
this.timestamp = timestamp;
this.gender = gender;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccount_id() {
return account_id;
}
public void setAccount_id(String account_id) {
this.account_id = account_id;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getIdcard() {
return idcard;
}
public void setIdcard(String idcard) {
this.idcard = idcard;
}
public Timestamp getTimestamp() {
return timestamp;
}
public void setTimestamp(Timestamp timestamp) {
this.timestamp = timestamp;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return id == account.id &&
Double.compare(account.balance, balance) == 0 &&
Objects.equals(account_id, account.account_id) &&
Objects.equals(realName, account.realName) &&
Objects.equals(password, account.password) &&
Objects.equals(idcard, account.idcard) &&
Objects.equals(timestamp, account.timestamp) &&
Objects.equals(gender, account.gender);
}
@Override
public int hashCode() {
return Objects.hash(id, account_id, balance, realName, password, idcard, timestamp, gender);
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", account_id='" + account_id + '\'' +
", balance=" + balance +
", realName='" + realName + '\'' +
", password='" + password + '\'' +
", idcard='" + idcard + '\'' +
", timestamp=" + timestamp +
", gender='" + gender + '\'' +
'}';
}
}
登录方法:如果账号和密码相符合,就可以登录
/**
* 模拟服务器接收客户端的用户账号和密码,然后使用jdbc将来查询表中是否有此用户名和密码对应的记录,
* 如果有,就将返回的记录封装成一个Account类型的对象。
*/
public class AppServer {
public Account checkLogin(String accountId, String password) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
Account account = null;
try {
conn = DButil.getConnection();
stat = conn.createStatement();
String sql = "select * from bank_account where account_id='" + accountId + "' and user_pwd='" + password + "'";
rs = stat.executeQuery(sql);
if(rs.next()) {
int id = rs.getInt("id");
double balance = rs.getDouble(3);
String realName = rs.getString(4);
String idcard = rs.getString(6);
Timestamp timestamp = rs.getTimestamp(7);
String gender = rs.getString(8);
account = new Account(id,accountId,balance,realName,password,idcard,timestamp,gender);
}
} catch (SQLException e) {
e.printStackTrace();
}
return account;
}
}
测试:
/**
* 模拟app登录的客户端
*/
public class AppClient {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入账号:");
String account_id = sc.next();
System.out.println("请输入密码:");
// String password = sc.next();
// 由此引出jdbc的注入问题,如果恶意修改查询语句结构,将会错误
String password = "111' or '1'='1";
// 创建一个服务器
AppServer server = new AppServer();
Account account = server.checkLogin(account_id,password);
if(account!=null){
System.out.println("输入正确");
}else {
System.out.println("输入错误,重新输入");
}
}
}
安全隐患
statement对象发送的语句结构可以被更改,可以添加一下其他条件,导致输入语句恒成立,这种问题就是SQL注入问题。有很大的安全隐患。
PreparedStatement类
PreparedStatement类型是Statement类型的子类型。
此类型可以确定SQL语句的结构,无法通过其它方式来增减条件。
此类型还通过占位符 "?"来提前占位,并确定语句的结构。
然后在提供相应的赋值方式:
ps.setInt(int index,int value)
ps.setString(int index,String value)
ps.setDouble(int index,double value)
ps.setDate(int index,Date value)
index表示sql语句中的占位符 ? 的索引。从1开始
value:占位符所对应的要赋予的值
/**
* 修改登录案例的服务端代码,将statment替换成子类型PreparedStatement
*/
public class AppServer1 {
public Account checkLogin(String accountId, String password) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Account account =null;
// 获取连接
conn = DButil.getConnection();
try {
String sql = "select * from bank_account where account_id=? and user_pwd=?";
ps = conn.prepareStatement(sql);
ps.setString(1,accountId);
ps.setString(2,password);
rs = ps.executeQuery();
if(rs.next()){
int id = rs.getInt(1);
double balance = rs.getDouble(3);
String realName = rs.getString(4);
String idcard = rs.getString(6);
Timestamp timestamp = rs.getTimestamp(7);
String gender = rs.getString(8);
account = new Account(id,accountId,balance,realName,password,idcard,timestamp,gender);
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DButil.closeConnection(conn,ps,rs);
}
return account;
}
}
四、jdbc中的事务:
4.1 银行转账案例:
1.需求:一个账号fromAccount向另一个账号toAccount转入money元钱
2.分析:-检查两个账号是否存在,不存在的话,结束转账行为
-检查转出账号的里金额是否充足,不充足,结束转账行为,充足的话,进行扣款money元
-转入账号进行增加money元
当转出账号已经扣钱后,发生异常,转入账号没有增加钱,会导致转账前后总金额不一致。因此我们应该确定新的规范,如果执行成功,那么钱就会转过去;如果失败,最后还是转账前的情况。对此我们应该把这个过程看成一个事务,如果成功,那么事务执行结束,如果失败,那么回滚到最初的情况。
4.2 事务的特征(ACID)
1)原子性(A): 事务要么成功,要么失败,不可分割。
2)一致性©: 事务开始前和结束后,数据必须保证一致。
3)隔离性(I): 多个用户操作一张表时,每个用户一个事务,并且每个事务之间互不干扰。
4)持久性(D): 一个事务被提交后,必须保证他可以持久存在。
4.3 事务的使用方法:(是对Connection接口的)
//此方法可以取消事务的自动提交功能,值为false。true可以提交事务。
Connection.setAutoCommit(boolean flag)
//进行事务提交。
Connection.commit()
//进行事务回滚。
Connection.rollback()
4.4 多事务遇到的问题
脏读 | 不可重复读 | 幻读 |
---|---|---|
事务A读取了事务B刚刚更新的数据,但是事务B回滚了,这样就导致事务A读取的为脏数据。 | 事务A读取同一条记录两次,但是在两次之间事务B对该条记录进行了修改并提交,导致事务A两次读取的数据不一致。 | 事务A在修改全表的数据,在未提交时,事务B向表中插入或删除数据,这样导致事务A读取的数据与需要修改的数据不一致,就和幻觉一样 |
对于一个数据 | 对于一个数据 | 对于一张表 |
事务A读取事务B未提交的。 | 事务A读取事务B开始前和结束后的。 | 事务A读取事务B开始前和结束后的。 |
4.5 事务的隔离机制
1)未提交读:就是不做隔离控制,可以读到“脏数据”,可能发生不可重复读,也可能出现幻读。
2)提交读:提交就是不允许读取事务没有提交的数据。显然这种级别可以避免了脏读问题。但是可能发生不可重复读,幻读。这个隔离级别是大多数数据库(除了mysql)的默认隔离级别。
3)可重复读:为了避免提交读级别不可重复读的问题,在事务中对符号条件的记录上“排他锁”,这样其他事务不能对该事物操作的数据进行修改,可以避免不重复读的问题产生。由于只对操作数据进行上锁的操作,所有当其他事务插入或删除数据时,会出现幻读的问题,此种隔离级别为mysql默认的隔离级别。
4)序列化:在事务中对表上锁,这样在事务结束前,其他事务都不能够对表数据进行操作(包括新增,删除和修改),不可重复读和幻读,是最安全的隔离级别。但是由于该操作是堵塞的,因此会严重影响性能。
/**
* 写一个银行转账客户端,说明事务的概念
*/
public class TransferTest {
public static void main(String[] args) {
String fromAccount = "6225113088436225";
String toAccount = "6225113088436226";
double money = 10000;
String password = "zgl123456";
boolean success = oneToOne(fromAccount,toAccount,money,password);
if (success){
System.out.println("成功");
}else {
System.out.println("失败");
}
}
/**
* 定义一个转账方法
* @param fromAccount 转出账号
* @param toAccount 转入账号
* @param money 转账金额
* @param password 转出账号密码
* @return 转账是否成功
*/
public static boolean oneToOne(String fromAccount,String toAccount,double money,String password){
if (fromAccount==null||fromAccount.length()==0||toAccount==null||toAccount.length()==0){
return false;
}
if (money <= 0) {
return false;
}
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DButil.getConnection();
// 在此处,关闭事务的自动提交,开启手动提交
// true表示自动提交。
conn.setAutoCommit(false);
// 判断转出账号是否存在
String sql = "select * from bank_account where account_id = ?";
ps = conn.prepareStatement(sql);
ps.setString(1,fromAccount);
rs = ps.executeQuery();
if (!rs.next()){
System.out.println("转出账号不存在");
return false;
}
// 账号存在,那么就保留一下账号的余额
double balance = rs.getDouble("account_balance");
// 判断余额是否充足,支持转账
if (balance < money) {
System.out.println("转出账号余额不足");
return false;
}
// 判断转入账号是否存在
ps.setString(1,toAccount);
rs = ps.executeQuery();
if(!rs.next()){
System.out.println("转入账号不存在");
return false;
}
// 到此,进行扣款操作
String reduce = "update bank_account set account_balance = account_balance-? where account_id = ? and user_pwd = ?";
ps = conn.prepareStatement(reduce);
ps.setDouble(1,money);
ps.setString(2,fromAccount);
ps.setString(3,password);
ps.executeUpdate();
// 模拟一个异常操作
int[] arr = {1,2};
System.out.println(arr[3]);
// 进行存款操作
String add = "update bank_account set account_balance = account_balance+? where account_id = ?";
ps = conn.prepareStatement(add);
ps.setDouble(1,money);
ps.setString(2,toAccount);
ps.executeUpdate();
// 关闭事务
//conn.setAutoCommit(true); 可以开启自动提交事务
conn.commit();
// 到此存款成功
return true;
} catch (SQLException e) {
e.printStackTrace();
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}finally {
DButil.closeConnection(conn,ps,rs);
}
return false;
}
}