反射实例之实现SQL插入操作

Posted 二木成林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了反射实例之实现SQL插入操作相关的知识,希望对你有一定的参考价值。

需求

如果用过MybatisPlus的人就知道Mapper接口只需要实现BaseMapper<T>就可以进行单表查询了,而不需要写任何方法。我们的需求就是也写这样一个BaseMapper<T>接口,里面定义一个insert(T t)默认方法,只需要传入要插入的实体类对象,就可以实现插入操作,不需要写任何SQL操作。

使用过程如下:

public class Test {
    public static void main(String[] args) throws SQLException, InvocationTargetException, IllegalAccessException {
        User user = new User(20, "刘三刀", "123456", "1008611", "1008611@163.com", "我是一名用户", new Date());
        UserMapper userMapper = new UserMapper();
        int result = userMapper.insert(user);
        System.out.println(result);
    }
}

class UserMapper implements BaseMapper<User> {

}

实现

我们可以通过反射来实现这一功能。

首先,我们需要创建一个连接数据库的静态方法,代码如下:

class JDBCUtil {
    public static Connection getConnection() {
        Connection connection = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost/test";
            String user = "root";
            String password = "root";
            connection = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }
}

接着就是创建一个名为BaseMapper的接口类,在该接口中定义一个insert默认方法,传入泛型参数。

interface BaseMapper<T> {
    /**
     * 向数据库内插入一条记录,插入SQL语句如例:insert into user(id,username,password,phone,email,memo,insert_time) values(20,'刘三刀','123456','1008611','1008611@163.com','我是一名用户','2021-08-10 21:28:35')
     *
     * @param t 带插入的实体类对象
     * @return 返回受影响行数
     * @throws IllegalAccessException
     * @throws SQLException
     * @throws InvocationTargetException
     */
    default int insert(T t) throws IllegalAccessException, SQLException, InvocationTargetException {
        // 使用泛型参数,是因为不清楚会传什么实体类,所以使用泛型
        // 获取输入参数t的Class类对象
        Class<?> tClass = t.getClass();
        // 使用StringBuilder来存放SQL语句
        StringBuilder sb = new StringBuilder();
        // 插入SQL语句的头部"insert into "
        sb.append("insert into ");
        // 获取类名,但需要将类名转换成小写,虽然在MySQL中不区分大小写
        sb.append(tClass.getSimpleName().toLowerCase());
        sb.append("(");
        // 获取Method数组,即获取t所表示的类中的所有方法
        Method[] methods = tClass.getDeclaredMethods();
        // 表示t所表示的类中"getXXX"方法的个数,例如:getUsername、getPassword等方法
        int i = 0;
        // 循环遍历方法数组
        for (Method method : methods) {
            if (method.getName().startsWith("get")) {// 用于判断方法名是否以"get"前缀开头,即只判断"getXXX"方法,而不处理如"setXXX"、"toString"等方法
                // 这里调用的是如getUsername()、getPassword()方法,不需要传入返回值,但可以通过这些方法获取该get方法所对应的属性值
                // 调用执行方法,并获取返回值,但没有传入参数
                Object o = method.invoke(t);
                // 之所以要判断getXXX方法的返回值,是只需要设置了属性值的getXXX方法
                if (o != null) {
                    // 添加方法名,其实是属性名,如实体类中的username、password、email等属性
                    sb.append(method.getName().toLowerCase().substring(3));
                    sb.append(",");
                    // 记录getXXX方法的个数
                    i++;
                }
            }
        }
        // 除去最后一个逗号,添加一个右括号
        String s = sb.substring(0, sb.length() - 1) + ")";
        StringBuilder result = new StringBuilder(s);
        result.append(" values(");
        for (int j = 0; j < i; j++) {
            result.append("?,");
        }
        // 到这一步,一个完整的SQL语句完成了,只是没有传入参数
        // 例如:insert into user(phone,username,insert_time,email,memo,id,password) values(?,?,?,?,?,?,?)
        String sql = result.substring(0, result.length() - 1) + ")";

        // 接下来就是进行插入操作
        // 获取数据库连接
        Connection connection = JDBCUtil.getConnection();
        // 根据Connection创建PreparedStatement对象
        PreparedStatement ps = connection.prepareStatement(sql);
        // 该参数表示是第几个待传入的参数,在PreparedStatement类中设置参数的方法的序号是从1开始的
        int j = 1;
        for (Method method : methods) {
            // 还是循环方法数组,判断getXXX方法
            if (method.getName().startsWith("get")) {
                // 执行实体类中getXXX方法,获取返回值,如getUsername()方法就可以获取username属性的属性值
                Object o = method.invoke(t);
                if (o != null) {
                    // 为PreparedStatement中第j个参数传递参数,使用PreparedStatement传递参数而不是使用字符串拼接是为了防止SQL注入问题
                    ps.setObject(j, o);
                    j++;
                }
            }
        }
        // 可以打印完整的SQL语句
        System.out.println(ps);
        // 执行SQL插入操作
        return ps.executeUpdate();
    }
}

