Java语言按内存地址来识别或区分一个类的不同对象,在关系数据库中按主键值来识别或区分同一个表中的不同记录,hibernate使用OID来建立内存对象与关系数据库表中记录的对应关系,OID在关系数据库表中的值是唯一的,内对中的java对象OID保存了数据库表中的主键值,确保OID的唯一性和久性,开发人员不应该让java应用程序来设定OID(很容易出现几个相同的OID),应该让数据库或者hibernate来管理这些OID(有些历史系统让java应用程序管理OID)。那么hibernate的OID生成策略是什么样子的呢?
1. 关系数据库按主键区分不同的记录
我们知道在关系数据库中,为了确保记录的唯一性,使用主键来区分不同的记录,主键必须有一定的约束条件。
Ø 不允许为null。
Ø 每条记录具有唯一的主键值,不允许主键值重复。
Ø 主键值是永久性的,不能改变的(相当于java中的final对象)。
一般来说,主键不应该含有业务含义(维护性问题),含有业务含义的主键称为自然主键,尽管可行,但有业务含义的主键不利于系统的维护,如某项业务改变可能会涉及到数据库表的改变,给维护带来了困难。比较合理的方式是使用代理主键(一般为ID),使主键不具备业务含义。
通常情况下,把主键(代理主键)定义为自动增长的标识符类型(代理主键的类型一般为short、int、long),许多数据库提供了自动增长的供,但不尽相同。
2. Java语言按内存地址区分不同对象
Java中的同一个对象是指引用指向同一块内存地址。”==” 比较的地址(哈希地址),equals(Object o)在没有重写的情况下也是按地址比较,他是Object类的一个方法,任何对象都拥有该方法并能重写,根据自己需要重写equals(Object o)(按值比较)对象。
- public class Customer {
- private String name;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- /**重写equals,按名字比较*/
- @Override
- public boolean equals(Object o) {
- if (this == o)
- return true;
- if (!(o instanceof Customer))
- return false;
- final Customer other = (Customer) o;
- if (this.getName().equals(other.getName()))
- return true;
- else
- return false;
- }
- }
JavaAPI中一些类已经重写,equals方法,按非值比较
Ø String类和Date类。
Ø Java包装类。
Ø 对于String来说,有点不同,new出来的对象在退栈区,直接赋值的在静态数据区。
- String s1 = new String("11"); //堆栈中
- String s2 = new String("11"); //堆栈中
- System.out.println(s1 == s2); //false
- System.out.println(s1.equals(s2)); //true
- String s3 = "11"; //String池中
- String s4 = "11"; //String池中
- System.out.println(s1 == s3); //false
- System.out.println(s1.equals(s3)); //true
- System.out.println(s3 == s4); //true
- System.out.println(s3.equals(s4)); //true
String池是按字符串的哈希码比较的,一个串的地址只有一个,如果几个对象的值相同,那么他们指向同一个地址。
3. Hibernate用对象标识符(OID-关系数据库中的主键)来区分对象
1) Java语言按内存地址识别或区分同一个类的不同对象。
2) 关系数据库使用主键来识别或区分同一张表中的不同记录。
3) Hibernate使用OID区分对象。
Hibernate作为中间者使用OID来统一这两者(对象-关系数据)的矛盾,主键—OID—java对象模型。
- Transaction tx = session.beginTransaction(); //1
- Customer c1 = (Customer)session.get(Customer.class,new Long(1));//2
- Customer c2 = (Customer)session.get(Customer.class,new Long(1));//3
- Customer c3 = (Customer)session.get(Customer.class,new Long(2));//4
- System.out.println(c1 == c2); //true //5
- System.out.println(c1 == c3); //false //6
- tx.commit(); //7
// 2 第一次加载OID为1的Customer对象时,从数据库拉出数据创建Customer对象实例,并保存到Session缓存中(SessionFactory一级缓存),c1引用指向该对象
// 3 第二次加载OID为1的Customer对象时,Hibernate首先在Session缓存中查找OID为1的对象(估计是使用反射机制自动比较OID,因为没有重写equals方法),若存在,直接把Session中的Customer的OID为1的对象的引用赋值给c2,此时c1与c2指向同一个对象。(在eclipse中使用日志并且设置显示SQL语句时会看到加载第二次时不会发送SQL语句)
//4 加载OID为3的Customer对象时,由于Session不存在OID为3的对象,所以要从数据库中拉出数据后创建Customer对象,并保存到Session缓存中。
Session有个clear()的函数,可以把Session中的缓存清除,如果在第一次加载OID为1的Customer对象后就清空Session缓存,那么在第二次加载OID为1的Customer对象时,由于缓存中没有OID为1的对象,所以要从数据库中拉出数据后创建Customer对象(与第一次加载OID为1的Customer对象不是同一个对象,所以c1 != c2),清空Session缓存可能在性能调优的时候用或者在某些特别事务里面不能使用缓存必须重新从数据库中读出数据后重新创建对象(安全性要求较高业务)。
为了持久化对象生成的标识符(OID-表的主键)的唯一性和不可变性,通常由hibernate或者底层数据库来给OID赋值(生成主键值)。把setID设为private就可以防止Java应用程序访问设置OID。
- private Long id;
- private void setId(Long id) {
- this.id = id;
- }
- public Long getId() {
- return this.id;
- }
在对象-关系映射文件中,<id>元素用来设置对象的标识符。
<id name="id"type="long" column="ID">
<generator class="increment"/>
</id>
上面的设置使用了hibernate生成标识符(OID)的策略,class 属性是生成OID所采用的策略,是一个类类型,生成标识符(OID)的类在org.hibernate.id包中,该包还包含了其他的标识符生成策略。increment是org.hibernate.id.IncrementGenerator的缩写。当然class的属性值可以使用全类名如 class=org.hibernate.id.IncrementGenerator
Hibernate提供的内置标识符(OID)生成器
标识符生成器 | 适用对象 | 描述 | 支持的数据库 |
increment | 代理主键 | 由hibernate自动一递升的方式生成标识符,每次增量为1 | |
indentify | 代理主键 | 由数据库底层生成标识符,前提是底层数据库支持自动增长字段类型 | DB2、Mysql、MS SQL Service、Sybase、HypersonicSQL |
sequence | 代理主键 | hibernate根据底层数据库的序列来生成标识符,前提是底层数据库支持序列 | DB2、PostgreSQL、Oracle、SAP DB |
hilo | 代理主键 | hibernate根据high/low算法来生成标识符,hibernate把特定表的字段作为"hight"的部分,默认情况下选用hibernate_unique_key表的next_hi字段 | |
native | 代理主键 | 根据底层数据库对自动生成标识符的支持能力,来选择identity、sequence、hilo | |
uuid.hex | 代理主键 | hibernate采用128位的UUID(Universal Unique Identification)算法来生成标识符,UUID能够在网络环境生成唯一的字符串标志符(估计跟MAC有关),不常用,占用空间多 | |
assigned | 自然主键 | 由java应用程序负责生成标识符,为了能让java应用程序设置OID,不能把setId()方法声明为private类型,应该尽量避免使用自然主键 | |
select | 遗留数据库代理/自然 | 有数据库的触发器生成标识符 | |
foreign | 一对一关联 | 用另一个关联的对象的标识符作为当期的标识符 |
4. Hibernate的内置生成器标识符生成器(OID-主键)的用法
以customers的ID字段(主键)和name字段为例
(1) increment标识符生成器
increment标识符生成器有hibernate递升的方式为代理主键赋值。
- *.hbm.xml
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <id name="id" type="long"column="ID">
- <meta attribute="scope-set">private</meta>
- <generator class="increment"></generator>
- </id>
- <property name="name" type="string">
- <column name="NAME" length="15"></column>
- </property>
- </class>
- </hibernate-mapping>
生成java代码。
- private Long id;
- private void setId(Long id) {
- this.id = id;
- }
- public Long getId() {
- return this.id;
- }
- 生成的SQL。
- create table customers(
- ID bigint not null,
- NAME varchar(15) not null
- primary key (ID)
- );
Insert SQL语句。
insert into customers(NAME,ID)values(?,?);
可以看出在insert语句时含有ID字段,说明Hibernate在持久化时会议递升的方式生成标识符,hibernate先从customers表中选出最大键,然后增量为1.
select max(ID) from customers;
这种标识符生成方式一般只在拥有一个服务器的数据库使用,在集群环境下会出现生成的标识符相同的问题。(好像是多线程问题无法执行数据库事务问题)
(2) identity标识符生成器
identity标识符生成器有底层数据库来负责生成标识符,他要求底层数据库把主键定义为自动增长字段类型。
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <id name="id" type="long"column="ID">
- <meta attribute="scope-set">private</meta>
- <generator class="identity"></generator>
- </id>
- <property name="name" type="string">
- <column name="NAME" length="15"></column>
- </property>
- </class>
- </hibernate-mapping>
Mysql数据库的SQL。
- create table customers(
- ID bigintauto_increment not null,
- NAME varchar(15) not null
- primary key (ID)
- );
MS SQL Server数据库的SQL.
- create table customers(
- ID bigintidentity not null,
- NAME varchar(15) not null
- primary key (ID)
- );
Insert SQL语句。
insert into customers(NAME) values(?);
插入SQL语句中没有ID字段,标识符生成有底层数据库完成。
Identity依赖于底层数据库,要求数据库支持自动增长字段类型,Oracle数据库不支持。
(3) sequence标识符生成器
sequence标识符生成器利用底层数据库提供的序列来生成标识符。
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <id name="id" type="long"column="ID">
- <meta attribute="scope-set">private</meta>
- <generator class="sequence">
- <param name="sequence">customers_id_seq</param>
- </generator>
- </id>
- <property name="name" type="string">
- <column name="NAME" length="15"></column>
- </property>
- </class>
- </hibernate-mapping>
Oracle数据库SQL.
- create table customers(
- ID bigint not null,
- NAME varchar(15) not null
- primary key (ID)
- );
create sequencecustomers_id_seq;
生成了sequence序列。
Insert SQL
- insert into customers(NAME,ID)values(?,?);
insert SQL中包含有ID字段,可见hibernate先从数据库中的customers_id_seq序列中获得一个唯一的序列号。
Sequence依赖底层数据库的序列,需要数据库支持序列才行,但是Mysql不支持。
(4) hilo标识符生成器
hilo标识符生成器有hibernate按照hight/low算法生成标识符,从数据库特定表中获取一个high值。
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <id name="id" type="long"column="ID">
- <meta attribute="scope-set">private</meta>
- <generator class="hilo">
- <param name="table">hi_value</param>
- <param name="column">next_value</param>
- <param name="max_lo">100</param>
- </generator>
- </id>
- <property name="name" type="string">
- <column name="NAME" length="15"></column>
- </property>
- </class>
- </hibernate-mapping>
SQL
- create table customers(
- ID bigint not null,
- NAME varchar(15) not null
- primary key (ID)
- );
create table hi_value(
next_value_integer
);
insert into hi_value values(0);
insert SQL
- insert into customers(NAME,ID)values(?,?);
insert SQL中含有ID字段,hibernate负责生成标识符,hilo生成标识符时读取hi_valu表中的next_value的值,并修改,这是由数据库的事务处理的,不是hibernate事务。
Hilo不依赖与底层数据库,所有具有通用性,,但是只能在一个数据库中保证唯一。,但用户自行提供数据库连接时或者使用JTA时,应用程序无法使用hilo,这种情况下如果数据库支持序列,可以使用seqhilo
(5) native标识符生成
native根据底层数据库对自动生成标识符的支持能力,选择identity、sequence、hilo标识符生成器。
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <id name="id" type="long"column="ID">
- <meta attribute="scope-set">private</meta>
- <generator class="native"></generator>
- </id>
- <property name="name" type="string">
- <column name="NAME" length="15"></column>
- </property>
- </class>
- </hibernate-mapping>
MySQL SQL
- create table customers(
- ID bigintauto_increment not null,
- NAME varchar(15) not null
- primary key (ID)
- );
MS SQL Server SQL
- create table customers(
- ID bigintidentity not null,
- NAME varchar(15) not null
- primary key (ID)
- );
Insert SQL
- insert into customers(NAME) values(?);
insert SQL语句中没有ID字段,由数据库底层生成标识符。
由于是根据各种数据库的自动生成序列,,适合跨平台开发。同一个hibernate连接多种数据库系统。
5. 映射自然主键
自然主键是具有业务含义的主键。(设计时应尽量避免使用)
(1) 映射单个自然主键
Customers使用name作为自然主键.
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <id name="name" column="NAME"type="string">
- <generator class="assigned"></generator>
- </id>
- <version name="version" column="VERSION"unsaved-value="null"></version>
- </class>
- </hibernate-mapping>
<version>版本控制元素,如果Customer对象的version属性为null,表示临时对象,Customers类必要version属性.saveorUpdate()根据version的值来决定是save()还是update().如果没有定义则必须手工控制(选择调用save()或者update(0)或者使用hibernate的拦截器(interceptor).
可以通过Session的getIdentifier()返回对象的OID.
(2) 映射复合自然主键
若使用name 和student_id作为复合主键(OID).
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <composite-id>
- <key-property name="name" column="NAME"type="string"/>
- <key-property name="student_id" column="STUDENT_ID" type="long"/>
- </composite-id>
- <version name="version" column="VERSION"unsaved-value="null"></version>
- </class>
- </hibernate-mapping>
但是Customer必须实现java.io.Serializable接口,并且重新定义equals()和hashCode()方法。equals()判断两个Customer对象相等得条件,两个Customer的nameh属性和studentId属性都相等。hashCode()方法实现原则:用equals()方法判断相等的两个Customer对象具有相同的哈希码.
不同还可以单独定义主键类,如CustomerId,实现java.io.Serializable接口并重写java.io.Serializable equals()和hashCode()方法。在Customer类中把CustomerId作为属性值。
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <composite-id>
- <key-property name="name" column="NAME"type="string"/>
- <key-property name="student_id" column="STUDENT_ID" type="long"/>
- </composite-id>
- <version name="version" column="VERSION"unsaved-value="null"></version>
- </class>
- </hibernate-mapping>
如studentId还作为外键参考Student,那么在Customer类中还需添加Student的属性。
- Customer.hbm.xml
- <hibernate-mapping package="com.model">
- <class name="Customer" dynamic-insert="true"dynamic-update="true">
- <composite-id name="customerId" class="com.model.CustomerId">
- <key-property name="name" column="NMAE"type="string"/>
- <key-property name="studentid" column="STUDENT_ID"type="long"/>
- </composite-id>
- <version name="version" column="VERSION"unsaved-value="null"/>
- <many-to-one name="student" class="Student"column="STUDENT_ID" insert="false" update="false"/>
- </class>
- </hibernate-mapping>
设置insert和update的属性为false,表名当Customer更新时会忽略student的属性。
- Student.hbm.xml
- <hibernate-mapping package="com.model">
- <class name="Student" dynamic-insert="true"dynamic-update="true">
- <composite-id name="customerId" class="CustomderId">
- <key-property name="name" column="NAME"type="string"/>
- <key-many-to-one name="student" class="Student"column="STUDENT_ID"/>
- </composite-id>
- <version name="version" column="VERSION"unsaved-value="null"/>
- </class>
- </hibernate-mapping>
6. 总结
Java语言按内存地址识别和区分同一个类的不同对象实例,关系数据库根据主键识别和区分同一张表中的不同记录值。Hibernate根据对象标识符(OID)识别和区分对象,它作为联系java对象和关系数据库库中表中的记录对应的桥梁。关系数据库表的主键尽量不要含有业务逻辑。Hibernate提供了几种内置标识符生成器,生成唯一的且不可改变的OID.不同数据所支持的标识符生成器不同。
Ø MySQL:identity、increment、hilo、native
Ø MS SQL Server:identity、increment、hilo、native
Ø Oracle:sequence、seqhilo、hilo、increment、native
Ø 跨平台开发:native
7. 参考书目
《精通Hibernate:Java对象持久化技术详解》 ( 第二版) 电子工业出版社 作者:孙卫琴