使用hibernate与mysql时数据不能插入的原因及解决办法
1.背景
之前从没用过hibernate,因此在网上搜了一下hibernate快速入门方面的信息,最后我按照《Myeclipse Hibernate 快速入门 中文版》(CSDN,百度文库都有)一文开始了我的hibernate之旅(为项目做技术准备)。
下面是在学习使用时用到的三个开发工具(Myeclipse , Mysql , SQL-Front)及其版本说明:
Myeclipse版本为6.5(没用最新的Myeclipse8.5,这是由于之前组里的项目都是在Myeclipse6.5中开发的,并且本项目是与其它人合作完成的,为了防止可能由于开发环境的不一致而引起的问题,我们统一使用Myeclipse6.5,所以我就在Myeclipse6.5环境下学习使用hibernate了)。
Mysql使用的是5.1版本。Mysql安装完后,需要手动进行配置,其中有一项是“please select the database usage”,我在这里选择的是“Muitifunctional Database”(如下图所示)。这里其实选则的是使用何种类型的数据库(InnoDB还是MyISAM),如果选第三个,就不能用InnoDB类型的数据库了,这个在每一个选项的说明中可以看到。
为了方便mysql的使用,又安装了图形化界面的SQL-Front,版本是5.1。
2.遇到的问题
我用SQL-Front在数据库中建了一简单的表用于学习,等同的SQL语句如下:
“author”表有两个字段,一个是主键“Id”,一个是“name”。需要说明的是SQL-Front在建表的时候默认数据库类型为InnoDB。
针对“author”表,按照《Myeclipse Hibernate 快速入门 中文版》一文的说明操作完成后,写了一段简单的测试代码:
即插如一条name字段为“author”的数据,但是发现执行完后数据根本就没有插入到数据库中。后来在网上搜了一下,有人给出了解决办法,即使用事务来解决,修改后的代码如下:
数据插入的问题是解决了,但是发现插入数据后,Id已经在插入前自动增加了(不是从1开始了),也就是说之前的测试虽然数据看似没有插入数据库,但是实际效果却跟插入了数据库一样(要不然Id不会自动增加),这又是为什么?在网上搜了一下,结合自己的理解,我分析了一下原因。
3.原因
上述问题的原因的本质我认为在于使用的数据库类型。
我使用的数据库类型是“InnoDB”,这是一个支持事务的数据库类型,这种数据库你无论什么操作,最后如果你不“commit”的话,等于啥也没干(这其中的道理在网上搜一下对数据库中事务的简单介绍应该不难理解)。虽然通过save方法可以将sql语句发送到数据库让其执行(备注:并不是所有的save方法都会将sql语句发送到数据库,当主键生成策略为native的时候会发送到数据库,比如“author”的主键Id,其生成策略就为native,不过可以通过dao.getSession().flush()强行将sql语句发送到数据库。这些我也只是知道一点皮毛,有时间还需要深入了解),数据库也确实执行了(分配了Id,并且将Id自动加1),但是这个执行的结果只是临时的,如果不“commit”的话,随着会话session的结束(即上述代码中dao.getSession().close()语句),这个临时的执行结果也就没了,直接的体现就是数据没有最终插入数据库。
通过SQL-Front可以很好的观察到这一过程(针对未引入事务的那段代码),首先在dao.getSession().Close()处设置断点,然后用SQL-Front打开这个表,并设置SQL-Front与mysql的会话隔离级别为(Read Uncommitted,默认的级别为Repeatable Read,这个网上也有很多介绍的,也不多说了),然后执行测试代码,执行到断点处后,也即刚刚执行完dao.save(author)而还没有关闭本次会话,通过SQL-Front会发现,这个数据插入到了表中(当然是“临时”插入数据,如果会话隔离级别不是Read Uncommitted的话是看不到这条插入的数据的),继续执行dao.getSession().close()后,通过SQL-Front刷新“author”表中的数据,会发现刚刚插入的那条数据又消失了。
不过这其中有一个比较特殊的地方,那就是Id这个自段了。这个字段是“author”表的主键,并且它的值是数据库自己产生的,在插入数据的时候不需要指定这个字段的值。这个字段的特殊之处在于你在插入数据的时候,无论最后是否“commit”,这个字段的值都会自动加1供下一次插入数据时使用,不会说由于本次会话没有“commit”,而在会话结束时自动减1恢复到原来的值。
这一点我觉得应该是出于并发的考虑(没有查阅相关资料,只是提出我自己的猜测)。假设有三个会话A和B,都要向“author”表中插入数据(假设表中没有数据,Id从1开始),A首先调用了save方法,数据库为A插入的数据分配的Id是1,然后“author”表将Id自动加1并保存,在A未“commit”之前,B也调用了save方法,自然数据库为B插入的数据分配的Id是2(不会是1,如果是1,A和B将要提交的数据主键Id值就冲突了,会造成A和B谁后“commit”数据谁失败,数据库就无法并发了),“author”表将Id字段再次加1后保存(此时为下一组数据使用的Id值为3),假设B首先“commit”了数据,即“author”表中有了Id字段值为2的数据,而A最终没有“commit”数据,如果此时“author”表的字段自动减1,可以看到,下一次数据插入分配的Id字段值就会是2,和现有的数据发成了主键冲突。因此,对于“author”表中Id这个字段,无论某个插入操作是否最终“commit”,只要调用了save方法,Id字段就会自加1。这也能解释之前遇到的那个问题,即数据没有插入,Id字段却自动增加了。
4.解决方法
总共有两种解决方法:
第一种: 可以考虑使用“MyISAM”类型的数据库,这种类型的数据库不支持事务,因此在调用save方法的时候(注意,如果主键生成策略不是native的,必须在save后调用dao.getSession().flush()方法,即强行将sql语句发送到数据库,否则一样没有插入数据),数据就已经最终插入到数据库里了(注意,这是最终结果,不是临时结果,这和使用“InnoDB”类型数据库时“commit”的效果是一样的)。当然了,直接使用事务机制(就像上面那段修改后的代码一样)也是可以的(“MyISAM”类型的数据库虽然不支持事务,但是并不代表不能用hibernate里的事务机制,这两个概念还不太一样。当主键生成策略不是native的时候,使用事务机制还省的调用dao.getSession().flush()方法了)。mysql支持“MyISAM”类型的数据库,可以在SQL-Front中直接将“InnoDB”类型的数据库转为“MyISAM”类型的数据库(建议这么做的时候小心,因为看到网上有人说这么做可能会产生问题,不过我转换的时候倒是没碰到什么问题,也可能是我的“author”表简单的缘故),也可在建表的时候直接指定类型为“MyISAM”。关于“MyISAM”类型的数据库的更多信息(比如相对于“InnoDB”类型数据库有什么优缺点)网上也有很多介绍,就不在这里罗嗦了。
第二种:还是使用“InnoDB”类型的数据库。这时可以通过两种途径解决:一是像上面那个修改后的代码一样,加入事务机制,这是最保险的(也推荐使用);第二个途径就是在hibernate的配置文件中,加入自动提交的属性,如下图所示:
这个属性的作用是,一旦调用了save方法(和第一种解决方法一样,如果主键生成策略不是native的,必须在save后调用dao.getSession().flush()方法),hibernate会自动帮你“commit”,在代码里不需要自己写关于事务的那些代码(例如commit调用)。这么做的缺点网上也有很多说明,也不在这里多说了。
5.总结
由于我水平实在有限(大菜鸟一个啦),表达能力也不是很好,所以说了这么多也可能还没说明白,没说透彻。不过我觉的对大家有用的一个结论就是,无论什么操作,都放在事务里提交,这样是最省事,也是最保险的。
整整浪费了我一天时间啊 我操