JDBC学习

    科技2024-08-02  74

    文章目录

    一. JDBC本质 二. 模拟JDBC的使用三. 配置文件四. JDBC编程六步走五. 数据库中所有字符串写到配置文件中六. 查select七. 使用Statement可能导致SQL注入问题八. PreparedStatement解决SQL注入问题九. 业务需要SQL注入十. preparedstatement实现插入和删除十一. JDBC事务十二. 封装成工具类十三. 行级锁;悲观锁;乐观锁

    一. 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.csdn.net/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,我提交自己数据的时候发现版本号和刚才的不一样了,所以会进行回滚,不修改数据。

    Processed: 0.013, SQL: 8