JDBC简单操作
jdbc编程步骤
- 加载数据库驱动
- 创建并获取数据库链接
- 创建jdbc statement对象
- 设置sql语句
- 设置sql语句中的参数(使用preparedStatement)
- 通过statement执行sql并获取结果
- 对sql执行结果进行解析处理
- 释放资源(resultSet、preparedstatement、connection)
问题总结
1.数据库连接,使用时就创建,不使用立即释放,对数据库进行频繁连接开启和关闭,造成数据库资源浪费,影响数据库性能。
设想:使用数据库连接池管理数据库连接。
2.将sql语句硬编码到java代码中,如果sql语句修改,需要重新编译java代码,不利于系统维护。
设想:将sql语句配置在xml配置文件中,即使sql变化,不需要对java代码进行重新编译。
3.向preparedStatement中设置参数,对占位符号位置和设置参数值,硬编码在java代码中,不利于系统维护。
设想:将sql语句及占位符号和参数全部配置在xml中。
4.从resultSet中遍历结果集数据时,存在硬编码,将获取表的字段进行硬编码,不利于系统维护。
设想:将查询的结果集,自动映射成java对象。
需要下载jar包mysql-connector-java-8.0.16,可以通过maven仓库进行下载;
import java.sql.*;
public static void main(String[] args) { //数据库连接 Connection conn = null; //预编译的Statement,使用预编译的Statement提高数据库性能(当我们发送给数据库一段sql语句,数据库会编译sql语句,并将编译的结果放到数据库的缓存中,当下次发送sql语句的时候,如果sql语句一样,就不会再编译了) PreparedStatement preparedStatement = null; //返回结果集 ResultSet resultSet = null; try { //加载驱动,通常不需要手动加载,会自动注册 Class.forName("com.mysql.cj.jdbc.Driver"); //Class.forName("com.mysql.jdbc.Driver"); //老版本的驱动 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/javas1?serverTimezone=UTC", "root", "xx"); //由于jar包版本是最新的,需要加上?servletTimezone=UTC,否则报错 String sql = "SELECT * FROM t_user WHERE username=?"; preparedStatement = conn.prepareStatement(sql); //给占位符赋值 preparedStatement.setString(1,"yy"); resultSet = preparedStatement.executeQuery(); System.out.println(resultSet); while (resultSet.next()){ String username = resultSet.getString("username"); System.out.println(username); } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { //反向释放资源 resultSet.close(); preparedStatement.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
补充
int i = preparedStatement.executeUpdate(); //表示执行更新(插入)操作,返回int类型数据
使用Statement引发sql注入的例子
public static void main(String[] args) { String str= "name1" or "1" = "1"; test(str); } public static void test(String str){ //数据库连接 Connection conn = null; try { conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/javas1?serverTimezone=UTC", "root", "123456"); //由于jar包版本是最新的,需要加上?servletTimezone=UTC,否则报错 //sql语句在拼接的时候就会造成sql注入 String sql = "SELECT * FROM USER WHERE NAME="" + str + """; // String sql = "SELECT * FROM USER WHERE NAME="user1""; System.out.println(sql);//SELECT * FROM USER WHERE NAME="user1" or 1=1 Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(sql); while (resultSet.next()) { System.out.println(resultSet.getString("name")); } } catch (SQLException e) { e.printStackTrace(); } finally { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
使用Dbutils简化JDBC
去官网上下载Dbutils的jar包
数据库连接池示例
class ConnectionPool implements DataSource { private static final String driver = "com.mysql.jdbc.Driver"; private static final String dbUrl = "jdbc:mysql://localhost:3306/db_blog?useSSL=true"; private static final String userName = "root"; private static final String password= "123456"; private LinkedList<Connection> pool; private Connection getOneConnection() { Connection connection = null; try { Class.forName(driver); connection = DriverManager.getConnection(dbUrl,userName,password); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } return connection; } @Override public Connection getConnection() throws SQLException { //初始化最大容量是2个连接 if(pool==null){ pool = new LinkedList<>(); for(int i=0;i<2;i++){ pool.add(getOneConnection()); } } //最大的连接数没有上限 if(pool.size()<=0){ return getOneConnection(); } return pool.remove(); } public void close(Connection connection){ //通过这个保证连接池的空间固定 if (pool.size()>2){ try { connection.close(); return; } catch (SQLException e) { e.printStackTrace(); } } pool.add(connection); } }
JDBC常见面试题
JDBC是如何实现Java程序和JDBC驱动的松耦合的?
JDBC API使用Java的反射机制来实现Java程序和JDBC驱动的松耦合。随便看一个简单的JDBC示例,你会发现所有操作都是通过JDBC接口完成的,而驱动只有在通Class.forName反射机制来加载的时候才会出现。
JDBC中的Statement 和PreparedStatement的区别?
- PreparedStatement是预编译的SQL语句,效率高于Statement。
- PreparedStatement支持?操作符,相对于Statement更加灵活。
- PreparedStatement可以防止SQL注入,安全性高于Statement。
- CallableStatement适用于执行存储过程。
- PreparedStatement 对象的开销比Statement大,对于一次性操作并不会带来额外的好处。如果查询一次性可以使用Statement(但是Statement会造成sql注入)
PreparedStatement又是如何避免SQL注入攻击的?
避免的原理不细说:原理就是会将参数值中的特殊符号进行转译;
preparedStatement也存在sql注入的问题:https://www.cnblogs.com/iyangyuan/archive/2015/09/15/4809494.html
使用PreparedStatement的Batch功能
PreparedStatement的缺点是什么,怎么解决这个问题?
PreparedStatement的一个缺点是,我们不能直接用它来执行in条件语句;需要执行IN条件语句的话,下面有一些解决方案:
- 分别进行单条查询——这样做性能很差,不推荐。
- 使用存储过程——这取决于数据库的实现,不是所有数据库都支持。
- 动态生成PreparedStatement——这是个好办法,但是不能享受PreparedStatement的缓存带来的好处了。
- 在PreparedStatement查询中使用NULL值——如果你知道输入变量的最大个数的话,这是个不错的办法,扩展一下还可以支持无限参数。
JDBC中大数据量的分页解决方法?
最好的办法是利用sql语句进行分页,这样每次查询出的结果集中就只包含某页的数据内容。
说说数据库连接池工作原理和实现方案?
JAVA EE服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接。客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其表记为忙。如果当前没有空闲连接,池驱动程序就新建一定数量的连接,新建连接的数量有配置参数决定。当使用的池连接调用完成后,池驱动程序将此连接表记为空闲,其他调用就可以使用这个连接。
实现方案:连接池使用集合来进行装载,返回的Connection是原始Connection的代理,代理Connection的close方法,当调用close方法时,不是真正关连接,而是把它代理的Connection对象放回到连接池中,等待下一次重复利用。
具体代码:
@Override public Connection getConnection() throws SQLException { if (list.size() > 0) { final Connection connection = list.removeFirst(); //看看池的大小 System.out.println(list.size()); //返回一个动态代理对象 return (Connection) Proxy.newProxyInstance(Demo1.class.getClassLoader(), connection.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //如果不是调用close方法,就按照正常的来调用 if (!method.getName().equals("close")) { method.invoke(connection, args); } else { //进到这里来,说明调用的是close方法 list.add(connection); //再看看池的大小 System.out.println(list.size()); } return null; } }); } return null; }
Java中如何进行事务的处理?
在JDBC编程模式中,一个数据库连接建立时,就处于一个自动提交模式,每一个SQL语句被执行完成后就会被自动提交,反映到数据库中。当需要把几条逻辑上相关的SQL组成的一个事务执行时,就需要关闭事务自动提交模式。如下面的语句所示: con.setAutoCommit(false); // 关闭自动提交模式 一旦关闭了事务自动提交模式,不会有任何SQL语句被提交至数据库系统执行,除非显式的调用提交方法。
Connection类中提供了4个事务处理方法:
setAutoCommit(Boolean autoCommit):设置是否自动提交事务,默认为自动提交,即为true,通过设置false禁止自动提交事务; commit():提交事务; rollback():回滚事务. savepoint:保存点 注意:savepoint不会结束当前事务,普通提交和回滚都会结束当前事务的
修改JDBC代码质量
原来的代码:
public void printProducts(){ Connection c = null; Statements s = null; ResultSet r = null; try{ c=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:sid","username","password"); s=c.createStatement(); r=s.executeQuery("select id, name, price from product"); System.out.println("Id Name Price"); while(r.next()){ int x = r.getInt("id"); String y = r.getString("name"); float z = r.getFloat("price"); System.out.println(x + " " + y + " " + z); } } catch(Exception e){ } }
修改后的代码:
class Constant{ public static final String URL="jdbc:oracle:thin:@127.0.0.1:1521:sid"; public static final String USERNAME="username"; public static final String PASSWORD="password"; } class DAOException extends Exception{ public DAOException(){ super(); } public DAOException(String msg){ super(msg); } } public class Test{ public void printProducts() throws DAOException{ Connection c = null; Statement s = null; ResultSet r = null; try{ c = DriverManager.getConnection(Constant.URL,Constant.USERNAME,Constant.PASSWORD); s = c.createStatement(); r = s.executeQuery("select id,name,price from product"); System.out.println("Id Name Price"); while(r.next()){ int x = r.getInt("id"); String y = r.getString("name"); float z = r.getFloat("price"); System.out.println(x + " " + y + " " + z); } } catch (SQLException e){ throw new DAOException("数据库异常"); } finally { try{ r.close(); s.close(); c.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
修改点:
- url、password等信息不应该直接使用字符串“写死”,可以使用常量代替
- catch中应该回滚事务,抛出RuntimeException也是回滚事务的一种方法
- 关闭资源
execute,executeQuery,executeUpdate的区别是什么?
- Statement的execute(String query)方法用来执行任意的SQL查询,如果查询的结果是一个ResultSet,这个方法就返回true。如果结果不是ResultSet,比如insert或者update查询,它就会返回false。我们可以通过它的getResultSet方法来获取ResultSet,或者通过getUpdateCount()方法来获取更新的记录条数。
- Statement的executeQuery(String query)接口用来执行select查询,并且返回ResultSet。即使查询不到记录返回的ResultSet也不会为null。我们通常使用executeQuery来执行查询语句,这样的话如果传进来的是insert或者update语句的话,它会抛出错误信息为 “executeQuery method can not be used for update”的java.util.SQLException。
- Statement的executeUpdate(String query)方法用来执行insert或者update/delete(DML)语句,或者 什么也不返回DDL语句。返回值是int类型,如果是DML语句的话,它就是更新的条数,如果是DDL的话,就返回0。
- 只有当你不确定是什么语句的时候才应该使用execute()方法,否则应该使用executeQuery或者executeUpdate方法。
JDBC的ResultSet是什么?
- 在查询数据库后会返回一个ResultSet,它就像是查询结果集的一张数据表。
- ResultSet对象维护了一个游标,指向当前的数据行。开始的时候这个游标指向的是第一行。如果调用了ResultSet的next()方法游标会下移一行,如果没有更多的数据了,next()方法会返回false。可以在for循环中用它来遍历数据集。
- 默认的ResultSet是不能更新的,游标也只能往下移。也就是说你只能从第一行到最后一行遍历一遍。不过也可以创建可以回滚或者可更新的ResultSet
- 当生成ResultSet的Statement对象要关闭或者重新执行或是获取下一个ResultSet的时候,ResultSet对象也会自动关闭。
- 可以通过ResultSet的getter方法,传入列名或者从1开始的序号来获取列数据。
一共有三种ResultSet对象。
- ResultSet.TYPE_FORWARD_ONLY:这是默认的类型,它的游标只能往下移。
- ResultSet.TYPE_SCROLL_INSENSITIVE:游标可以上下移动,一旦它创建后,数据库里的数据再发生修改,对它来说是透明的。
- ResultSet.TYPE_SCROLL_SENSITIVE:游标可以上下移动,如果生成后数据库还发生了修改操作,它是能够感知到的。
JDBC的RowSet是什么,有哪些不同的RowSet?
RowSet用于存储查询的数据结果,和ResultSet相比,它更具灵活性。RowSet继承自ResultSet,因此ResultSet能干的,它们也能,而ResultSet做不到的,它们还是可以。RowSet接口定义在javax.sql包里。
RowSet提供的额外的特性有
- 提供了Java Bean的功能,可以通过setter和getter方法来设置和获取属性。RowSet使用了JavaBean的事件驱动模型,它可以给注册的组件发送事件通知,比如游标的移动,行的增删改,以及RowSet内容的修改等。
- RowSet对象默认是可滚动,可更新的,因此如果数据库系统不支持ResultSet实现类似的功能,可以使用RowSet来实现。
JDBC中存在哪些不同类型的锁?
从广义上讲,有两种锁机制来防止多个用户同时操作引起的数据损坏。
- 乐观锁——只有当更新数据的时候才会锁定记录。
- 悲观锁——从查询到更新和提交整个过程都会对数据记录进行加锁。
java.util.Date和java.sql.Date有什么区别?
java.sql.Date Sun Oct 06 22:57:53 CST 2019
java.sql.Date 1970-01-01 (不包含时间)
如果需要将时间存如到数据库,推荐使用 Timestamp
public static void main(String[] args) { Date date = new Date(); long time = date.getTime(); Timestamp timestamp = new Timestamp(time); System.out.println(timestamp); //2019-10-06 23:00:42.65 }