Hibernate

1.  O/R Mapping
a. Object Relational Mapping对象关系映射 
b. 常见O/R Mapping框架: hibernate toplink jdo ibatis 
c. JPA java persistence API 
 
2. 相关JAR包
slf4j-api: simple Logging Facade for Java API 日志的Facde API
slf4j-nop: slf4j no-operation   slf4j自己的实现, 也可以使用l4j的实现,但需额外加入slf4j-log4j 的转换JAR包
antlr: Auto Tool for Language Recognition 可以构建语言识别器,解析HQL语言
commons-collections: Apache的扩展集合类
javassist: 操作字节码,与cglib相关
jta: 定义JTA规范的JAR包,当Hibernate使用JTA时需要
ejb3-persistence: 实体类中的注解都是在这个JAR包中定义的.
 
3. 配置文件
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
  <session-factory>
    <!-- Database connection settings -->
		<!--ORACLE为 oracle.jdbc.driver.OracleDriver-->
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
		<!--ORACLE为 jdbc:oracle:thin:@localhost:1521:username-->
    <property name="connection.url">jdbc:mysql://localhost/hibernate</property>
    <property name="connection.username">root</property>
    <property name="connection.password">root</property>

    <!-- JDBC connection pool (use the built-in) -->
    <property name="connection.pool_size">1</property>

    <!-- SQL "方言" -->
    <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>

    <!-- Enable Hibernate's automatic session context management -->
    <property name="current_session_context_class">thread</property>

    <!-- Disable the second-level cache  -->
    <property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>

    <!-- 是否在控制台显示SQL语句 -->
    <property name="show_sql">true</property>

    <!-- Drop and re-create the database schema on startup -->
    <property name="hbm2ddl.auto">create</property>
    <mapping resource="org/hibernate/tutorial/domain/Event.hbm.xml"/>
  </session-factory>
</hibernate-configuration>
 
4. 实体映射的Anotation:
package com.fortest.model;

import java.util.Calendar;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;

@Entity  //实体
@Table(name="std_table")  //重命名表
public class Student {
	private int id;
	private String name;
	private int age;
	private boolean isMale;
	private Calendar birthday;
	private Group group;
	private boolean hasGirlFriend;

	@Id  //主键
	@GeneratedValue  //自增长
	public int getId() {
		return id;
	}
	
	@Column(name="stu_name")  //自定义字段名
	public String getName() {
		return name;
	}
	
	@Basic   //默认
	public int getAge() {
		return age;
	}
	public void setId(int id) {
		this.id = id;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	public void setAge(int age) {
		this.age = age;
	}

	public boolean isMale() {
		return isMale;
	}

	public void setMale(boolean isMale) {
		this.isMale = isMale;
	}
	
	@Temporal(TemporalType.DATE)  //只记日期
	public Calendar getBirthday() {
		return birthday;
	}

	public void setBirthday(Calendar birthday) {
		this.birthday = birthday;
	}
	
	@Column(name="std_group")    //group是sql保留字,必须改名
	@Enumerated(value=EnumType.STRING)  //枚举类型,value=可以省略
	public Group getGroup() {
		return group;
	}

	public void setGroup(Group group) {
		this.group = group;
	}

	@Transient     //忽略
	public boolean isHasGirlFriend() {
		return hasGirlFriend;
	}

	public void setHasGirlFriend(boolean hasGirlFriend) {
		this.hasGirlFriend = hasGirlFriend;
	}
	
}
package com.fortest.model;

public enum Group {
	GroupA,GroupB,GroupC
}
 
 
5. 联合主键
a.将主键封装成类,并实现Serializable, 重写equals  hashCode方法
b. 将主键类注解为@Embeddable,并将实体类对应的get方法注解为@Id(不常用). 或:
将直接将实体类对应的get方法注解为@EmbeddedId. 或
将实体类对应的get方法注解为@IdClass(value=TablePk),或略写为@IdClass(TablePK),并将实体中所有属于主键的属性都注解为@Id
 
6. sessionFactory
package com.fortest.dao;

import java.util.Calendar;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.fortest.model.Group;
import com.fortest.model.Student;


public class StudentDaoTest {
	private static SessionFactory sf = null;
	
