0
点赞
收藏
分享

微信扫一扫

JDBC与mysql数据类型的映射&通用crud操作的BaseDAO<T>的简单实现&dbutils工具类的简单使用

夏天的枫_ 2022-04-26 阅读 119

目录

1 JDBC与mysql常用数据类型的对应

1.1 日期类型

1.2  整形

1.3  浮点数与定点数

1.4  文本字符串类型

1.5 二进制

 1.6 json类型

 1.7 总结

2  实现一个BaseDAO

2.1 三层DAO关系

2.2 javabean

2.3 实现BaseDAO

3 bdUtils工具类

3.1 简单说明其常用方法

3.2  ResultSetHandler的探索

3.3  自定义类实现ResultSetHandler<>接口

3.4 改变实体类setXxx与getXxx方法


昨天通过druid连接mysql,使用dbutils工具类实现基本的crud操作,遇见了一个问题,后来发现是我javabean对象的数据类型错了,与数据库对不上号,今天总结一下mysql常用的数据类型在jdbc中的对应,对DAO以及dbUtils的理解再此奉上。

1 JDBC与mysql常用数据类型的对应

1.1 日期类型

        日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。我们需要知道数据的 时间标签,从而进行数据查询、统计和处理。 在mysql中常用的日期类型有:

  • year            年
  • date            年-月-日
  • time            时:分:秒
  • datetime    年-月-日  时:分:秒
  • timestamp 年-月-日  时:分:秒

 我们现在建一个表test_time

 

 在java中连接数据库,打印这一行的值和类型,类型通过反射获取

Connection conn = JDBCUtilsByDruid.getConnection();
String sql = "select * from test_date";
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
for (int i = 0; i < columnCount; i++) {
String columnLabel = rsmd.getColumnLabel(i + 1);//得到标签名
Object columnValue = rs.getObject(i + 1);//当前字段值
String typeName = columnValue.getClass().getTypeName();//当前字段在java中的类型
System.out.println(columnLabel + " :" + columnValue + " ,java类型:" + typeName);
}
}

得到运行结果:

 我们在定义对应的字段的类型时,就可以按照上面的结果导包。注意一点,我都是用的getObject(index)接收数据,因为不确定数据类型,但是可以看出值的实际类型不是Object,通过强制转型可以不报错的赋值给他实际类型的引用。

1.2  整形

演示5个整形,加一个无符号整形

类型字节有符号范围无符号范围
tinyint1-128~127(-2^7~2^7-1)0~255(0~2^8-1)
smallint2

-32768~32767

0~65535

mediumint3

-8388608~8388607

0~16777215

int4

2147483648~2147483647

0~4294967295

bigint8-2^63~2^63-10~2^64-1

这里说明一下,1个字节所表示的最大二进制无符号整数:11111111(8个1),对应10进制就是255。

下面建表:

 

 通过jdbc获取该行的所有数据,并打印

 

 我们可以看到bigint,无符号整数,定宽度并用0填充的是Long,其余全是Integer类型的。

1.3  浮点数与定点数

  • float
  • double
  • numeric(m,n)     浮点数
  • decimal(m,n)     定点数

在官方文档中表名,未来float和double可能被抛弃,所以不建议使用。

 

 

 BigDecimal是java.math包提供的一个API类,来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。传统的float和double是近似值保存数据,就拿一个经典例子,1.0-0.9打印结果0.09999999999998,而BigDecimal就可以很好的解决这个问题。

如果你要转化为其他类型,BigDecimal提供有方法

  • toString()                 将BigDecimal对象的数值转换成字符串。
  • doubleValue()         将BigDecimal对象中的值以双精度数返回。
  • floatValue()              将BigDecimal对象中的值以单精度数返回。
  • longValue()              将BigDecimal对象中的值以长整数返回。
  • intValue()                  将BigDecimal对象中的值以整数返回。

1.4  文本字符串类型

其实文本字符串类型没啥好说的,就是返回String,四个text就写一个为代表。

 text文本类型,可以存比较大的文本段,搜索速度稍慢,因此如果不是特别大的内容,建议使用 char和 varchar来代替。还有 TEXT 类型不用加默认值,加了也没用。而且 text blob 类型的数据删除后容易导致 “空洞 ,使得文件碎片比较多,所以频繁使用的表不建议包含 TEXT 类型字段,建议单独分出去,单独用 一个表。

然后是枚举类型enumset类型

enum类型字段是要求从给定的值中选一个

set类型是要求从给定的多个值中选一个或多个进行组合,用双(单)引号全部包着,用英文的逗号隔开

 

 返回类型全是String。

1.5 二进制

 主要有三个类型

  • binary
  • varbinary
  • bolb

先建表

