Hibernate——主键生成策略CRUD 基础API区别的总结 和 注解的使用
Posted dashuai的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hibernate——主键生成策略CRUD 基础API区别的总结 和 注解的使用相关的知识,希望对你有一定的参考价值。
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及的知识点总结如下:
- hibernate的主键生成策略
- UUID
- 配置的补充:hbm2ddl.auto属性用法
- 注解还是配置文件
- hibernate注解的基本用法
- 使用Session API CRUD操作对象,以及对象状态的转换
- hibernate缓存的概念
- get()/load()的区别到底是什么,源码分析
- 代理模式实现的懒加载
- saveOrUpdate()/merge()的区别
Assigned(常用,一般情况使用很方便):
由程序生成主键值,并且在save()之前指定,否则会抛出异常。
特点:主键的生成值完全由用户决定,与底层数据库无关。用户需要维护主键值,在调用session.save()之前要指定主键值。唯一的例外:int auto_increment类型主键除外,在程序中不用指定主键值,直接看代码:
sid是主键,且是int类型,设置为自动增长。
/** * StudentDao * * @author Wang Yishuai. * @date 2016/3/11 0011. * @Copyright(c) 2016 Wang Yishuai,USTC,SSE. */ public class StudentDao { private static final Logger LOG = LoggerFactory.getLogger(StudentDao.class); private Session session; @Before public void init() { Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); this.session = sessionFactory.getCurrentSession(); } @After public void clear() { } @Test public void save() { Transaction transaction = session.beginTransaction(); Student student = new Student(); try { //student.setSname("dashuai"); student.setSname("dadadad"); session.save(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } } }
配置文件设置:
<hibernate-mapping> <!-- class 里面的属性必须和对应的类名,表名保持一致,实现映射--> <class name="zhujian.vo.Student" table="students"> <!-- id代表数据库表的主键, name="userId",对应数据库表里的字段column="userId"--> <id name="sid" column="sid" type="string"> <generator class="assigned"/> </id> <!-- 数据库里其他普通字段和实体属性的映射,属性的类型需要小写--> <property name="sname" column="sname" type="string"/> </class> </hibernate-mapping>
先后保存两个学生对象,没有错误,如下:
即使我设置了主键生成策略为assigned,因为学生表的sid是自动增长且为int类型,所以程序中不用人工指定sid值,这是一个例外情况。其他情况如果设置主键为asigned策略,必须手动设主键值,否则会出问题。
把sid的自动增长设置取消,再运行之前的代码,会发生问题:如果不设置sid(主键)值,则无法成功插入数据,且会生成主键为0的数据记录(if之前没有),本地测试没有报错,但是无法成功插入。
把主键改为String(varchar)类型(对象映射文件和实体类也需要修改),再运行之前的代码,同样只能插入sid为0的记录(if之前没有),其他的无法继续插入。若改为手动设置主键值,则恢复正常。
Increment(MySQL不适用,集群不适用,多进程不适用,与底层数据库有关)
Increment方式对主键值采取自动增长的方式生成新的主键值,但要求底层数据库支持Sequence序列。如Oracle,DB2等。需要在映射文件xxx.hbm.xml中加入Increment标志符的设置。
特点:由Hibernate本身维护,适用于所有的数据库,不适合多进程并发更新数据库,适合单一进程访问数据库。不能用于群集环境。
比如现在有4个数据库的集群,共享一张学生表,我从中查找某个编号(主键)为1000的学生,如果使用的hibernate的主键生成策略为increment,则无法保证编号1000的学生在集群里是唯一的。
Identity根据底层数据库支持自动增长,无需手动设置主键,但不同的数据库用不同的主键增长方式,移植性不好。
特点:与底层数据库有关,适用于mysql、DB2、MS SQL Server,采用数据库生成的主键,用于为long、short、int类型生成唯一标识。使用SQL Server 和 MySQL 的自增字段,这个方法不能放到 Oracle 中,Oracle 不支持自增字段,要设定sequence(MySQL 和 SQL Server 中很常用),Identity无需Hibernate和用户的干涉,使用较为方便,但不便于在不同的数据库之间移植程序。
Sequence (常用)
Sequence需要底层数据库支持Sequence方式,例如Oracle数据库等。
特点:需要底层数据库的支持序列,支持序列的数据库有DB2、PostgreSql、Qracle、SAPDb等在不同数据库之间移植程序,特别从支持序列的数据库移植到不支持序列的数据库需要修改配置文件。比如:Oracle:create sequence seq_name increment by 1 start with 1; 需要主键值时可以调用seq_name.nextval或者seq_name.curval得到,数据库会帮助我们维护这个sequence序列,保证每次取到的值唯一,如:insert into tbl_name(id, name) values(seq_name.nextval, ‘Jimliu’);
配置:
<id name="id" column="id" type="long">
<generator class="sequence">
<param name="sequence">seq_name</param>
</generator>
</id>
Native(常用,推荐使用,移植性好,也适合多数据库)
Native主键生成方式会根据不同的底层数据库自动选择Identity、Sequence、Hilo主键生成方式。
特点:根据不同的底层数据库采用不同的主键生成方式。由于Hibernate会根据底层数据库采用不同的映射方式,因此便于程序移植,项目中如果用到多个数据库时,可以使用这种方式。
配置:
<id name="id" column="id">
<generator class="native" /></id>
UUID(常用在网络环境,但是耗费存储空间)
UUID使用128位UUID算法生成主键,能够保证网络环境下的主键唯一性,也就能够保证在不同数据库及不同服务器下主键的唯一性。
特点;能够保证数据库中的主键唯一性,生成的主键占用比较多的存贮空间。
配置:
<id name="id" column="id">
<generator class="uuid.hex" /></id>
Hilo(不常用)
使用高低位算法生成主键,高低位算法使用一个高位值和一个低位值,然后把算法得到的两个值拼接起来作为数据库中的唯一主键。Hilo方式需要额外的数据库表和字段提供高位值来源。默认请况下使用的表是hibernate_unique_key,默认字段叫作next_hi。next_hi必须有一条记录否则会出现错误。
特点:和底层数据库无关,但是需要额外的数据库表的支持,能保证同一个数据库中主键的唯一性,但不能保证多个数据库之间主键的唯一性。Hilo主键生成方式由Hibernate 维护,所以Hilo方式与底层数据库无关,但不应该手动修改hilo算法使用的表的值,否则会引起主键重复的异常。
配置的补充:hbm2ddl.auto属性用法
在项目的配置文件里设置:<property name="hbm2ddl.auto">update</property>,属性含义的解释:
- create:表示启动的时候先drop数据库,再create。
- create-drop: 也表示创建,只不过再系统关闭前执行一下drop。
- update: 这个操作启动的时候会去检查schema是否一致,如果不一致会做scheme更新。
- validate: 启动时验证现有schema与你配置的hibernate是否一致,如果不一致就抛出异常,并不做更新。
在Hibernate是用映射文件好还是用注解的方式好?
两种方式本质上没区别。使用元数据可以在写实体的同时配好映射,而 XML 则需要来回切换配置和实体,不太方便。从维护上来说,注解更好。因为代码和“配置”在一起,不容易漏掉信息。从这一点上来说,注解必须是第一选择。但是也有人说到这样一个问题,如果使用注解,有很大可能会违反开闭原则,使用配置文件则不会。闲言少叙,看代码:
<?xml version=‘1.0‘ encoding=‘UTF-8‘?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory name="MySQL"> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="connection.url">jdbc:mysql://localhost:3306/bbs</property> <property name="connection.username">root</property> <property name="connection.password">123</property> <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property> <property name="show_sql">true</property> <property name="format_sql">true</property> <property name="hbm2ddl.auto">update</property> <property name="current_session_context_class">thread</property> <mapping class="net.nw.vo.Students"/> </session-factory> </hibernate-configuration>
写好配置文件,代码如下:
package net.nw.vo; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; //学生实体类 @Entity public class Students { private int sid; private String sname; @Id @GeneratedValue(strategy=GenerationType.AUTO) public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
测试:
import net.nw.vo.Students; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.AnnotationConfiguration; public class HibernateTest { public static void main(String[] args) { AnnotationConfiguration annotationConfiguration = new AnnotationConfiguration().configure(); SessionFactory sessionFactory = annotationConfiguration.buildSessionFactory(); Session session = sessionFactory.getCurrentSession(); Transaction transaction = session.beginTransaction(); try { Students s = new Students(); //s.setSid(1); s.setSname("xxxx"); session.save(s); transaction.commit(); } catch (Exception ex) { transaction.rollback(); ex.printStackTrace(); } } }
执行成功!本地测试成功插入xxxxx(接之前的bbs数据库表students):
小结:
- 使用AnnotationConfiguration创建config对象代替之前的Configuration方式(hibernate3的方式)。
- hibernate4中新增了ServiceRegistry接口。4.0以后改为使用ServiceRegistry 注册:
Configuration cfg = new Configuration().configure(); ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(cfg.getProperties()).buildServiceRegistry(); SessionFactory factory = cfg.buildSessionFactory(serviceRegistry); Session s = factory.openSession();
- @Entity——必选的注解,在class类体上面,注解将一个类声明为一个实体bean。属性 name 可选,对应数据库中的一个表。若表名与实体类名相同,则可以省略。
-
@Table(name="",catalog="",schema="") ——该注解可选,通常和@Entity 配合使用,只能标注在实体的 class 定义处,表示实体对应的数据库表的信息。属性:
-
name - 可选,表示表的名称,默认地,表名和实体名称一致,只有在不一致的情况下才需要指定表名
-
catalog - 可选,表示Catalog名称,默认为 Catalog("").
-
schema - 可选 , 表示 Schema 名称 , 默认为Schema("").
-
- @Id——必选,一般加在getter方法上面, 定义了映射到数据库表的主键的属性,一个实体只能有一个属性被映射为主键,置于get方法前。
- @GeneratedValue(strategy=GenerationType,generator="") ——可选,用于定义主键生成策略。属性:
- Strategy - 表示主键生成策略,取值有:
- GenerationType.AUTO - 根据底层数据库自动选择(默认),若数据库支持自动增长类型,则为自动增长。
-
GenerationType.INDENTITY - 根据数据库的Identity字段生成,支持DB2、MySQL、MS、SQL Server、SyBase与HyperanoicSQL数据库的Identity类型主键。
-
GenerationType.SEQUENCE - 使用Sequence来决定主键的取值,适合Oracle、DB2等 ,支持Sequence的数据库,一般结合@SequenceGenerator使用。(Oracle没有自动增长类型,只能用Sequence)
-
GenerationType.TABLE - 使用指定表来决定主键取值,结合@TableGenerator使用。如:
@Id @TableGenerator(name="tab_cat_gen",allocationSize=1) @GeneratedValue(Strategy=GenerationType.Table)
-
Generator - 表示主键生成器的名称,这个属性通常和ORM框架相关 , 例如:Hibernate 可以指定 uuid 等主键生成方式
- Strategy - 表示主键生成策略,取值有:
-
@Version——可以在实体bean中使用@Version注解,通过这种方式可添加对乐观锁定的支持。
-
@Basic ——用于声明属性的存取策略:
-
@Basic(fetch=FetchType.EAGER) 即时获取(默认的存取策略)
-
@Basic(fetch=FetchType.LAZY) 延迟获取
-
-
@Temporal ——用于定义映射到数据库的时间精度:@Temporal(TemporalType=TIME) 时间、@Temporal(TemporalType=DATE) 日期、@Temporal(TemporalType=TIMESTAMP) 两者兼具
注解太多了,以后一一说明。
- 使用注解,在Hibernate.cfg.xml中同样也需要注册实体类。当然配置文件不用配,只是还是需要引入实体类的关系,<mapping class="net.nw.vo.Students"/>,配置文件方式的resource=改为了class=。
使用session api实现crud
结合之前的总结,看看crud时对象状态的转换关系。直接看代码:
- create,session.save(obj);,注意这里面的玄机:
- 如果当前对象是持久态,则执行save方法,会自动触发Update语句的执行,而不是insert语句。
- 如果当前对象为瞬时态,则执行save方法,会立刻执行insert语句,使得对象从瞬时态转换为持久态。
@Test public void save() { Transaction transaction = session.beginTransaction(); Student student = new Student(); try { student.setSname("xiaoli"); // 之前的student是瞬时态的 session.save(student); // 之后变为持久态 transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
debug一下,验证之前的理论:在sava处断点,step over之后,立即控制台打印了insert into students(sname) values(?)。后台数据库也插入了xiaoli。
下面看如果当前对象是持久态的情况:
@Test public void save() { Transaction transaction = session.beginTransaction(); // Student student = new Student(); try { // student.setSname("xiaoli"); // 从数据库读取xiaoli,该对象是持久态的 Student student = (Student) session.get(Student.class, 7); // 重新赋值,再保存 student.setSname("小李"); session.save(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
还是在save处debug,发现首先发送SQL语句:
select
student0_.sid as sid2_0_,
student0_.sname as sname2_0_
from
students student0_
where
student0_.sid=?
执行save之后,发送的SQL是:
update students set sname=? where sid=?
道理很明显,如果还是插入语句,则会插入新的记录,显然不对了。
- retrive,hibernate基本的查询api是get和load(只根据主键查询,不能根据其它字段查询,如果想根据非主键查询,可以使用HQL),之前也看了get的用法了,使用get查询对象,执行之后,对象变为持久态的,立刻发送SQL语句select……而且也可以直接填写实体类名,get查找返回的是真正的实体对象,如果找不到则返回null,代码如下:
Student student = (Student) session.get("zhujian.vo.Student", 1);
LOG.info("sid = {}, sname = {}", student.getSid(), student.getSname());
debug一下:
执行48句之后:
再看load方法,参数写法和get一样的,但是load是一种懒加载的方式,只有使用的时候才生成sql语句:
@Test public void retrive() { Transaction transaction = session.beginTransaction(); try { Student student = (Student) session.load(Student.class, 2); student.setSname("小李"); session.save(student); transaction.commit(); } catch (Exception e) { LOG.error("save error", e); transaction.rollback(); } }
load方法查询的对象是代理对象,且执行此方法时不会立即发出查询语句。因为使用load加载对象去查询某一条数据的时候并不会直接将这条数据以指定对象的形式来返回,而是在真正需要使用该对象里面的一些属性的时候才会去查找对象。他的好处就是可以减少程序本身因为与数据库频繁的交互造成的处理速度缓慢的问题。
而hibernate的load方法延迟加载实现原理是使用代理(代理设计模式在hibernate的的应用),Hibernate的懒加载,是通过在内存中对类、集合等的增强(即在内存中扩展类的特性[继承])来实现的,这些类通常称为代理类。比如我们通过session.load(class, id)操作,加载一个对象的时候,hibernate返回的实际上是实体类的代理类实例。如图:
执行65句之后:
但如通过session.get操作,则返回实际的对象实例(不是代理类实例),对上例而言,get操作返回实体类的实例。采用load()方法加载数据,如果数据库中没有相应的记录,则会抛出异常:org.hibernate.ObjectNotFoundException: No row with the given identifier exists:……所以如果我知道该id在数据库中一定有对应记录存在,那么我就可以使用load方法来实现延迟加载。
session缓存(hibernate一级缓存)概念
在继续探讨这个问题之前,必须先提前总结下缓存的一些东西。
Hibernate缓存包括两大类:Hibernate一级缓存和Hibernate二级缓存。
一级缓存就是Session级别的缓存,一个Session做了一个查询操作,它会把这个操作的结果放在一级缓存中,如果短时间内这个session(一定要同一个session)又做了同一个操作,那么hibernate直接从一级缓存中拿,而不会再去连数据库取数据。它是属于事务范围的缓存,Session对象的生命周期通常对应一个数据库事务或者一个应用事务,一级缓存中,持久化类的每个实例都具有唯一的OID。这一级别的缓存由hibernate管理,无需主动干预即可工作。
二级缓存就是 SessionFactory 级别的缓存,查询的时候会把查询结果缓存到二级缓存中,如果同一个sessionFactory创建的某个session执行了相同的操作,hibernate就会从二级缓存中拿结果,而不会再去连接数据库,因为SessionFactory对象的生命周期和应用程序的整个过程对应,因此Hibernate二级缓存是进程范围或者集群范围的缓存,有可能出现并发问题,因此需要采用适当的并发访问策略,该策略为被缓存的数据提供了事务隔离级别,需要人工配置和更改,可以动态加载,卸载(二级缓存是一个可配置的插件,默认下SessionFactory不会启用这个插件。)。而一级缓存不可以卸载。
当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查,查不到,如果配置了二级缓存,那么从二级缓存中查,如果都查不到,再查询数据库,把结果按照ID放入到缓存删除、更新、增加数据的时候,同时更新缓存。
Student student = (Student) session.get(Student.class, 6); LOG.info("student sid = {}, student sname = {}", student.getSid(), student.getSname()); Student student1 = (Student) session.get(Student.class, 6); LOG.info("student1 sid = {}, student1 sname = {}", student1.getSid(), student1.getSname());
debug查看效果:执行完第一个get语句,立即发送了SQL语句,执行第二个一样的get语句时,我发现并没打印SQL语句,而是直接打印了log,只能说明,它第二次一样的对象查询是在缓存中查找到的。
网上有人说get不从缓存中查找,这是不对的,还有人说get不会去二级缓存中查找,其实也不对。或者严格的说必须指定哪个版本的hibernate再来讨论这个问题,下面还会分析源码来再次佐证(hibernate 3)。
再看load的方式:
Student student = (Student) session.load(Student.class, 6); LOG.info("sid = {}, sname = {}", student.getSid(), student.getSname()); Student student1 = (Student) session.load(Student.class, 6); LOG.info("student1 sid = {}, student1 sname = {}", student.getSid(), student.getSname());
也是第二次查找同一个对象的时候,没有再生成SQL语句,直接打印的两个log,只能说明它是在缓存中查询了。load方法也是走缓存的。那么具体怎么走,下面看源码级别的分析。
load和get的源码分析:
先看get和load的api源码,位于SessionImpl类,它是Session接口的一个实现类:
上面是load方法的,下面是get方法:
两种方式类似,那么继续看重载的load和get方法源码:
public Object get(String entityName, Serializable id) throws HibernateException { LoadEvent event = new LoadEvent(id, entityName, false, this); boolean success = false; Object var5; try { this.fireLoad(event, LoadEventListener.GET); success = true; var5 = event.getResult(); } finally { this.afterOperation(success); } return var5; } public Object load(String entityName, Serializable id) throws HibernateException { LoadEvent event = new LoadEvent(id, entityName, false, this); boolean success = false; Object var5; try { this.fireLoad(event, LoadEventListener.LOAD); if(event.getResult() == null) { this.getFactory().getEntityNotFoundDelegate().handleEntityNotFound(entityName, id); } success = true; var5 = event.getResult(); } finally { this.afterOperation(success); } return var5; }
关键区别是this.fireLoad(event, LoadEventListener.LOAD);和this.fireLoad(event, LoadEventListener.GET);这两句代码,官方文档说,使用fireLoad方法把加载事件event和事件加载的监听器(一个接口)的方式组合,也就是这两个方法触发事件的方式不一样。下面先看看这两个代表不同触发方式的常量:
LoadEventListener.LoadType GET = (new LoadEventListener.LoadType("GET")).setAllowNulls(true).setAllowProxyCreation(false).setCheckDeleted(true).setNakedEntityReturned(false); LoadEventListener.LoadType LOAD = (new LoadEventListener.LoadType("LOAD")).setAllowNulls(false).setAllowProxyCreation(true).setCheckDeleted(true).setNakedEntityReturned(false);
区别就是:在allowNulls上get允许空,也就是
以上是关于Hibernate——主键生成策略CRUD 基础API区别的总结 和 注解的使用的主要内容,如果未能解决你的问题,请参考以下文章