事实上,上面的执行代码有问题的,因为我们在数据库表中设置了id字段为主键。

所以不应该传入id字段,并且需要创建一个除id字段之外所有其他字段的构造方法,完整的实例代码如下:

public class Test {
    public static void main(String[] args) throws SQLException, InvocationTargetException, IllegalAccessException {
        User user = new User("刘三刀", "123456", "1008611", "1008611@163.com", "我是一名用户", new Date());
        UserMapper userMapper = new UserMapper();
        int result = userMapper.insert(user);
        System.out.println(result);
    }
}

class UserMapper implements BaseMapper<User> {

}

class User {

    private int id;
    private String username;
    private String password;
    private String phone;
    private String email;
    private String memo;
    private Date insert_time;

    public User() {
    }

    public User(String username, String password, String phone, String email, String memo, Date insert_time) {
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
        this.memo = memo;
        this.insert_time = insert_time;
    }

    public User(int id, String username, String password, String phone, String email, String memo, Date insert_time) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
        this.memo = memo;
        this.insert_time = insert_time;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }

    public Date getInsert_time() {
        return insert_time;
    }

    public void setInsert_time(Date insert_time) {
        this.insert_time = insert_time;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\\'' +
                ", password='" + password + '\\'' +
                ", phone='" + phone + '\\'' +
                ", email='" + email + '\\'' +
                ", memo='" + memo + '\\'' +
                ", insert_time=" + insert_time +
                '}';
    }
}

class JDBCUtil {
    public static Connection getConnection() {
        Connection connection = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost/test";
            String user = "root";
            String password = "root";
            connection = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }
}