	@BeforeClass
	public static void inti(){
		Configuration cfg = new Configuration();
		cfg.configure();
		//cfg.configure("hibConfig.xml");  不带参数的方法自动加载"hibernate.cfg.xml",重载的configure(String filename)可以加载指定配置文件
		ServiceRegistry  sr = new ServiceRegistryBuilder().applySettings(cfg.getProperties()).buildServiceRegistry();
		sf = cfg.buildSessionFactory(sr);
		
	}
	
	@Test
	public void testSave(){
		Student student = new Student();
		student.setAge(22);
		student.setName("wang");
		student.setMale(true);
		student.setBirthday(Calendar.getInstance());
		student.setGroup(Group.GroupB);
		
		
		//获得session的两种方式:
		//1. 从上下文中找到session,如果没有,创建一个新的
		/*
		 * getCurrentSession()获取策略通过配置配置文件的hibernate.current_session_context_class属性指定:
		 * 取值有 jta/thread/managed/custom
		 * */
		Session session = sf.getCurrentSession();
		
		
		
		//2. 每次都创建一个新的session
		//Session session = sf.openSession();
		
		
		
		//事务开始
		session.beginTransaction();
		
		session.save(student);
		Student std = (Student)session.get(Student.class, new Integer(1));
		
		//事务结束
		session.getTransaction().commit();
		
		assertThat(std.getName(),equalTo("wang"));
		System.out.println(std.getName());
		
		//使用sessioinFactory.getCurrentSession()时,当执行commit后,会自动close session,故不能写session.close(). 使用openSession时则须写
		//session.close();
	}
	
	@AfterClass
	public static void close(){
		sf.close();
	}
}
 
7.JTA
a. 概念:java transation API ,java事务API. JTA允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据
b. 两种事务: 依赖于数据库本身(单数据库) 和 JTA事务(分布式 多数据库)
 
8. 数据持久化的三种状态:
转换关系:
Hibernate
 
 RAMcachedatabase
Transient
Persistent● id● id● id
Detached● id● id
 
9. CRUD
a. get与load的区别:
i.不存在对应记录时表现不一样.
ii.load返回的是代理对象,等到真正用到对象的内容时才发出SQL语句.
iii.get直接从数据库加载,不会延时.
iv.都会首先查找缓存,如果没有,才会查找数据库.所以多次get或load同一对象时,只会发出一条sql语句.使用clean()方法可以使其重发SQL.
b. update只要保证对象的id不为空,且在数据库中有对应的ID时,就能生效.
c. 如何实现update只更新有改动的字段,使用hql
d.clean 清除session缓存
e.flush 正常情况下,只有在session.getTransation.commit()时才会与数据库同步, flush方法强制每次当对象set时,都会去同步.
 
10. 一对一关联
a.在get方法前加@OneToOne注解.在数据库表中默认生成的外键字段名为XX_id,若需改变,则加入注解@JoinColumn(name="xxId")
b.若为双向关联,则需在一方(getHusband)将OneToOne注解改为@(mappedBy="wife"),此意义在于对于husband和wife类来说,可以相互引用,而在数据库中的表现与一对一单向外键关联没有区别
 
11. 多对一关联
a. 在多的类的少的类的引用的GET方法前加@ManyToOne:
@Entity
@Table(name="User_Table")
public class User {
	private int id;
	private String name;
	private Group group;
	