再传入一数据,再得到数据,打印 

 Connection conn = JDBCUtilsByDruid.getConnection();
//向表中插入一个blob类型的数据
String sql="insert into test_binary values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
//填充撒4个占位符
ps.setObject(1,"伊蕾娜");
ps.setObject(2,"零".getBytes(StandardCharsets.UTF_8));
FileInputStream fs1 = new FileInputStream("src/typeConversion/binary.txt");
FileInputStream fs2 = new FileInputStream("src/typeConversion/零.jpg");
ps.setObject(3,fs1);
ps.setObject(4,fs2);
ps.execute();

String sql1 = "select * from test_binary";
ps = conn.prepareStatement(sql1);
ResultSet rs = ps.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
for (int i = 0; i < columnCount; i++) {
String columnLabel = rsmd.getColumnLabel(i + 1);//得到标签名
Object columnValue = rs.getObject(i + 1);//当前字段值
String typeName = columnValue.getClass().getTypeName();//当前字段在java中的类型
System.out.println(columnLabel + " :" + columnValue + " ,java类型:" + typeName);
}
}

 

 好了,差不多都是这些类型。实际中通常不会向数据库中存大数据,因为会加重数据库的负担,通常都是向数据库存文件地址。

 1.6 json类型

json是一种轻量级的 数据交换格式 。简洁和清晰的层次结构使得 json成 为理想的数据交换语言。它易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效 率。JSON 可以将 JavaScript 对象中表示的一组数据转换为字符串,然后就可以在网络或者程序之间轻 松地传递这个字符串,并在需要的时候将它还原为各编程语言所支持的数据格式。

创建一个json字段

 查询

 

 在jdbc中得到一个String类型的结果

 1.7 总结

类型mysqlJDBC
整形

int

int unsigned

int (L) zerofill

java.langInteger
bigIntjava.lang.Long
浮点型与定点型

            float

java.langFloat
doublejava.langDouble

numeic(m,n)

decimal(m,n)

java.math.Decimal
日期类型

year(yyyy或yy)

date(yyyy-MM-DD)

java.sql.Date
time(HH:mm:ss)java.sql.time
datetime(yyyy-MM-DD HH:mm:ss)java.time.LocalDateTime
timstamp(yyyy-MM-DD HH:mm:ss)java.sql.Timestamp
文本类型

varchar(m)

char(m)

text(L)

java.lang.String
Enum枚举类型enum(a,b,......)java.lang.String
Set类型set(a,b,c.......)java.lang.String
json类型jsonjava.lang.String

2  实现一个BaseDAO

2.1 三层DAO关系

Dao专门负责一些对数据库的访问,然后是业务处理层,用来使用户和数据库交互的中间层,可以对用户的请求做出处理的,最一层就是用户使用的层。

control层负责控制,会有参数传进来,告诉你具体做什么,然后传到service服务层,这层只显示服务的名称,具体操作还是到dao层里执行,其实一层dao就可以解决所有问题,不过三层看起来层次更加清晰。

为什么要写一个BaseDAO?因为当对数据库有大量的crud操作的时候,这些操作都有一个公共的部分代码,如果不写一个基本的通用操作,那么就会造成大量的代码冗余,大大增加开发成本,就好比编写函数是一个道理,说白了就是提高代码复用率。

如果说cotroller就像服务员提交菜单(客户端业务),那么service就是厨师,根据菜单作出对应的菜(具体业务逻辑),而做菜的原料就是厨房打杂(DAO)提供,而BaseDAO就像加工原材料的基础技能,每个打杂(DAO)都只对一种原料(表)进行基本加工。这三层子上而下互不干扰。

2.2 javabean

ORM中:

  • 类  —> 表
  • 属性 —>字段
  • 对象 —> 记录

javabean也就是实体类

特点:

  • 必须为public class
  • 必须提供私有属性,属性名与表中字段一致
  • 提供公共的setXXX和getXXX,XXX为属性名
  • 提供公共的无参构造器

2.3 实现BaseDAO

我们实现一个通用的增删改查,要传入

  • Connection conn        数据库的连接
  • String sql                     执行的sql语句
  • Object...params         传入的要填充占位符的参数,由于类型未知,所以使用Object

 2.3.1 先实现dml(增删改)操作

 public void update(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(sql);
//填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
ps.execute();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("update出错");
} finally {
JDBCUtils.closeResource(null, ps);
}
}

为什么捕获了异常还要抛出异常,因为在进行事务的时候,如果抛出异常,在Filter会捕获异常,进行事务回滚,还有catch异常还可以使用finally语句,进行资源的关闭,就是prepareStatement和ResultSet的关闭,这里因为是dml操作,所以没有ResultSet,而连接conn则是在Filter进行关闭。

