文章目录
一. JDBC本质
- JDBC是SUN公司制定的一套接口interface
接口都是调用者和实现者
面向接口调用、面向接口写实现类,这都属于面向接口编程。 - 为什么要面向 接口编程
解耦合 :降低程序的耦合度,提高程序的扩展力。
接口为什么能够降低耦合度 - 多态机制就是非常典型的:面向抽象编程(不要面向具体编程)
多态能降低耦合度
多态是面向对象的重要特性,简单点说:“一个接口,多种实现”,就是同一种事物表现出的多种形态。编程其实就是一个将具体世界进行抽象化的过程,多态就是抽象化的一种体现,把一系列具体事物的共同点抽象出来, 再通过这个抽象的事物, 与不同的具体事物进行对话。
比如有一个函数是叫某个人来吃饭,函数要求传递的参数是人的对象,可是来了一个美国人,你看到的可能是用刀和叉子在吃饭,而来了一个中国人你看到的可能是用筷子在吃饭,这就体现出了同样是一个方法,可以却产生了不同的形态,这就是多态!
如果父类与子类有同名的属性---------执行父类的属性
如果父类与子类有同名的方法(重写)----执行子类重写之后的方法
若想要调用子类中独有的成员
(强制类型转化) 造型 铸型 (向上/向下转型)
建议:
Animal a = new Cat();
Animal a = new Dog();
//当你定义其他函数,比如feed函数:可以定义feed(Animal a){}
//此时,只要你传入的参数是Animal以及子类都可以,
//传入参数Cat,就调用Cat的对象的方法属性
//传入参数Dog,就调用Dog的对象的方法属性
//这就是面向抽象编程,面向一个抽象的Animal,而不是面向具体的Dog或者Cat,
//这样,当有一天Dog和Cat删除了,也不影响我们的eat函数,因为Animal父类还在
public void feed(Animal a){
//面向父类类型编程
}
不建议:
Dog d = new Dog();
Cat c = new Cat();
- 为什么SUN制定一套JDBC接口呢?
因为每一个数据的底层实现原理都不一样。Oracle数据库有自己的原理。MySQL数据库也有自己的原理。每一个数据库产品都有自己独特的实现原理。程序员不可能挨个调用使用,所以设计JDBC接口。
这样以后程序员面向JDBC接口写代码,调用JDBC提供的几个类。
JDBC其实就是一堆的class类文件。
这些接口的实现由各大数据库厂家自己进行编写。去实现接口中的各个类class。
二. 模拟JDBC的使用
Sun公司:制定接口
数据库厂家:实现接口,子类数据库厂家 继承 Sun公司的接口
程序员:通过多态,调用父类----接口中方法,实际调用 子类----数据库厂家 中 重写 接口的方法
Sun公司:
/*
SUN公司负责制定这套SUN接口
*/
public interface JDBC{
//数据库连接方法
void getConnection();
}
数据库厂家:
/*
Oracle的数据库厂家负责编写JDBC接口的实现方法
*/
public class Oracle implements JDBC{
//数据库连接方法
void getConnection(){
System.out.println("连接Oracle数据库成功");
}
}
/*
SqlServer的数据库厂家负责编写JDBC接口的实现方法
*/
public class SqlServer implements JDBC{
//数据库连接方法
void getConnection(){
System.out.println("连接SqlServer数据库成功");
}
}
程序员:new方法创建对象
/*
Java程序员角色:
不需要关心具体是哪个品牌的数据库,只需要面向JDBC接口写代码
面向接口编程,面向抽象编程,不要面向具体编程。
*/
public class JavaProgrammer{
public static void main(String[] args){
JDBC jdbc = new SqlServer(); //使用哪一个数据库,就new哪一个数据库
//JDBC类型,调用接口中的方法就可以了
jdbc.getConnection(); //多态:调用父类的方法即可
// 子类重写了父类方法,默认调用子类重写的方法。
// 不需要关注继承接口的具体数据库类是如何实现的,不用程序员自己去实现,
// 数据库厂家自己进行实现
//最终,我们调用接口中的抽象方法,但是却可以调用各个子类的堆抽象方法的实现。
}
}
通过多态:JDBC jdbc = new SqlServer();
,可以实现调用父类的方法名字,实际上调用的是子类重写父类的方法。
这样以后想要换一家数据库也只需要,修改一个代码JDBC jdbc = new Oracle();
即可,后面的代码调用方法也都是调用父类的JDBC的方法(虽然实际调用的是子类的方法),不需要修改其他。
但是如果你就面向具体编程了,比如SqlServer jdbc = new SqlServer();
那么不同厂家有不同的函数名,就比如连接这个函数,SqlServer厂家的方法是conn,Oracle厂家的方法是connection。
之前数据库使用SqlServer ,调用连接方法是jdbc.conn,
那么当你把数据库变换Oracle以后,首先类一定要修改Oracle jdbc = new Oracle();
,那么调用方法就该为jdbc.connection。
修改的地方太多了,这样对程序员来说太难了,所以规定了一个JDBC接口,制定规则,各大厂家去实现这个接口,程序员只需要调用父类JDBC中的方法,就可以通过多态实际上调用的是子类重写的方法。
程序员:反射方法创建对象
public class JavaProgrammer{
public static void main(String[] args){
//除了new的方法创建对象
//还可以通过反射的方法创建对象
Class c = Class.forName("Oracle");
JDBC jdbc = (JDBC)c.newInstance();
jdbc.getConnection();
}
}
三. 配置文件
如果让客户在程序中修改使用数据库类型,不大现实,所以经常使用配置文件,来帮助用户随时修改使用哪种数据库。
首先定义一个配置文件:jdbc.properties
文件:里面只有一行:
className = Oracle
然后程序员的代码变为:
public class JavaProgrammer{
public static void main(String[] args) throws Exception {
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String className = bundle.getString("className");//里面输入key值,即等号左边
Class c = Class.forName(className);
JDBC jdbc = (JDBC)c.newInstance();
jdbc.getConnection();
}
}
//此时,如果想要修改使用的数据库类型,只需要在配置文件中修改就可以了。
//改配置文件,就可以,以后都不用改代码了
所以,如果需要使用数据库的时候,需要自己下载对应数据库的jar包,然后配置到环境中,jar包实际上就是各种实现接口的类,即是一堆class文件。
四. JDBC编程六步走
- 注册驱动(作用:告诉JAVA程序员,即将要连接的是哪个品牌的数据库)
- 获取连接(表示JVM的进程和数据库进程之间的通道打开了,这属于进程之间的通信,重量级的,使用之后一定要关闭)
- 获取数据库操作对象(专门执行sql语句的对象)
- 执行SQL语句(DQL DML…)
- 处理查询结果集(只有当第四步执行的是delect语句的时候,才有这第五步处理查询结果集)
- 释放资源(使用资源之后一定要关闭资源。Java和数据库属于进程之间的通信,重量级的,使用之后一定要关闭)
import java.sql.*;
public class JDBCTest01 {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
//由于要关闭的内容都在try中,不能关闭,所以讲try中的几个变量移到外面去:
try{
// 1、注册驱动
Driver driver = new com.mysql.jdbc.Driver(); //多态,父类型引用指向子类型对象
DriverManager.registerDriver(driver);
//第1步也可以合为一句:
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());
//注册驱动无非就是加载数据库的jar包,所以还可以通过反射机制
//1、注册驱动另一种写法
Class.forName("com.mysql.jdbc.Driver()");
//反射会导致后面的类的加载,类加载就会自动执行静态代码块
//直接完成了驱动注册,也不需要接收返回值
// 2、获取连接
String url = "jdbc:mysql://127.0.0.1:3306/mydatabase"; //url: 协议;IP;端口;资源名
String user = "root"; // 数据库使用者
String password = "146"; //密码
conn = DriverManager.getConnection(url,user,password);
// 3、获取数据库操作对象
stmt = conn.createStatement();
// 4、执行sql语句
// int executeUpdate(String sql) 返回值是“影响数据库中的记录条数”
int count = stmt.executeUpdate("update dept set dname = '销售部',loc = '合肥' where deptno = 20;");
// 5、处理查询结果集 由于不是select,所以不用处理
} catch(SQLException e) {
e.printStackTrace();
} finally {
// 6、释放资源
// 从小到大依次关闭,每个资源都需要捕获异常
//为保证资源一定释放,在finally语句块中关闭资源,分别对其try...catch...
if(stmt != null) {
try {
stmt.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
五. 数据库中所有字符串写到配置文件中
实际开发中不建议把连接数据库的信息写死到java程序中,所以考虑写到配置文件中。
六. 查select
rs = stmt.executeQuery("select empno,ename,sal from emp");
每次调用next方法,向下一行,有数据就返回true
主要如何处理查询结果:
方式一:
//通过下标获取查询结果,1就是第一列结果,2是第二列结果
String empno = rs.getString(1);
String ename = rs.getString(2);
String sal = rs.getString(3);
System.out.println(empno + "," + ename + "," + sal);
方式二:
// 按下标取出,程序不健壮
String empno = rs.getString("empno");
String ename = rs.getString("ename");
String sal = rs.getString("sal");
System.out.println(empno + "," + ename + "," + sal);
方式三:
// 以指定的格式取出
int empno = rs.getInt(1);
String ename = rs.getString(2);
double sal = rs.getDouble(3);
System.out.println(empno + "," + ename + "," + (sal + 100));
整体查询代码:
import java.sql.*;
import java.util.*;
public class JDBCTest05 {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
ResourceBundle rb = ResourceBundle.getBundle("jdbc");
String driver = rb.getString("driver");
String url = rb.getString("url");
String user = rb.getString("user");
String password = rb.getString("password");
// 1、注册驱动
Class.forName(driver);
// 2、建立连接
conn = DriverManager.getConnection(url,user,password);
// 3、获取数据库操作对象
stmt = conn.createStatement();
// 4、执行sql语句
rs = stmt.executeQuery("select empno,ename,sal from emp");
// 5、获取查询结果集
while(rs.next()){
/*
String empno = rs.getString(1);
String ename = rs.getString(2);
String sal = rs.getString(3);
System.out.println(empno + "," + ename + "," + sal);
*/
/*
// 按下标取出,程序不健壮
String empno = rs.getString("empno");
String ename = rs.getString("ename");
String sal = rs.getString("sal");
System.out.println(empno + "," + ename + "," + sal);
*/
/*
// 以指定的格式取出
int empno = rs.getInt(1);
String ename = rs.getString(2);
double sal = rs.getDouble(3);
System.out.println(empno + "," + ename + "," + (sal + 100));
*/
int empno = rs.getInt("empno");
String ename = rs.getString("ename");
double sal = rs.getDouble("sal");
System.out.println(empno + "," + ename + "," + (sal + 200));
}
} catch(Exception e){
e.printStackTrace();
}finally{
// 6、释放资源
if(rs != null){
try{
rs.close();
} catch (Exception e){
e.printStackTrace();
}
}
if(stmt != null){
try{
stmt.close();
} catch (Exception e){
e.printStackTrace();
}
}
if(conn != null){
try{
conn.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}
七. 使用Statement可能导致SQL注入问题
实现功能:
- 需求:
模拟用户登录功能的实现 - 业务描述:
程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码。
用户输入用户名和密码以后,提交信息,java程序收集到用户信息。
java程序连接数据库验证用户和密码是否合法。
合法:显示登录成功。
不合法:显示登陆失败。
所以,代码实现:
String sql = "select * from t_user where loginName = '"+loginName + "'and loginPwd = '" + loginPwd + "'";
rs = stmt.executeQuery(sql);
if(rs.next()){
loginSucess = true;
}
此时如果输入:
用户名:fdsa
密码:fdsa’ or ‘1’='1
‘1’ = '1’是对的,所以or后面是对的,就会导致整个表都被输出。所以rs.next()一定会输出。
问题出在用户输入的内容有sql的关键字导致SQL注入。
导致SQL注入的根本原因是什么:用户输入信息中含有sql语句的关键字,而这些关键字参与sql语句的编译过程。导致sql语句的原意被扭曲,进而达到sql注入。
八. PreparedStatement解决SQL注入问题
只要不让他参加编译就可以了啊,但是上面的代码不行,正好完成了sql语句的拼接编译进去了。
//Statement stmt = null; 变为下面那句
PreparedStatement ps = null;
//然后后面的3变化
// 3、获取预编译的数据库操作
String sql = "select * from t_user where loginName = ? and loginPwd= ? ";
ps = conn.prepareStatement(sql);
//给占位符?传值(第一个问号下标是1,第二个问号下标是2)
ps.setString(1,loginNamme);
ps.setString(2,loginPwd);
// 4、执行sql语句
rs = ps.executeQuery();
if(rs.next()){
loginSucess = true;
}
Statement和PreparedStatement的区别:
- Statement存在sql注入问题,PreparedStatement解决sql注入问题。
- Statement是编译一次执行一次,PreparedStatement效率较高一些。
- PreparedStatement会在编译阶段做类型的安全检查。
九. 业务需要SQL注入
- 但有些系统需要SQL注入,这时使用statement。
比如:京东升序降序。必须SQL注入,order。 - 需要进行SQL拼接的时候需要SQL注入,如果只是传值,使用preparedstatement。
- 综上:preparedstatement使用较多。只有极少情况下需要使用statement。
十. preparedstatement实现插入和删除
// 3、获取预编译的数据库操作
String sql = "insert into dept (deptno , dname, loc) values (?,?,?)";
ps = conn.prepareStatement(sql);
//给占位符?传值(第一个问号下标是1,第二个问号下标是2)
ps.setInt(1,60);
ps.setString(2,"销售部");
ps.setString(3,"上海");
// 4、执行sql语句
int count = ps.executeUpdate();
十一. JDBC事务
JDBC中的事务是自动提交的,什么是自动提交?
只要执行任意一条DML语句,则自动提交一次。这是JDBC默认的事务行为。
但是在实际业务当中,通常都是N条DML语句共同联合才能完成的,必须保证他们这些DML语句在同一事务中同时成功或者同时失败。
假设一个银行转账系统,数据库中有用户名actno int
类型和银行余额balance double
类型。
账户111:余额20000
账户222:余额0
现在手写代码,实现账户111给账户222转账10000元。按理来说,转账成功后:
账户111:余额10000
账户222:余额10000
正常情况下代码:
//设置账户1:
// 3、获取预编译的数据库操作
String sql = "update t_act set balance = ? where actno = ?";
ps = conn.prepareStatement(sql);
//给占位符?传值(第一个问号下标是1,第二个问号下标是2)
ps.setDouble(1,10000);
ps.setInt(2,111);
// 4、执行sql语句
int count = ps.executeUpdate();
//设置账户2:
ps.setDouble(1,10000);
ps.setInt(2,222);
// 4、执行sql语句
count += ps.executeUpdate();
System.out.println(count == 2?"转账成功":"转账失败");
//正常情况下:输出:转账成功
//账户111余额为10000
//账户222余额10000
制造异常:
验证JDBC事务是否是默认提交的。
//设置账户1:
// 3、获取预编译的数据库操作
String sql = "update t_act set balance = ? where actno = ?";
ps = conn.prepareStatement(sql);
//给占位符?传值(第一个问号下标是1,第二个问号下标是2)
ps.setDouble(1,10000);
ps.setInt(2,111);
// 4、执行sql语句
int count = ps.executeUpdate();
//##############制造异常###########
String s = null;
s.toString();
//异常会导致程序从这里直接退出,导致账户2未收到转账信息
//设置账户2:
ps.setDouble(1,10000);
ps.setInt(2,222);
// 4、执行sql语句
count += ps.executeUpdate();
System.out.println(count == 2?"转账成功":"转账失败");
//账户111余额为10000
//账户222余额0
//因为异常导致,直接不执行后面的语句,直接执行finally了。
//账户2没能执行到,凭空丢了10000
修改默认提交:
conn.setAutoCommit(false);
在第2步,链接后面,加上,将自动提交机制修改为手动提交
conn.commit();
当多个数据库语句成功执行完毕,说明没有异常,事务结束,手动提交。
if(conn != null){
conn.rollback();
}
如果有异常,判断conn是否为空,不为null,则应当进行回滚操作。
import java.sql.*;
public class JDBCTest01 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try{
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver()");
// 2、获取连接
String url = "jdbc:mysql://127.0.0.1:3306/mydatabase"; //url: 协议;IP;端口;资源名
String user = "root"; // 数据库使用者
String password = "146"; //密码
conn = DriverManager.getConnection(url,user,password);
//**************将自动提交机制修改为手动提交**************
conn.setAutoCommit(false);
//设置账户1:
// 3、获取预编译的数据库操作
String sql = "update t_act set balance = ? where actno = ?";
ps = conn.prepareStatement(sql);
//给占位符?传值(第一个问号下标是1,第二个问号下标是2)
ps.setDouble(1,10000);
ps.setInt(2,111);
// 4、执行sql语句
int count = ps.executeUpdate();
//##############制造异常###########
String s = null;
s.toString();
//如果有异常,就进行回滚操作,没有异常就正常手动提交。
//设置账户2:
ps.setDouble(1,10000);
ps.setInt(2,222);
// 4、执行sql语句
count += ps.executeUpdate();
System.out.println(count == 2?"转账成功":"转账失败");
//能够执行到这,说明程序没有异常,事务结束,手动提交
conn.commit();
// 5、处理查询结果集 由于不是select,所以不用处理
} catch(SQLException e) {
if(conn != null){
conn.rollback();
}
e.printStackTrace();
} finally {
// 6、释放资源
// 从小到大依次关闭,每个资源都需要捕获异常
if(ps != null) {
try {
ps.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
十二. 封装成工具类
将刚才的代码封装成工具类,防止每天写重复的代码。
import java.sql.*;
public class DBUtil {
/*
工具类中的构造方法都是私有的:https://blog.youkuaiyun.com/static_void_james/article/details/78254465
因为工具类当中方法都是静态的,不需要new对象,直接采样类名调用。
和类绑定,这样每次调用就不需要new对象。
*/
private DBUtil(){};
//静态块,加载类的时候自动执行,使用数据库一定要先注册驱动,所以讲这一部分写在静态块中
static {
try {
Class.forName("jdbc:mysql://127.0.0.1:3306/mydatabase");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(
"jdbc:mysql://127.0.0.1:3306/mydatabase",
"root",
"146");
}
public static void close(Connection conn,Statement ps, ResultSet rs){//面向父类编程,Statement p而不是PreparesStatement,更具有一般性。
if(ps != null) {
try {
ps.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null) {
try {
conn.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
}
}
使用工具类进行模糊查询:
public class JDBCTest{
public static void main(String[] args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
String sql = "select ename from emp where ename like ?";
ps = conn.prepareStatement(sql);
ps.setString(1,"_A%");
rs = ps.executeQuery();
while (rs.next()){
System.out.println(rs.getString("ename"));
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(conn, ps, rs);
}
}
}
十三. 行级锁;悲观锁;乐观锁
- 行级锁
select ename, job, sal from emp where job='MANAGER' for update;
数据库查询的时候有了for update表示行级锁。
加一个for update就是行级锁,工作岗位是MANAGER中的数据被锁住,其他线程不能够操作。
当前事务还没有结束的时候,这几行数据被锁住。 - 悲观锁:
事务必须排队执行,数据锁住,不允许并发。 - 乐观锁:
意思是任何人都可以修改,但是数据库中后面有一个版本号,比如我当时取走数据的时候版本号1.0,另一个线程修改了数据库版本号变为2.0,我提交自己数据的时候发现版本号和刚才的不一样了,所以会进行回滚,不修改数据。