	@Id
	@GeneratedValue
	public int getId() {
		return id;
	}
	public String getName() {
		return name;
	}
	@ManyToOne
	public Group getGroup() {
		return group;
	}
	public void setId(int id) {
		this.id = id;
	}
	public void setName(String name) {
		this.name = name;
	}
	public void setGroup(Group group) {
		this.group = group;
	}
}
b. 双向关联时,注意写mappiedBy
 
12.一对多关联
a.在一的一方是以Set类型存储多达引用(Set是不重复的容器)
private Set<User> users = new HashSet<User>();
List (与Set差不多 多个@OrderBy)
Map @Mapkey
b.引用的GET方法中加注解@OneToMany
c.若不在一的一方加@JoinColumn(name="groupId") hibernate会生成中间表,用于存两者关系,将其当做多对多的一种特殊情况
d.双向关联时,注意写mappiedBy
 
13. 多对多关联
a. 加中间表
b. 要改变默认生成中间表的名字及字段名
@ManyToMany
@JoinTable(name="t_s",
	joinColumns={@JoinColumn(name="teacher_id")},          //本model的ID
	inverseJoinColumns={@JoinColumn(name="student_id")}   //对于model的ID
	)
c. 多对多双向关联注意写mappiedBy
 
14. 关联关系的CRUD
a.增. 要使hibernate自动存储关联关系的表,就必须在根类加Cascade
b.CascadeType的取值:
ALL Cascade all operations所有情况
MERGE Cascade merge operation合并(merge=save+update)
PERSIST Cascade persist operation存储 persist()
REFRESH Cascade refresh operation刷新
REMOVE Cascade remove operation删除
c. 
@OneToMany(mappedBy="group",
cascade={CascadeType.ALL}, //控制增删改(即CUD) fetch=FetchType.EAGER //控制查询(即R) EAGER值代表取出关联 LAZY值为不取关联
//多的一方fetch取值默认为LAZY 一的一方默认为EAGER
)
d. Cascade只会影响增删改,不会影响读取,读取由fetch指定
e. 双向不要两边设置Eager(会有多余的査询语句发出),对多方设置fetch的时候要谨慎,结合具体应用,一般用Lazy不用eager,特殊情况(多方数量不多的时候可以考虑,提高效率的时候可以考虑)
 
15. 继承关系映射的三种方式
a. 单表.JPA的写法:
@Entity
@DiscriminatorValue("student")
public class Student extends Person{}//不要再子类中写ID属性
 
@Entity
@DiscriminatorValue("teacher")
public class Teacher extends Person{}//不要再子类中写ID属性
 
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="discriminator",discriminatorType=DiscriminatorType.STRING)  //指定区分字段类型
@DiscriminatorValue("person")
public class Person{}
 
b.每个类一张表: 用table的ID生成策略
c.联合,JPA写法(子类不要写ID属性,子类表的id字段不要写自动生成)
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public class Person{}
 
16. 树状结构的关系映射
a.实体:
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;

@Entity
public class Tree {
	private int id;
	private String name;
	private Tree parent;
	private Set <Tree> children = new HashSet <Tree> ();
	
	@Id
	@GeneratedValue
	public int getId() {
		return id;
	}
	public String getName() {
		return name;
	}
	
	@ManyToOne
	@JoinColumn(name="parent_id")
	public Tree getParent() {
		return parent;
	}
	
	@OneToMany(cascade=CascadeType.ALL, mappedBy="parent",fetch=FetchType.EAGER)
	public Set<Tree> getChildren() {
		return children;
	}
	public void setId(int id) {
		this.id = id;
	}
	public void setName(String name) {
		this.name = name;
	}
	public void setParent(Tree parent) {
		this.parent = parent;
	}
	public void setChildren(Set<Tree> children) {
		this.children = children;
	}
	public Tree(String name) {
		super();
		this.name = name;
	}
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return this.name;
	}
	public Tree() {   //要成功使用HIBERNATE读取到数据,就必须保证有不带参数的构造方法
		super();
	}
}
b. 生成的SQL语句:
create table Tree (
        id integer not null auto_increment,
        name varchar(255),
        parent_id integer,
        primary key (id)
    )