2.3.2 查询返回单个特殊值

 public Object queryScalar(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
if (rs.next()) {
return rs.getObject(1);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null, ps, rs);
}
return null;
}

 2.3.3 查询返回一行记录

也就是返回该表对应的实体类的一个实例,将查询的结果封装在该实例中,就是创建一个实例obj,只给与查询结果对应的属性赋值,其余属性就位默认值,然后将该对象返回。怎么确定到底是那个类?传入一个Class对象,利用反射技术创建实例,给属性赋值。

因为我们每次都要手动传入一个Class对象,对于同一张表,我们对这张表的原始增删改查操作都在同一个DAO类  XXXDAOImpl  extend BaseDAO<XXX>  implement  XXXDAO  

所以我们使用泛型

public abstract class oldBaseDAO<T> {

private Class<?> clazz = null;

{
//获取当前子类继承父类中的泛型
Type genericSuperclass = this.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
Type[] arguments = parameterizedType.getActualTypeArguments();
clazz = (Class<?>) arguments[0];
}

 在子类XXXDAOImpl被实例时,优先调用父类的普通代码块,这个this就是子类本身,this.getclass()就是子类XXXDAOImpl,getGenericSuperclass就是获取父类的类型,通过强制转型转化为ParameterizedType 类型,调用ParameterizedType 类的方法getActualTypeArguments()获取真实类型的数组,而子类在继承父类的时候XXXDAOImpl  extend BaseDAO<XXX>  implement  XXXDAO     如这里所示就已经指明了具体类型 XXX 该数组的第一个元素就是XXX.class

这样我们就获取到了,但是注意,如果子类继承父类时依旧写的XXXDAOImpl <T> extend BaseDAO<T>  implement  XXXDAO  ,那么获取的类型是T。

这个时候就可以开始写了

  public T querySingle(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(sql);
//填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//得到元数据,里面可以得到查询的标签名
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
//
T t = null;
if (rs.next()) {
t = (T) clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
Field field = clazz.getDeclaredField(rsmd.getColumnName(i + 1));
field.setAccessible(true);
field.set(t, rs.getObject(i + 1));
}
}
return t;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("查询单行出错");
} finally {
JDBCUtils.closeResource(null, ps, rs);
}
}

 

