反射实例之实现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插入操作的主要内容,如果未能解决你的问题,请参考以下文章