c.保存:
public void TreeSaveTest(){
	Tree t = new Tree("t");
	Tree t1 = new Tree("t1");
	Tree t2 = new Tree("t2");
	Tree t11 = new Tree("t11");
	Tree t12 = new Tree("t12");
	Tree t21 = new Tree("t21");
		
	t.getChildren().add(t1);
	t.getChildren().add(t2);
	t1.getChildren().add(t11);
	t1.getChildren().add(t12);
	t2.getChildren().add(t21);
		
	t1.setParent(t);
	t2.setParent(t);
	t11.setParent(t1);
	t12.setParent(t1);
	t21.setParent(t2);

	Session s = sf.getCurrentSession();
	s.beginTransaction();
	s.save(t);
	s.getTransaction().commit();
}
d.读取,并用递归打印: (必须保证Tree类有不带参数的构造方法)
public void TreeLoadTest(){
	TreeSaveTest();
	Session s = sf.openSession();
	s.beginTransaction();
	Tree tt = (Tree)s.load(Tree.class, 1);
	s.getTransaction().commit();
	printTree(tt);
	s.close();
}
	
public void printTree(Tree t){
	System.out.println(t);
	for(Tree tt : t.getChildren()){
		printTree(tt);
	}
}
 
17. HQL实例
a.结构:
Hibernate
 
b.类代码:
import javax.persistence.*;
@Entity
public class Category {
	private int id;
	private String name;
	@Id
	@GeneratedValue
	public int getId() {
		return id;
	}
	public String getName() {
		return name;
	}
	public void setId(int id) {
		this.id = id;
	}
	public void setName(String name) {
		this.name = name;
	}
}
 
import java.util.Date;
import javax.persistence.*;
@Entity
public class Topic {
	private int id;
	private String title;
	private Category category;
	private Date creatDate;
	@Id
	@GeneratedValue
	public int getId() {
		return id;
	}
	public String getTitle() {
		return title;
	}
	@ManyToOne(fetch=FetchType.LAZY)
	@JoinColumn(name="category_id")
	public Category getCategory() {
		return category;
	}
	public Date getCreatDate() {
		return creatDate;
	}
	public void setId(int id) {
		this.id = id;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public void setCategory(Category category) {
		this.category = category;
	}
	public void setCreatDate(Date creatDate) {
		this.creatDate = creatDate;
	}
}
 
import javax.persistence.*;

@Entity
public class Msg {
	private int id;
	private String content;
	private Topic topic;
	
	@Id
	@GeneratedValue
	public int getId() {
		return id;
	}
	public String getContent() {
		return content;
	}
	