2.3.4查询返回多行记录

 public List<T> queryMutil(Connection conn, String sql, Object... args) {
PreparedStatement ps = null;
ResultSet rs = null;
List<T> list = new ArrayList<>();
try {
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();

while (rs.next()) {
T t = (T) clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
Field field = clazz.getDeclaredField(rsmd.getColumnName(i + 1));
field.setAccessible(true);
field.set(t, rs.getObject(i + 1));
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("查询返回多行记录出错");
} finally {
JDBCUtils.closeResource(null, ps, rs);
}
}

 说明一下,我们这里是通过反射直接获取属性,然后filed.set()方法来设置值的。

3 bdUtils工具类

3.1 简单说明其常用方法

这是个好东西啊,简单说一下吧,用得最多的就那四五个。

我们导入一个包就可以使用,然后new 一个

QueryRunner  中有很多现成的crud方法使用。

  • qr.update(conn,sql,params)        conn是连接,sql是dml操作语句,params是要传入的占位符填充参数,没有就不写  。返回一个int 类型,表示操作改变的行数
  • qr.query(conn,sql,ResultSetHandler<? extends Object>,params)   
    • 返回单个特殊值        qr.query(conn,sql,new ScalarHandler(),params)
    • 返回一行记录             qr.query(conn,sql,new  BeanHanler<>(clazz) ,params)
    • 返回多行记录              qr.query(conn,sql,new BeanListHandler<>(clazz),params)

3.2  ResultSetHandler的探索

这个工具类底层自动帮我们关闭prepareStatement和ResultSet,我们也不用在DAO层关闭连接,所以直接执行crud操作,给service层提供数据,出错就抛出异常,不需要考虑资源的关闭,这就是DAO层,又叫作数据持久层或者数据连接层。

看看底层源码

 

 query方法的返回值由ResultSetHandler<>接口的实现类的handle(rs)决定。如果我们不重写handler方法,那么就使用他底层提供的几个实现类的handler,这些实现类,比如BeanHanler<>(clazz)返回单行记录,将记录封装在一个实例中。

实例类型就由我们传入的clazz 决定,通过反射调用无参构造器,再根据handler(rs)方法中的

rs属性,也就是ResultSet  rs  ,封装了结果集的对象,我们通过rs.getMetaData()得到元数据,里面有标签名XXX,默认就是对象对应的一个属性名XXX,再赋值。

这里有不同,之前我们是通过反射得到属性,直接设置属性的值。但这里是通过setXXX方法来设置值的,也就是说,底层给属性private double salary设置值newSalary,是通过反射调用setSalary()方法设置值的。方法名setSalary去掉set,剩下的部分Salary首字母小写:salary就是要设置的属性名。有个例外,对于Boolean类型的属性,其set方法是isXXX()。

这里就完美解释了为什么javabean需要满足上面4条要求。 

3.3  自定义类实现ResultSetHandler<>接口

我们写一个表

 再写一个对应的实体类,所有属性的set和get方法全部一键自动生成

public class UserDetail {

private Integer id;
private String nickName;
private Integer age = -2;
private String sex;
private Integer bestFriend;

 

我们可以自定义我们的ResultSetHandler<>(),重写hanler方法来自定义返回值

 

Connection conn = JDBCUtilsByDruid.getConnection();
QueryRunner qr = new QueryRunner();
String sql = "select * from userDetail where id=?";

ResultSetHandler<Object> resultSetHandler = new ResultSetHandler<Object>() {
@Override
public Object handle(ResultSet resultSet) throws SQLException {
//这个resultSet就是返回的结果集,我们可以这个结果集自定义我们想要的返回值
ResultSetMetaData rmds = resultSet.getMetaData();
if (resultSet.next()) {
for (int i = 0; i < rmds.getColumnCount(); i++) {
System.out.println(rmds.getColumnLabel(i + 1) + ":" + resultSet.getObject(1 + i).toString());
}
}
return "这是我自定义的ResultSetHandler";
}
};
Object query = qr.query(conn, sql, resultSetHandler, 2);
System.out.println(query);
conn.close();

结果:

传入我们自定义的实现了ResultSetHandler接口,重写了handler方法,该方法返回string类型的固定字符串,而query底层最后返回的就是这个handler方法的返回值。

3.4 改变实体类setXxx与getXxx方法

还是这张表 userDetail不变,bestFriend在表中是 int 类型,是某一个记录的id

现在给出它的javabean类

public class UserDetail {

private Integer id;
private String nickName;
private Integer age = -2;
private String sex;

private Integer salary = -1;//无关属性,表中没有salary字段

//请注意,我把这里setAge的方法体改变了
public void setAge(Integer salary) {
this.salary = salary;
}

public Integer getAge() {
return age;
}

public Integer getId() {
return id;
}

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

public String getNickName() {
return nickName;
}

public void setNickName(String nickName) {
this.nickName = nickName;
}


public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

@Override
public String toString() {
return "UserDetail{" +
"id=" + id +
", nickName='" + nickName + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", salary=" + salary +
'}';
}
}

 请注意,类中没有bestFriend这个属性,但不会报错,找不到bestFriend对应的set方法,就自动舍弃该字段的结果,不封装了。

还有一点,我把setAge方法改变了

方法内不给age赋值,给salary赋值,我们运行一下看看结果

结果显示并没有给age赋值,age为默认值-1,而调用setAge方法,方法内给salary赋值了,salary为19,由此我们证实了dbutils底层通过标签名xxx,找到setXxx()方法,再反射传参并调用该方法完成给属性xxx赋值。 执行该方法,封装完成,至于有没有属性被赋值,赋值的属性是不是 xxx 都不关心,底层只管调用setXXX方法并传入参数,且成果无异常执行完方法就ok。

下面我们再改变一下,删除salary,加上一个属性,并设置它的get和set方法.因为我们表中bestFriend字段是int类型,但我在实体类中设置的bestFriend属性为 UserDetail,这是一个我们自己定义的类,按照前面所讲的,我们是不是只改变set方法就行了?

请注意下面我设置的get和set方法,我不但改变了set方法的参数类型为Integer,因为结果集中的bestFriend字段是Integer类型的,所以将参数类型改为Integer。

那为什么将get方法的返回值也设置为Integer类型?我经过反复debug发现,如果不改get的返回值类型与结果集对应的字段类型一样,那么底层找不到该set方法,这个底层同时要求setXxx方法的参数和getXxx的返回值与结果集字段Xxx类型一致,这样才能找到setXxx方法,并传参调用该set方法。所以也就解释了上一步,光改变setAge不改变getAge但对结果没有影响,因为结果集中age和类中属性age的类型都是Integer。

打印结果:

 现在终于正确了,bestFriend属性一个UserDetail类型,id属性为表中bestFriend字段的值,这是我们在setBestFriend方法设置的结果。

注意,表中bestFriend字段为int类型,但是我类中的bestFriend属性是我自己定义的类UserDetail,我使用bdutils应该怎么做?答案很简单,之前说过dbutils底层是通过setXXX给属性设置值,调用的是方法!!!那么我只需要改变setBestFriend方法的参数和方法体

这就是我所理解的dbutils。

举报

相关推荐

0 条评论