【JDBC实战】水果库存系统 [代码优化]

2022-11-15 14:14:26 浏览数 (1)

CSDN话题挑战赛第2期

参赛话题:学习笔记

JDBC专栏

(点击进入专栏)

【1】idea添加mysql-jar包

【2】使用IDEA连接数据库,执行增删改操作。

【3】IDEA连接数据库,执行查询操作,返回结果集并输出。

【4】JDBC实战 水果库存系统 [设计阶段]

【5】 水果库存系统 [功能实现①](接口实现类FruitDAOImpl)

【6】 水果库存系统 [功能实现②] 功能完善 使用效果

【7】 水果库存系统 代码优化

【8】连接数据库,执行批处理操作。

【9】数据库连接池:德鲁伊druid的使用


JDBC实战,优化水果库存系统

  • JDBC专栏
  • 一、前言
  • 二、包装:加载驱动,连接数据库的操作
  • 三、包装:关闭资源的操作
  • 四、包装:执行增删改操作,返回影响行数
  • 五:包装:执行查询操作,返回结果集输出
  • 六、包装:查询指定数据,返回单个实体对象
  • 七、BaseDAO类:存放优化后的通用方法
  • 八、优化后的实现类FruitDAOImpl
  • 九、总结

一、前言

在上一篇文章中,我们完成了水果库存系统的功能实现,大家应该能明显感觉到,代码还是有些过于重复了,不少方法中都会用到相同的代码,造成了代码冗余的情况。

那么接下来,在这篇文章中,我们将把相对冗余的代码提取出来,包装成单独的方法,那么对应的代码只需要写一份,就能被多次使用,提升代码的复用性,同时通用方法的包装还能提升系统的可拓展性。

注意:文章中的通用方法都会被放置在一个单独的BaseDAO类中,需要调用通用方法,就需要继承BaseDAO


二、包装:加载驱动,连接数据库的操作

经过前面文章的学习,我们都知道,对数据库进行增删改查等常规操作,加载驱动,连接数据库等过程都是必不可少的

也就是说,基本每一个与操作数据库功能相关的方法,都会用到这些操作的代码,那么我们就可以将其包装起来,减少代码重复造成的冗余。