	@ManyToOne(fetch=FetchType.LAZY)
	@JoinColumn(name="topic_id")
	public Topic getTopic() {
		return topic;
	}
	public void setId(int id) {
		this.id = id;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public void setTopic(Topic topic) {
		this.topic = topic;
	}
}
 
c. 持久化
public void outputTest(){
 	Session se = sf.getCurrentSession();
 	se.beginTransaction();
 	for(int i=1;i<11;i++){
 		Category c = new Category();
 		c.setName("category"+i);
 		se.save(c);
 	}
 	
 	for(int i=1;i<11;i++){
 		Category c = new Category();
 		c.setId(1);
 		Topic t = new Topic();
 		t.setTitle("title"+i);
 		t.setCreatDate(new Date());
 		t.setCategory(c);
 		se.save(t);
 	}
 		
 	for(int i=1;i<11;i++){
 		Topic t = new Topic();
 		t.setId(1);
 		Msg message = new Msg();
 		message.setContent("message"+i);
 		message.setTopic(t);
 		se.save(message);
 	}
 
 	se.getTransaction().commit();
 }
操作结果
Hibernate
 
d.用查询获得model类的List集(查询对象)
public void testHQL01(){
 	Session se = sf.getCurrentSession();
 	se.beginTransaction();
 	Query q = se.createQuery("from Category");
 	List<Category> categorys = (List<Category>) q.list();
 	for(Category c : categorys){
 		System.out.println(c.getName());
 	}
	se.getTransaction().commit();
}
 e. 查询对象
public void testHQL02(){
	Session s = sf.getCurrentSession();
	s.beginTransaction();
	Query q = s.createQuery("from Category c where c.name > 'category5'");
 		
	//Query q = s.createQuery("from Category c order by c.name desc");    //倒序排列

	//Query q = s.createQuery("select distinct c from Category c"); //选择不重复的对象(主键)
		

	/*Query q = s.createQuery("from Category c where c.id>:min and c.id<:max");
	q.setParameter("min",2);    //还可以写作链式编程模式
	q.setParameter("max",8); 
	*/   //使用占位符


	/*Query q = s.createQuery("from Category c");
	q.setMaxResults(4);
	q.setFirstResult(2);
	*/  //分页

	List<Category> c = (List<Category>)q.list();
 	for(Category ca : c){
 		System.out.println(ca.getName());
 	}
 	s.getTransaction().commit();
 }
 
f. 查询字段
public void testHQL03(){
 	Session s = sf.getCurrentSession();
 	s.beginTransaction();
 	Query q = s.createQuery("select c.id, c.name from Category c ");
	//Query q = s.createQuery("select c.name, t.name from Title t join t.category c");//Join
 	List<Object[]> o = (List<Object[]>) q.list();
 	for(Object[] ob : o){
 		System.out.println(ob[0]+"-"+ob[1]);
 	}
 	s.getTransaction();
 }
 
q.list返回的结果是一个由Object对象数组组成的List:

Hibernate
 
g. 使用临时对象接收
先编写临时接收类:
package com.hibernate.test;
public class MsgInfo {
	private int id;
	private String title;
	private String content;
	private String category;
	
	public MsgInfo(int id, String title, String content, String category) {
		super();
		this.id = id;
		this.title = title;
		this.content = content;
		this.category = category;
	}

	public int getId() {
		return id;
	}
	public String getTitle() {
		return title;
	}
	public String getContent() {
		return content;
	}
	public String getCategory() {
		return category;
	}

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

	public void setTitle(String title) {
		this.title = title;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public void setCategory(String category) {
		this.category = category;
	}
}
 
接收
public void tempObjectTest(){
 	Session s = sf.getCurrentSession();
 	s.beginTransaction();
 	Query q = s.createQuery("select new com.hibernate.test.MsgInfo(m.id, m.topic.title, m.content, m.topic.category.name) from Msg m");  //必须保证接收类(MsgInfo)有对应的构造方法
  	for(Object o : q.list()){
  		MsgInfo mi = (MsgInfo)o;
 		System.out.println(mi.getId());
 		System.out.println(mi.getCategory());
 		System.out.println(mi.getTitle());
 		System.out.println(mi.getContent());
 	}
 	s.getTransaction().commit();
 }
 
h.取数据库日期时间
public void getCurrentTime(){
 	Session s = sf.openSession();
 	s.beginTransaction();
 	Query q = s.createQuery("select current_date,current_time,current_timestamp, t.id from Topic t");
 	System.out.println(q.list().size());
 	for(Object o : q.list()){
 		Object[] arr = (Object[])o;
		System.out.println(arr[0]+" "+arr[1]+" "+arr[2]);
 	}
 	s.getTransaction().commit();
 	s.close();
 }
 i.QBC语言和QBE语言
 
18. 注意session.clear()的运用
当运用hibernate在一个大集合中进行遍历,处理,需及时使用clear()方法清除缓存
 
19. 1+N问题
a.问题描述:一个oneToMany的对象,由于默认的fetch为eager,在欲获取此对象的list时,会自动同时发出SQL去除所有关联对象.如欲取出Topic的集合,而实际会把关联的Category全部select出来.
b.解决方案:
i.设置fetch=fetchType.LAZY,
造成的结果是不会去取对应的Category
ii.在HQL语句后加上join fetch:  "from Topic t left join fetch t.category c",
造成的结果是同样会去取Category,但会通过join多张表,只生成一句SQL语句
 
20. Query.list() 与Query.iterator()的区别(一般只用list):
a.用List 接收会直接取出所有数据,而Iterator只会先取得id,当需要其他数据时再取
b.iterator会用缓存
 
21. 缓存
a. 一级缓存 session级别的缓存
b. 二级缓存 sessionFactory级别的缓存,可以跨session存在
c. 使用二级缓存
a) hibernate.cfg.xml
<property name="cache.use_second_level_cache">true</property><!-- 使用二级缓存 -->
<property name="cache.provider_class">org.hibernate.cache.EhcacheProvider</property>  <!-- 使用哪一种二级缓存的实现 -->
b) 引入ehcache.jar commons-logging.jar
c) 具体缓存参数在ehcache.xml中配置
d) 在经常要读取的实体类上加@Cache注解
@Entity
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Category{}
d. 二级缓存使用场合
a) 经常被访问
b) 不经常改动
c) 数量有限
d) 如权限 组织机构
e.查询缓存(三级缓存)
a) 查询缓存依赖于二级缓存,要使用查询缓存,必须先打开二级缓存
b) 查询缓存可以实现多次相同的查询,只会发出一条SQL语句
c) 查询缓存是跨session的
f.使用查询缓存
a) hibernate.cfg.xml
<property name=cache.use_query_cache">true</property>
 b) 调用Query.setCacheable(true)