interface BaseMapper<T> {
    /**
     * 向数据库内插入一条记录,插入SQL语句如例:insert into user(id,username,password,phone,email,memo,insert_time) values(20,'刘三刀','123456','1008611','1008611@163.com','我是一名用户','2021-08-10 21:28:35')
     *
     * @param t 带插入的实体类对象
     * @return 返回受影响行数
     * @throws IllegalAccessException
     * @throws SQLException
     * @throws InvocationTargetException
     */
    default int insert(T t) throws IllegalAccessException, SQLException, InvocationTargetException {
        // 使用泛型参数,是因为不清楚会传什么实体类,所以使用泛型
        // 获取输入参数t的Class类对象
        Class<?> tClass = t.getClass();
        // 使用StringBuilder来存放SQL语句
        StringBuilder sb = new StringBuilder();
        // 插入SQL语句的头部"insert into "
        sb.append("insert into ");
        // 获取类名,但需要将类名转换成小写,虽然在MySQL中不区分大小写
        sb.append(tClass.getSimpleName().toLowerCase());
        sb.append("(");
        // 获取Method数组,即获取t所表示的类中的所有方法
        Method[] methods = tClass.getDeclaredMethods();
        // 表示t所表示的类中"getXXX"方法的个数,例如:getUsername、getPassword等方法
        int i = 0;
        // 循环遍历方法数组
        for (Method method : methods) {
            if (method.getName().startsWith("get")) {// 用于判断方法名是否以"get"前缀开头,即只判断"getXXX"方法,而不处理如"setXXX"、"toString"等方法
                // 这里调用的是如getUsername()、getPassword()方法,不需要传入返回值,但可以通过这些方法获取该get方法所对应的属性值
                // 调用执行方法,并获取返回值,但没有传入参数
                Object o = method.invoke(t);
                // 之所以要判断getXXX方法的返回值,是只需要设置了属性值的getXXX方法
                if (o != null) {
                    // 添加方法名,其实是属性名,如实体类中的username、password、email等属性
                    sb.append(method.getName().toLowerCase().substring(3));
                    sb.append(",");
                    // 记录getXXX方法的个数
                    i++;
                }
            }
        }
        // 除去最后一个逗号,添加一个右括号
        String s = sb.substring(0, sb.length() - 1) + ")";
        StringBuilder result = new StringBuilder(s);
        result.append(" values(");
        for (int j = 0; j < i; j++) {
            result.append("?,");
        }
        // 到这一步,一个完整的SQL语句完成了,只是没有传入参数
        // 例如:insert into user(phone,username,insert_time,email,memo,id,password) values(?,?,?,?,?,?,?)
        String sql = result.substring(0, result.length() - 1) + ")";

        // 接下来就是进行插入操作
        // 获取数据库连接
        Connection connection = JDBCUtil.getConnection();
        // 根据Connection创建PreparedStatement对象
        PreparedStatement ps = connection.prepareStatement(sql);
        // 该参数表示是第几个待传入的参数,在PreparedStatement类中设置参数的方法的序号是从1开始的
        int j = 1;
        for (Method method : methods) {
            // 还是循环方法数组,判断getXXX方法
            if (method.getName().startsWith("get")) {
                // 执行实体类中getXXX方法,获取返回值,如getUsername()方法就可以获取username属性的属性值
                Object o = method.invoke(t);
                if (o != null) {
                    // 为PreparedStatement中第j个参数传递参数,使用PreparedStatement传递参数而不是使用字符串拼接是为了防止SQL注入问题
                    ps.setObject(j, o);
                    j++;
                }
            }
        }
        // 可以打印完整的SQL语句
        System.out.println(ps);
        // 执行SQL插入操作
        return ps.executeUpdate();
    }
}

结果如下:

 

优化

 对于主键id的值,如果传入设置的值比已有的主键值大,那插入会是成功的,如果小则会抛出异常,但是我们看插入的SQL是将id设置为0进行插入,这也是能够插入成功的。

但注意观察打印的SQL发现插入的字段顺序是不一样的,不是按照实体类的属性名顺序排序的,因为我们是通过getXXX方法名来获取属性名的,而方法数组并不是按照属性顺序排序。但这其实不是很重要,因为会正确插入数据。

但我们如果想要解决主键id的这个问题,也有办法,引入注解,在如User这个实体类的id属性上添加一个@PrimaryKey注解,表示该属性是数据库表中的主键,它会自增长,不需要在SQL语句中设置该字段。

首先创建一个PrimaryKey注解类,如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface PrimaryKey {
    String name() default "id";
}

接着是在User实体类中使用该注解,即在id属性上添加@PrimaryKey注解。

class User {
    @PrimaryKey
    private int id;
    private String username;
    private String password;
    private String phone;
    private String email;
    private String memo;
    private Date insert_time;

    public User() {
    }

    public User(String username, String password, String phone, String email, String memo, Date insert_time) {
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
        this.memo = memo;
        this.insert_time = insert_time;
    }

    public User(int id, String username, String password, String phone, String email, String memo, Date insert_time) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
        this.memo = memo;
        this.insert_time = insert_time;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }

    public Date getInsert_time() {
        return insert_time;
    }

    public void setInsert_time(Date insert_time) {
        this.insert_time = insert_time;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\\'' +
                ", password='" + password + '\\'' +
                ", phone='" + phone + '\\'' +
                ", email='" + email + '\\'' +
                ", memo='" + memo + '\\'' +
                ", insert_time=" + insert_time +
                '}';
    }
}