代码语言:javascript复制
    //将加载驱动,连接数据库的操作包装成方法,减少代码冗余
    private Connection conn(){

        try {
            //加载驱动
            Class.forName(DRIVER);
            //数据库管理器,连接数据库
            connection = DriverManager.getConnection(URL, USER, PSW);
            return connection;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

该方法返回的是Connection类型,也就是数据库管理器与数据库连接的对象。

当需要调用这个包装的方法时,只需要用连接对象接收即可:

代码语言:javascript复制
//加载驱动,连接数据库
Connection connection = conn();

三、包装:关闭资源的操作

当我们连接数据库,进行了查询或者更新等操作后,关闭资源是非常重要的步骤,不可或缺。

也就是说,关闭资源等操作的代码也是会被重复使用到的,我们也可以将其包装成独立的方法,需要关闭资源时直接调用即可。

代码语言:javascript复制
    //将关闭资源的操作包装成方法
    private void close(ResultSet rs,PreparedStatement pstm,Connection connection){
        try {
            if(rs != null)
                rs.close();
            if(pstm != null)
                pstm.close();
            if(connection != null)
                connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

其中的rs对象是执行查询操作时返回的结果集,更新操作不会用到,但是也不影响使用,因为关闭前会判断对象是否为空。

执行更新操作时,没有创建rs对象,那么自然为空,对应关闭资源操作无需被执行。


四、包装:执行增删改操作,返回影响行数

JDBC连接数据库,进行增删改操作,可以归类为更新操作,这类操作的实现步骤基本一致,唯一的区别就是需要预处理的SQL语句不相同。

我们可以将SQL语句以及SQL语句中需要填充的参数作为形参传入,其他的步骤则包装成通用方法:

代码语言:javascript复制
//Object... params 代表传入不确定数量参数,用于传入`SQL`语句中需要填充的参数
protected int executeUpdate(String sql,Object... params)

。。。

我们还需要遍历传入的不确定数量的参数,填充进SQL语句中的 ?占位符:

代码语言:javascript复制
            if(params != null && params.length > 0){
                for(int i = 0;i < params.length;  i){
                    pstm.setObject(i 1,params[i]);
                }
            }

。。。

完整代码

代码语言:javascript复制
//执行更新,返回影响行数的方法
    protected int executeUpdate(String sql,Object... params){       
       try {
       //调用包装好的连接数据库方法
            connection = conn();
            //预处理对象
            pstm = connection.prepareStatement(sql);
            //参数填充不通用,靠参数传递进来
            if(params != null && params.length > 0){
                for(int i = 0;i < params.length;  i){
                    pstm.setObject(i 1,params[i]);
                }
            }

            return pstm.executeUpdate();//执行更新,返回影响行数

        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
        //调用包装好的资源关闭方法
            close( rs, pstm, connection);
        }
        return 0;//影响行数为0
    }

。。。

这么一来,我们在FruitDAOImpl实现类中的更新操作相关单精度方法也需要改动。

原本需要实现JDBC连接数据库完成更新操作步骤的过程,现在只需要将不通用的SQL语句 以及 需要填充的参数 传入到包装好的通用方法中即可:

代码语言:javascript复制
    @Override
    public boolean addFruit(Fruit fruit){
        //sql语句
        String sql = "insert into t_fruit values(0,?,?,?,?)";
        return super.executeUpdate(sql,fruit.getFname(),fruit.getPrice(),fruit.getFcount(),fruit.getRemark())>0;
    }

    @Override
    public boolean UpdateFruit(Fruit fruit) {
        //sql语句
        String sql = "update t_fruit set fcount = ? where fname like ?";
        return super.executeUpdate(sql,fruit.getFcount(),fruit.getFname()) >0;
    }

    @Override
    public boolean DelFruit(String fname) {
        //sql语句
        String sql = "delete from t_fruit where fname like ?";
        return super.executeUpdate(sql,fname) > 0;
    }


五:包装:执行查询操作,返回结果集输出

在包装查询操作相关方法前,我们可以先把给预处理参数填充参数的操作包装起来:

代码语言:javascript复制
    //给预处理参数设置参数
    protected void setParams(PreparedStatement psmt,Object... params) throws SQLException {
        if(params != null && params.length > 0 ){
            for(int i = 0;i < params.length;  i){
                psmt.setObject(i 1,params[i]);
            }
        }
    }

。。。

我们知道,当执行完查询操作后,会返回结果集,当我们输出结果集时,需要获取结果集中每一行的所有数据。

所以要遍历结果集的每一行数据,同时获取当中每一列的信息,将这一行的信息存储到一个Fruit类型的对象当中去,而保存每一行信息的Fruit类型对象放入List集合中输出。

但是,我们现在要将功能包装成通用方法,我们需要执行查询操作的表格不一定是我们之前使用的t_fruit表.

我们将来在调用查询数据的通用方法时,甚至不知道表格中有多少列,而这每列的数据又是什么类型的。

所以我们的通用方法返回的List集合中保存的类型需要先用泛型<T>代替,而这个类型<T>将会由继承通用方法所在类的子类决定:

我们通过子类调用父类的构造方法的机制,来获取<T>:

代码语言:javascript复制
//获取T的对象
    private Class entityClass;

    //构造方法
    public BaseDAO(){
        //getClass() 获取Class对象,我们当前创建的是FruitDAOImpl对象,new FruitDAOImpl();
        //那么子类的构造方法内部首先调用父类(BaseDAO)的空参构造器,
        //因此此处的getCalss()会被执行,但是获取的是子类FruitDAOImpl的Class
        //所以getGenericSuperclass()获取的是BaeDAO的class
        Type genericType = getClass().getGenericSuperclass();//获取泛型父类类型
        //强转成 ParameterizedType 参数化类型
        //getActualTypeArguments 获取实际的类型参数
        Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
        //只传入了一个参数,数组首位就是我们需要获取的<T>的真实类型
        Type actualType = actualTypeArguments[0];
        //actualType.getTypeName();获取类型名
        try {
            entityClass = Class.forName(actualType.getTypeName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

查询相关的通用方法展示:

代码语言:javascript复制
protected List<T> executeQuery(String sql,Object... params);

。。。

那么接下来,我们为了得到查询的结果集中每一列的数据信息,就需要借助结果集ResultSet类型对象 rs来获取结果集的元数据,里面包含我们想知道的信息(有多少列…每一列的类型等)

代码语言:javascript复制
            //通过rs可以获取结果集的元数据
            ResultSetMetaData rsmd = rs.getMetaData();

            //获取元数据列数
            int columnCount = rsmd.getColumnCount();
            
            //获取指定列的列名
            String columnName = rsmd.getColumnName(index);
            
            //获取当前行指定列的值
            Object columValue = rs.getObject(index);

我们需要将当前行每一列的信息整合到一个类型对象当中再放入集合,这个类型对象<T>我们已经通过继承的子类获取到了。

我们现在要做的就是将每一列对应列名的值依次放入类型的对象中:

第一个参数:类型的对象

第二个参数:获取到的列名columnName

第三个参数:当行指定列名columnName 对应的值columValue

代码语言:javascript复制
    //通过反射技术,给obj对象的property属性赋propertyValue值
    protected void setValue(Object obj, String property,Object propertyValue){
        Class clazz = obj.getClass();
        try {
            //获取property这个字符串对应的属性名,比如fid去找obj对象中对应的fid属性值
            Field field = clazz.getDeclaredField(property);
            if(field != null){
                field.setAccessible(true);//强制访问(即使private属性也能访问),防止属性为private
                field.set(obj,propertyValue);
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

上述操作我们需要遍历某一行的所有列,重复地进行:

代码语言:javascript复制
    // 执行查询,返回结果集并输出
    protected List<T> executeQuery(String sql,Object... params){
        List<T> list = new ArrayList<>();
        try {
            //加载驱动,连接数据库的方法
            connection = conn();
            //预处理对象
            pstm = connection.prepareStatement(sql);

            setParams(pstm,params);

            //执行查询,返回结果集
            rs = pstm.executeQuery();

            //通过rs可以获取结果集的元数据
            //元数据:描述结果集信息的数据(有哪些列,什么类型。。。)
            ResultSetMetaData rsmd = rs.getMetaData();

            //获取元数据列数
            int columnCount = rsmd.getColumnCount();

            while(rs.next()){
                T entity = (T) entityClass.newInstance();

                for(int i = 0;i < columnCount;  i){
                    String columnName = rsmd.getColumnName(i   1);
                    Object columValue = rs.getObject(i   1);
                    setValue(entity,columnName,columValue);
                }
                list.add(entity);

            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } finally{
            close( rs, pstm, connection);
        }
        return list;
    }


六、包装:查询指定数据,返回单个实体对象

该返回单个实体的查询操作思路与上文的通用查询操作相似:

代码语言:javascript复制
    //执行查询,返回单个实体对象
    protected T load(String sql,Object... params){
        try {
            //加载驱动,连接数据库的方法
            connection = conn();
            //预处理对象
            pstm = connection.prepareStatement(sql);

            setParams(pstm,params);

            //执行查询,返回结果集
            rs = pstm.executeQuery();

            //通过rs可以获取结果集的元数据
            //元数据:描述结果集信息的数据(有哪些列,什么类型。。。)
            ResultSetMetaData rsmd = rs.getMetaData();

            //获取元数据列数
            int columnCount = rsmd.getColumnCount();

            if(rs.next()){
                T entity = (T) entityClass.newInstance();

                for(int i = 0;i < columnCount;  i){
                    String columnName = rsmd.getColumnName(i   1);
                    Object columValue = rs.getObject(i   1);
                    setValue(entity,columnName,columValue);
                }
                return entity;

            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } finally{
            close( rs, pstm, connection);
        }
        return null;
    }


七、BaseDAO类:存放优化后的通用方法

BaseDAO类完整代码

代码语言:javascript复制
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author .29.
 * @create 2022-09-25 19:30
 */
public abstract class BaseDAO<T> {
    public final String DRIVER = "com.mysql.cj.jdbc.Driver";
    public final String URL = "jdbc:mysql://localhost:3306/fruitdb?useSSL=false&useUnicode=true&CharacterEncoding=utf-8";
    public final String USER = "root" ;
    public final String PSW = "" ;

    protected Connection connection;
    protected PreparedStatement pstm;
    protected ResultSet rs;

    //获取T的对象
    private Class entityClass;

    //构造方法
    public BaseDAO(){
        //getClass() 获取Class对象,我们当前创建的是FruitDAOImpl对象,new FruitDAOImpl();
        //那么子类的构造方法内部首先调用父类(BaseDAO)的空参构造器,
        //因此此处的getCalss()会被执行,但是获取的是子类FruitDAOImpl的Class
        //所以getGenericSuperclass()获取的是BaeDAO的class
        Type genericType = getClass().getGenericSuperclass();//获取泛型父类类型
        //强转成 ParameterizedType 参数化类型
        //getActualTypeArguments 获取实际的类型参数
        Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
        //只传入了一个参数,数组首位就是我们需要获取的<T>的真实类型
        Type actualType = actualTypeArguments[0];
        //actualType.getTypeName();获取类型名
        try {
            entityClass = Class.forName(actualType.getTypeName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    //将加载驱动,连接数据库的操作包装成方法,减少代码复用率
    protected Connection conn(){
        try {
            //加载驱动
            Class.forName(DRIVER);
            //数据库管理器,连接数据库
            connection = DriverManager.getConnection(URL, USER, PSW);
            return connection;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    //将关闭资源的操作包装成方法
    protected  void close(ResultSet rs,PreparedStatement pstm,Connection connection){
        try {
            if(rs != null)
                rs.close();
            if(pstm != null)
                pstm.close();
            if(connection != null)
                connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    //执行更新,返回影响行数的方法(如果是插入操作,返回自增列主键值)
    protected int executeUpdate(String sql,Object... params){//... params不确定数量的参数
        boolean insertFlag = false;
        insertFlag = sql.trim().toUpperCase().startsWith("INSERT");
        try {
            connection = conn();
            //(sql语句不通用,靠参数传递进来)
            //sql语句
            //String sql = "update t_fruit set fcount = ? where fname like ?";
            //预处理对象
            if(insertFlag){
                pstm = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
            }else{
                pstm = connection.prepareStatement(sql);
            }

            setParams(pstm,params);
            int count = pstm.executeUpdate();

            rs = pstm.getGeneratedKeys();

            if(rs.next()){
                return ((Long)rs.getLong(1)).intValue();
            }

            //参数填充也不通用,也靠参数传递进来
            if(params != null && params.length > 0){
                for(int i = 0;i < params.length;  i){
                    pstm.setObject(i 1,params[i]);
                }
            }

            return count;//执行更新,返回影响行数

        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
            close( rs, pstm, connection);
        }
        return 0;//影响行数为0
    }


    //通过反射技术,给obj对象的property属性赋propertyValue值
    protected void setValue(Object obj, String property,Object propertyValue){
        Class clazz = obj.getClass();
        try {
            //获取property这个字符串对应的属性名,比如fid去找obj对象中对应的fid属性值
            Field field = clazz.getDeclaredField(property);
            if(field != null){
                field.setAccessible(true);//强制访问(即使private属性也能访问),防止属性为private
                field.set(obj,propertyValue);
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }



    //执行查询,返回单个实体对象
    protected T load(String sql,Object... params){
        try {
            //加载驱动,连接数据库的方法
            connection = conn();
            //预处理对象
            pstm = connection.prepareStatement(sql);

            setParams(pstm,params);

            //执行查询,返回结果集
            rs = pstm.executeQuery();

            //通过rs可以获取结果集的元数据
            //元数据:描述结果集信息的数据(有哪些列,什么类型。。。)
            ResultSetMetaData rsmd = rs.getMetaData();

            //获取元数据列数
            int columnCount = rsmd.getColumnCount();

            if(rs.next()){
                T entity = (T) entityClass.newInstance();

                for(int i = 0;i < columnCount;  i){
                    String columnName = rsmd.getColumnName(i   1);
                    Object columValue = rs.getObject(i   1);
                    setValue(entity,columnName,columValue);
                }
                return entity;

            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } finally{
            close( rs, pstm, connection);
        }
        return null;
    }


    //给预处理参数设置参数
    protected void setParams(PreparedStatement psmt,Object... params) throws SQLException {
        if(params != null && params.length > 0 ){
            for(int i = 0;i < params.length;  i){
                psmt.setObject(i 1,params[i]);
            }
        }
    }

    // 执行查询,返回结果集并输出
    protected List<T> executeQuery(String sql,Object... params){
        List<T> list = new ArrayList<>();
        try {
            //加载驱动,连接数据库的方法
            connection = conn();
            //预处理对象
            pstm = connection.prepareStatement(sql);

            setParams(pstm,params);

            //执行查询,返回结果集
            rs = pstm.executeQuery();

            //通过rs可以获取结果集的元数据
            //元数据:描述结果集信息的数据(有哪些列,什么类型。。。)
            ResultSetMetaData rsmd = rs.getMetaData();

            //获取元数据列数
            int columnCount = rsmd.getColumnCount();

            while(rs.next()){
                T entity = (T) entityClass.newInstance();

                for(int i = 0;i < columnCount;  i){
                    String columnName = rsmd.getColumnName(i   1);
                    Object columValue = rs.getObject(i   1);
                    setValue(entity,columnName,columValue);
                }
                list.add(entity);

            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } finally{
            close( rs, pstm, connection);
        }
        return list;
    }
}


八、优化后的实现类FruitDAOImpl

实现类FruitDAOImpl优化后代码

代码语言:javascript复制
import com.haojin.fruit.dao.FruitDAO;
import com.haojin.fruit.dao.base.BaseDAO;
import com.haojin.fruit.pojo.Fruit;
import java.util.List;

/**
 * @author .29.
 * @create 2022-09-23 17:56
 */
public class FruitDAOImpl extends BaseDAO<Fruit> implements FruitDAO {


    @Override
    public List<Fruit> getFruitList() {
        String sql = "select * from t_fruit";
        return executeQuery(sql);
    }

    @Override
    public boolean addFruit(Fruit fruit){
        //sql语句
        String sql = "insert into t_fruit values(0,?,?,?,?)";
        //如果是insert操作,count返回的是自增列的值
        int count = super.executeUpdate(sql,fruit.getFname(),fruit.getPrice(),fruit.getFcount(),fruit.getRemark());

        return count>0;
    }

    @Override
    public boolean UpdateFruit(Fruit fruit) {
        //sql语句
        String sql = "update t_fruit set fcount = ? where fname like ?";
        return super.executeUpdate(sql,fruit.getFcount(),fruit.getFname()) >0;
    }

    @Override
    public boolean DelFruit(String fname) {
        //sql语句
        String sql = "delete from t_fruit where fname like ?";
        return super.executeUpdate(sql,fname) > 0;
    }

    @Override
    public Fruit getFruitByFname(String fname) {
        String sql = "select * from t_fruit where fname like ?";
        return load(sql,fname);
    }


}


九、总结

到这里,我们的代码优化就完成啦…

可能大家会问,虽然接口实现类FruitDAOImpl看起来好像是简介了很多,但是又多出来一个存放通用方法的BaseDAO类,看似被简化的代码其实就是被放到BaseDAO类中去了而已。

但是我们要意识到,现在完成的通用方法,即使在将来需要对不同的表进行更新或者查询的操作时也能直接拿来用,只需要继承BaseDAO类,调用包装好的通用方法即可。

这么一来,实现的代码都会像优化后的FruitDAOImpl类一般简洁,实实在在地提升了整个系统的功能易拓展性以及优化了内存的空间。做到跟冗余代码说拜拜…

作者

0 人点赞