session.createQuery("from Topic t").setCacheable(true).list();
g.缓存算法
a)什么是缓存算法:缓存满后,新增的对象如何替换缓存中的对象
b) 三种缓存算法
i. LRU: least recently used 最近很少被使用
ii.LFU: least frequently used 最近使用频率很低,命中率低
iii.FIFO: first in first out 先进先出
c) 对于eccache的二级缓存实现,配置缓存算法的方法为,在其配置文件ehcache.xml中加入memoryStoreEvictionPolicy="LRU"
 
22. 事务的特性
ACID atomic原子性  Consistency一致性 Itegrity独立性 Durablility持久性
 
23. 事务并发出现的三种问题
a) 脏读: 读了另外一个事务没有提交的数据
b) 不可重复读: 对同一个数据前后读取值不一样
c) 幻读: 插入删除新数据对读取的影响
 
24.数据库处理三种问题的方案--数据库事务隔离机制
a)几种隔离级别
i.2read-uncommitted 可以读到未提交的数据 级别最低,效率最高 会出现 脏读 不可重复读 幻读
ii.4read-committed 只能读已提交的数据 最常用 不会出现脏读,但会出现不可重复读 幻读
iii.8repeatable-read 可重复读,读时加锁 事务执行中其他事务无法执行修改或插入操作
iv.serial 序列化读,级别最高,效率最低 不会出现此三项并发问题
b)数据库都有默认的隔离级别,mysql默认的隔离级别是repeatable-read,查看方式是
SELECT @@tx_isolation;
c)要自定义隔离级别,在hibernate配置文件中加人hibernate.connection.isolation=2,会通过JDBC设置数据库的隔离级别
 
25.乐观锁与悲观锁
a)为什么要用乐观锁和悲观锁:一般为提高效率,会将数据库的事务隔离级别设为read-committed,但这种情况下,可能会产生不可重复读和幻读的情况,此时在hibernate中,可以另外通过锁来解决
b)悲观锁:在提交前 将数据加锁锁定.
实现方式:session.load(Sccount.class, 12, LockMode.UPGRADE);
c)乐观锁:加入version字段,前后对比判断中间是否有其他session改变过
实现方式:在表对应的实体中新增属性int version; 在get方法上加注解@Version
当出现不可重复读或幻读时,会抛出org.hibernate.StaleObjctStateException异常
 
 
 
 
 
 
 
 

相关推荐