接着是添加对该注解的判断,注意,我们这里添加的Field数组,而不是Method数组。

interface BaseMapper<T> {
    /**
     * 向数据库内插入一条记录,插入SQL语句如例:insert into user(id,username,password,phone,email,memo,insert_time) values(20,'刘三刀','123456','1008611','1008611@163.com','我是一名用户','2021-08-10 21:28:35')
     *
     * @param t 带插入的实体类对象
     * @return 返回受影响行数
     * @throws IllegalAccessException
     * @throws SQLException
     * @throws InvocationTargetException
     */
    default int insert(T t) throws IllegalAccessException, SQLException, InvocationTargetException {
        // 使用泛型参数,是因为不清楚会传什么实体类,所以使用泛型
        // 获取输入参数t的Class类对象
        Class<?> tClass = t.getClass();
        // 使用StringBuilder来存放SQL语句
        StringBuilder sb = new StringBuilder();
        // 插入SQL语句的头部"insert into "
        sb.append("insert into ");
        // 获取类名,但需要将类名转换成小写,虽然在MySQL中不区分大小写
        sb.append(tClass.getSimpleName().toLowerCase());
        sb.append("(");
        // 获取属性数组,即获取t所表示的实体类中的所有属性,而不是该类中的所有方法
        Field[] fields = tClass.getDeclaredFields();
        int i = 0;
        for (Field field : fields) {
            // 添加对@PrimaryKey注解的判断,如果属性上有该注解则跳过本次循环,不向SQL语句中添加该属性
            if (field.getAnnotation(PrimaryKey.class) != null) {
                continue;
            }
            sb.append(field.getName());// 添加属性名
            sb.append(",");
            i++;
        }
        // 除去最后一个逗号,添加一个右括号
        String s = sb.substring(0, sb.length() - 1) + ")";
        StringBuilder result = new StringBuilder(s);
        result.append(" values(");
        for (int j = 0; j < i; j++) {
            result.append("?,");
        }
        // 到这一步,一个完整的SQL语句完成了,只是没有传入参数
        // 例如:insert into user(phone,username,insert_time,email,memo,id,password) values(?,?,?,?,?,?,?)
        String sql = result.substring(0, result.length() - 1) + ")";

        // 接下来就是进行插入操作
        // 获取数据库连接
        Connection connection = JDBCUtil.getConnection();
        // 根据Connection创建PreparedStatement对象
        PreparedStatement ps = connection.prepareStatement(sql);
        // 该参数表示是第几个待传入的参数,在PreparedStatement类中设置参数对应值的方法的序号是从1开始的
        int j = 1;
        for (Field field : fields) {
            // 添加对@PrimaryKey注解的判断,如果属性上有该注解则跳过本次循环,不向SQL语句中添加该属性
            if (field.getAnnotation(PrimaryKey.class) != null) {
                continue;
            }
            field.setAccessible(true);
            ps.setObject(j, field.get(t));
            j++;
        }
        // 可以打印完整的SQL语句
        System.out.println(ps);
        // 执行SQL插入操作
        return ps.executeUpdate();
    }
}

核心代码如下:

 优化后的完整实例代码如下:

public class Test {
    public static void main(String[] args) throws SQLException, InvocationTargetException, IllegalAccessException {
        User user = new User(15, "刘三刀", "123456", "1008611", "1008611@163.com", "我是一名用户", new Date());
        UserMapper userMapper = new UserMapper();
        int result = userMapper.insert(user);
        System.out.println(result);
    }
}

class UserMapper implements BaseMapper<User> {

}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface PrimaryKey {
    String name() default "id";
}

class User {
    @PrimaryKey
    private int id;
    private String username;
    private String password;
    private String phone;
    private String email;
    private String memo;
    private Date insert_time;

    public User() {
    }

    public User(String username, String password, String phone, String email, String memo, Date insert_time) {
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
        this.memo = memo;
        this.insert_time = insert_time;
    }

    public User(int id, String username, String password, String phone, String email, String memo, Date insert_time) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
        this.memo = memo;
        this.insert_time = insert_time;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }

    public Date getInsert_time() {
        return insert_time;
    }

    public void setInsert_time(Date insert_time) {
        this.insert_time = insert_time;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\\'' +
                ", password='" + password + '\\'' +
                ", phone='" + phone + '\\'' +
                ", email='" + email + '\\'' +
                ", memo='" + memo + '\\'' +
                ", insert_time=" + insert_time +
                '}';
    }
}

class JDBCUtil {
    public static Connection getConnection() {
        Connection connection = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost/test";
            String user = "root";
            String password = "root";
            connection = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }
}

interface BaseMapper<T> {
    /**
     * 向数据库内插入一条记录,插入SQL语句如例:insert into user(id,username,password,phone,email,memo,insert_time) values(20,'刘三刀','123456','1008611','1008611@163.com','我是一名用户','2021-08-10 21:28:35')
     *
     * @param t 带插入的实体类对象
     * @return 返回受影响行数
     * @throws IllegalAccessException
     * @throws SQLException
     * @throws InvocationTargetException
     */
    default int insert(T t) throws IllegalAccessException, SQLException, InvocationTargetException {
        // 使用泛型参数,是因为不清楚会传什么实体类,所以使用泛型
        // 获取输入参数t的Class类对象
        Class<?> tClass = t.getClass();
        // 使用StringBuilder来存放SQL语句
        StringBuilder sb = new StringBuilder();
        // 插入SQL语句的头部"insert into "
        sb.append("insert into ");
        // 获取类名,但需要将类名转换成小写,虽然在MySQL中不区分大小写
        sb.append(tClass.getSimpleName().toLowerCase());
        sb.append("(");
        // 获取属性数组,即获取t所表示的实体类中的所有属性,而不是该类中的所有方法
        Field[] fields = tClass.getDeclaredFields();
        int i = 0;
        for (Field field : fields) {
            // 添加对@PrimaryKey注解的判断,如果属性上有该注解则跳过本次循环,不向SQL语句中添加该属性
            if (field.getAnnotation(PrimaryKey.class) != null) {
                continue;
            }
            sb.append(field.getName());// 添加属性名
            sb.append(",");
            i++;
        }
        // 除去最后一个逗号,添加一个右括号
        String s = sb.substring(0, sb.length() - 1) + ")";
        StringBuilder result = new StringBuilder(s);
        result.append(" values(");
        for (int j = 0; j < i; j++) {
            result.append("?,");
        }
        // 到这一步,一个完整的SQL语句完成了,只是没有传入参数
        // 例如:insert into user(phone,username,insert_time,email,memo,id,password) values(?,?,?,?,?,?,?)
        String sql = result.substring(0, result.length() - 1) + ")";

        // 接下来就是进行插入操作
        // 获取数据库连接
        Connection connection = JDBCUtil.getConnection();
        // 根据Connection创建PreparedStatement对象
        PreparedStatement ps = connection.prepareStatement(sql);
        // 该参数表示是第几个待传入的参数,在PreparedStatement类中设置参数对应值的方法的序号是从1开始的
        int j = 1;
        for (Field field : fields) {
            // 添加对@PrimaryKey注解的判断,如果属性上有该注解则跳过本次循环,不向SQL语句中添加该属性
            if (field.getAnnotation(PrimaryKey.class) != null) {
                continue;
            }
            field.setAccessible(true);
            ps.setObject(j, field.get(t));
            j++;
        }
        // 可以打印完整的SQL语句
        System.out.println(ps);
        // 执行SQL插入操作
        return ps.executeUpdate();
    }
}

即使是设置了id属性的值,也不会抛出任何异常,也不会插入到数据库中。

以上是关于反射实例之实现SQL插入操作的主要内容,如果未能解决你的问题,请参考以下文章

Java反射之剖析方法

OpenGL片段着色器不照亮场景

Mybatis 实现批量插入和批量删除源码实例

C# 反射之Activator用法举例

Python操作excel进行插入删除行操作实例演示,利用xlwings库实现

Python操作excel进行插入删除行操作实例演示,利用xlwings库实现