首先,我们将创建一个简单的控制台(console-based)Hibernate程序。我们使用内置数据库(in-memory database) (HSQL DB)。所以我们不必安装不论什么数据库server。
让我们如果我们希望有一个小程序能够保存我们希望关注的事件(Event)和这些事件的信息。 (译者注:在本教程的后面部分,我们将直接使用Event而不是它的中文翻译“事件”,以免混淆。)
我们做的第一件事是建立我们的开发文件夹。并把全部须要用到的Java库文件放进去。 从Hibernate站点的下载页面下载Hibernate分发版本号。
解压缩包并把/lib以下的全部库文件放到我们新的开发文件夹以下的/lib文件夹以下。 看起来就像这样:
. +lib antlr.jar cglib-full.jar asm.jar asm-attrs.jars commons-collections.jar commons-logging.jar ehcache.jar hibernate3.jar jta.jar dom4j.jar log4j.jar
This is the minimum set of required libraries (note that we also copied hibernate3.jar, the main archive) for Hibernate. See the README.txt file in the lib/ directory of the Hibernate distribution for more information about required and optional third-party libraries. (Actually, Log4j is not required but preferred by many developers.) 这个是Hibernate执行所须要的最小库文件集合(注意我们也拷贝了Hibernate3.jar,这个是最重要的库)。
能够在Hibernate分发版本号的lib/文件夹下查看README.txt。以获取很多其它关于所需和可选的第三方库文件信息 (其实,Log4j并非必须的库文件可是很多开发人员都喜欢用它)。
接下来我们创建一个类。用来代表那些我们希望储存在数据库里面的event.
我们的第一个持久化类是 一个简单的JavaBean class,带有一些简单的属性(property)。 让我们来看一下代码:
import java.util.Date; public class Event { private Long id; private String title; private Date date; Event() {} public Long getId() { return id; } private void setId(Long id) { this.id = id; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } }
你能够看到这个class对属性(property)的存取方法(getter and setter method) 使用标准的JavaBean命名约定,同一时候把内部字段(field)隐藏起来(private visibility)。 这个是个受推荐的设计方式。但并非必须这样做。 Hibernate也能够直接訪问这些字段(field),而使用訪问方法(accessor method)的优点是提供了程序重构的时候健壮性(robustness)。
id 属性(property) 为一个Event实例提供标识属性(identifier property)的值- 假设我们希望使用Hibernate的全部特性,那么我们全部的持久性实体类(persistent entity class)(这里也包含一些次要依赖类) 都须要一个标识属性(identifier property)。
而其实,大多数应用程序(特别是web应用程序)都须要识别特定的对象,所以你应该 考虑使用标识属性而不是把它当作一种限制。然而,我们通常不会直接操作一个对象的标识符(identifier), 因此标识符的setter方法应该被声明为私有的(private)。这样当一个对象被保存的时候,仅仅有Hibernate能够为它分配标识符。 你会发现Hibernate能够直接訪问被声明为public,private和protected等不同级别訪问控制的方法(accessor method)和字段(field)。 所以选择哪种方式来訪问属性是全然取决于你,你能够使你的选择与你的程序设计相吻合。
全部的持久类(persistent classes)都要求有无參的构造器(no-argument constructor); 由于Hibernate必需要使用Java反射机制(Reflection)来实例化对象。构造器(constructor)的訪问控制能够是私有的(private), 然而当生成执行时代理(runtime proxy)的时候将要求使用至少是package级别的訪问控制。这样在没有字节码编入 (bytecode instrumentation)的情况下。从持久化类里获取数据会更有效率一些。
我们把这个Java源码文件放到我们的开发文件夹以下一个叫做src的文件夹里。 这个文件夹如今应该看起来像这样:
. +lib <Hibernate and third-party libraries> +src Event.java
在下一步里,我们将把这个持久类(persisten class)的信息通知Hibernate
Hibernate须要知道如何去载入(load)和存储(store)我们的持久化类的对象。
这里正是Hibernate映射文件(mapping file)发挥作用的地方。 映射文件告诉Hibernate它应该訪问数据库里面的哪个表(table)和应该使用表里面的哪些字段(column)。
一个映射文件的基本结构看起来像这样:
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> [...] </hibernate-mapping>
注意Hibernate的DTD是很复杂的。
你能够在你的编辑器或者IDE里面使用它来自己主动提示并完毕(auto-completion)那些用来映射的XML元素(element)和属性(attribute)。
你也能够用你的文本编辑器打开DTD-这是最简单的方式来浏览全部元素和參数,查看它们的缺省值以及它们的凝视,以得到一个总体的概观。
同一时候也要注意Hibernate不会从web上面获取DTD文件。尽管XML里面的URL或许会建议它这样做。可是Hibernate会首先查看你的程序的classpath。 DTD文件被包含在hibernate3.jar。同一时候也在Hibernate分发版的src/路径下。
在以后的样例里面,我们将通过省略DTD的声明来缩短代码长度。可是显然,在实际的程序中,DTD声明是必须的。
在两个hibernate-mapping标签(tag)中间, 我们包括了一个 class元素(element)。
全部的持久性实体类(persistent entity classes)(再次声明, 这里也包括那些依赖类,就是那些次要的实体)都须要一个这种映射,来映射到我们的SQL database。
<hibernate-mapping> <class name="Event" table="EVENTS"> </class> </hibernate-mapping>
我们到如今为止做的一切是告诉Hibernate如何从数据库表(table)EVENTS里持久化和 载入Event类的对象。每一个实例相应数据库里面的一行。
如今我们将继续讨论有关唯一标识属性(unique identifier property)的映射。 另外,我们不希望去考虑如何产生这个标识属性,我们将配置Hibernate的标识符生成策略(identifier generation strategy)来产生代用主键。
<hibernate-mapping> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </id> </class> </hibernate-mapping>
id元素是标识属性(identifer property)的声明。 name="id" 声明了Java属性(property)的名字 - Hibernate将使用getId()和setId()来訪问它。 字段參数(column attribute)则告诉Hibernate我们使用EVENTS表的哪个字段作为主键。
嵌套的generator元素指定了标识符的生成策略 - 在这里我们使用increment,这个是很easy的在内存中直接生成数字的方法。多数用于測试(或教程)中。
Hibernate同一时候也支持使用数据库生成(database generated),全局唯一性(globally unique)和应用程序指定(application assigned) (或者你自己为不论什么已有策略所写的扩展) 这些方式来生成标识符。
最后我们还必须在映射文件中面包含须要持久化属性的声明。
缺省的情况下,类里面的属性都被视为非持久化的:
<hibernate-mapping> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </id> <property name="date" type="timestamp" column="EVENT_DATE"/> <property name="title"/> </class> </hibernate-mapping>
和id元素相似,property元素的name參数 告诉Hibernate使用哪个getter和setter方法。
为什么date属性的映射包含column參数,可是title却没有? 当没有设定column參数的时候。Hibernate缺省使用属性名作为字段(column)名。对于title,这样工作得非常好。 然而,date在多数的数据库里。是一个保留keyword。所以我们最好把它映射成另外一个名字。
下一件有趣的事情是title属性缺少一个type參数。
我们声明并使用在映射文件中面的type,并不像我们假想的那样,是Java data type, 同一时候也不是SQL database type。这些类型被称作Hibernate mapping types。 它们把数据类型从Java转换到SQL data types。假设映射的參数没有设置的话,Hibernate也将尝试去确定正确的类型转换和它的映射类型。 在某些情况下这个自己主动检測(在Java class上使用反射机制)不会产生你所期待或者 须要的缺省值。这里有个样例是关于date属性。
Hibernate无法知道这个属性应该被映射成以下这些类型中的哪一个: SQL date,timestamp。time。
我们通过声明属性映射timestamp来表示我们希望保存全部的关于日期和时间的信息。
这个映射文件(mapping file)应该被保存为Event.hbm.xml,和我们的EventJava 源文件放在同一个文件夹下。
映射文件的名字能够是随意的,然而hbm.xml已经成为Hibernate开发人员社区的习惯性约定。 如今文件夹应该看起来像这样:
. +lib <Hibernate and third-party libraries> +src Event.java Event.hbm.xml
我们继续进行Hibernate的主要配置。
我们如今已经有了一个持久化类和它的映射文件。是时候配置Hibernate了。
在我们做这个之前,我们须要一个数据库。 HSQL DB。一个java-based内嵌式SQL数据库(in-memory SQL Database),能够从HSQL DB的站点上下载。
实际上。你只须要下载/lib/文件夹中的hsqldb.jar。把这个文件放在开发文件夹的lib/文件夹里面。
在开发文件夹以下创建一个叫做data的文件夹 - 这个是HSQL DB存储它的数据文件的地方。
Hibernate是你的程序里连接数据库的那个应用层,所以它须要连接用的信息。连接(connection)是通过一个也由我们配置的JDBC连接池(connection pool)。 Hibernate的分发版里面包含了一些open source的连接池。可是我们已经决定在这个教程里面使用内嵌式连接池。
假设你希望使用一个产品级的第三方连接池软件,你必须拷贝所需的库文件去你的classpath并使用不同的连接池设置。
为了配置Hibernate,我们能够使用一个简单的hibernate.properties文件, 或者一个略微复杂的hibernate.cfg.xml,甚至能够全然使用程序来配置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> <!-- Database connection settings --> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:data/tutorial</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">1</property> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.HSQLDialect</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create</property> <mapping resource="Event.hbm.xml"/> </session-factory> </hibernate-configuration>
注意这个XML配置使用了一个不同的DTD。我们配置Hibernate的SessionFactory- 一个关联于特定数据库全局性的工厂(factory)。假设你要使用多个数据库,通常应该在多个配置文件里使用多个<session-factory> 进行配置(在更早的启动步骤中进行)。
最開始的4个property元素包括必要的JDBC连接信息。
dialectproperty 表明Hibernate应该产生针对特定数据库语法的SQL语句。hbm2ddl.auto选项将自己主动生成数据库表定义(schema)- 直接插入数据库中。当然这个选项也能够被关闭(通过去除这个选项)或者通过Ant任务SchemaExport来把数据库表定义导入一个文件里进行优化。 最后。为持久化类增加映射文件。
把这个文件复制到源码文件夹以下。这样它就位于classpath的root路径上。Hibernate在启动时会自己主动 在它的根文件夹開始寻找名为hibernate.cfg.xml的配置文件。
在这个教程里面,我们将用Ant来编译程序。你必须先安装Ant-能够从Ant download page 下载它。如何安装Ant不是这个教程的内容。请參考Ant manual。
当你安装完了Ant,我们就能够開始创建编译脚本,它的文件名称是build.xml。把它直接放在开发文件夹以下。
注意Ant的分发版通常功能都是不完整的(就像Ant FAQ里面说得那样),所以你经常不得不须要自己动手来完好Ant。 比如:假设你希望在你的build文件中面使用JUnit功能。
为了让JUnit任务被激活(这个教程里面我们并不须要这个任务), 你必须拷贝junit.jar到ANT_HOME/lib文件夹下或者删除ANT_HOME/lib/ant-junit.jar这个插件。
一个主要的build文件看起来像这样
<project name="hibernate-tutorial" default="compile"> <property name="sourcedir" value="${basedir}/src"/> <property name="targetdir" value="${basedir}/bin"/> <property name="librarydir" value="${basedir}/lib"/> <path id="libraries"> <fileset dir="${librarydir}"> <include name="*.jar"/> </fileset> </path> <target name="clean"> <delete dir="${targetdir}"/> <mkdir dir="${targetdir}"/> </target> <target name="compile" depends="clean, copy-resources"> <javac srcdir="${sourcedir}" destdir="${targetdir}" classpathref="libraries"/> </target> <target name="copy-resources"> <copy todir="${targetdir}"> <fileset dir="${sourcedir}"> <exclude name="**/*.java"/> </fileset> </copy> </target> </project>
这个将告诉Ant把全部在lib文件夹下以.jar结尾的文件增加classpath中用来进行编译。 它也将把全部的非Java源码文件,比如配置和Hibernate映射文件,复制到目标文件夹下。
假设你如今执行Ant, 你将得到下面输出:
C:/hibernateTutorial/>ant Buildfile: build.xml copy-resources: [copy] Copying 2 files to C:/hibernateTutorial/bin compile: [javac] Compiling 1 source file to C:/hibernateTutorial/bin BUILD SUCCESSFUL Total time: 1 second
是时候来载入和储存一些Event对象了。可是首先我们不得不完毕一些基础的代码。 我们必须启动Hibernate。这个启动过程包含创建一个全局性的SessoinFactory并把它储存在一个应用程序easy訪问的地方。
SessionFactory能够创建并打开新的Session。 一个Session代表一个单线程的单元操作,SessionFactory则是一个线程安全的全局对象,仅仅须要创建一次。
我们将创建一个HibernateUtil帮助类(helper class)来负责启动Hibernate并使 操作Session变得easy。这个帮助类将使用被称为ThreadLocal Session 的模式来保证当前的单元操作和当前线程相关联。让我们来看一眼它的实现:
import org.hibernate.*; import org.hibernate.cfg.*; public class HibernateUtil { public static final SessionFactory sessionFactory; static { try { // Create the SessionFactory from hibernate.cfg.xml sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) { // Make sure you log the exception, as it might be swallowed System.err.println("Initial SessionFactory creation failed." + ex); throw new ExceptionInInitializerError(ex); } } public static final ThreadLocal session = new ThreadLocal(); public static Session currentSession() throws HibernateException { Session s = (Session) session.get(); // Open a new Session, if this thread has none yet if (s == null) { s = sessionFactory.openSession(); // Store it in the ThreadLocal variable session.set(s); } return s; } public static void closeSession() throws HibernateException { Session s = (Session) session.get(); if (s != null) s.close(); session.set(null); } }
这个类不只在它的静态初始化过程(仅当载入这个类的时候被JVM运行一次)中产生全局SessionFactory, 同一时候也有一个ThreadLocal变量来为当前线程保存Session。不论你何时 调用HibernateUtil.currentSession(),它总是返回同一个线程中的同一个Hibernate单元操作。 而一个HibernateUtil.closeSession()调用将终止当前线程相联系的那个单元操作。
在你使用这个帮助类之前,确定你明确Java关于本地线程变量(thread-local variable)的概念。一个功能更加强大的 HibernateUtil帮助类能够在CaveatEmptorhttp://caveatemptor.hibernate.org/找到 -它同一时候也出如今书:《Hibernate in Action》中。注意当你把Hibernate部署在一个J2EE应用server上的时候,这个类不是必须的: 一个Session会自己主动绑定到当前的JTA事物上,你能够通过JNDI来查找SessionFactory。
假设你使用JBoss AS,Hibernate能够被部署成一个受管理的系统服务(system service)并自己主动绑定SessionFactory到JNDI上。
把HibernateUtil.java放在开发文件夹的源码路径以下,与 Event.java放在一起:
. +lib <Hibernate and third-party libraries> +src Event.java Event.hbm.xml HibernateUtil.java hibernate.cfg.xml +data build.xml
再次编译这个程序不应该有问题。
最后我们须要配置一个日志系统 - Hibernate使用通用日志接口,这同意你在Log4j和 JDK 1.4 logging之间进行选择。多数开发人员喜欢Log4j:从Hibernate的分发版(它在etc/文件夹下)拷贝 log4j.properties到你的src文件夹,与hibernate.cfg.xml.放在一起。 看一眼配置演示样例,你能够改动配置假设你希望看到很多其它的输出信息。缺省情况下。仅仅有Hibernate的启动信息会显示在标准输出上。
教程的基本框架完毕了 - 如今我们能够用Hibernate来做些真正的工作。
最终,我们能够使用Hibernate来载入和存储对象了。我们编写一个带有main()方法 的EventManager类:
import org.hibernate.Transaction; import org.hibernate.Session; import java.util.Date; public class EventManager { public static void main(String[] args) { EventManager mgr = new EventManager(); if (args[0].equals("store")) { mgr.createAndStoreEvent("My Event", new Date()); } HibernateUtil.sessionFactory.close(); } }
我们从命令行读入一些參数,假设第一个參数是"store",我们创建并储存一个新的Event:
private void createAndStoreEvent(String title, Date theDate) { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); Event theEvent = new Event(); theEvent.setTitle(title); theEvent.setDate(theDate); session.save(theEvent); tx.commit(); HibernateUtil.closeSession(); }
我们创建一个新的Event对象并把它传递给Hibernate。Hibernate如今负责创建SQL并把 INSERT命令传给数据库。在执行它之前。让我们花一点时间在Session和Transaction的处理代码上。
每一个Session是一个独立的单元操作。你会对我们有另外一个API:Transaction而感到惊奇。 这暗示一个单元操作能够拥有比一个单独的数据库事务更长的生命周期 - 想像在web应用程序中,一个单元操作跨越多个Http request/response循环 (比如一个创建对话框)。依据“应用程序用户眼中的单元操作”来分割事务是Hibernate的基本设计思想之中的一个。我们调用 一个长生命期的单元操作Application Transaction时,通常包装几个更生命期较短的数据库事务。
为了简化问题,在这个教程里我们使用Session和Transaction之间是1对1关系的粒度(one-to-one granularity)。
Transaction.begin()和commit()都做些什么?rollback()在哪些情况下会产生错误? Hibernate的Transaction API 实际上是可选的, 可是我们一般会为了便利性和可移植性而使用它。 假设你宁可自己处理数据库事务(比如,调用session.connection.commit()),通过直接和无管理的JDBC。这样将把代码绑定到一个特定的部署环境中去。 通过在Hibernate配置中设置Transaction工厂,你能够把你的持久化层部署在不论什么地方。 查看第 12 章 事务和并发了解很多其它关于事务处理和划分的信息。在这个样例中我们也忽略不论什么异常处理和事务回滚。
为了第一次执行我们的应用程序,我们必须添加一个能够调用的target到Ant的build文件里。
<target name="run" depends="compile"> <java fork="true" classname="EventManager" classpathref="libraries"> <classpath path="${targetdir}"/> <arg value="${action}"/> </java> </target>
action參数的值是在通过命令行调用这个target的时候设置的:
C:/hibernateTutorial/>ant run -Daction=store
你应该会看到,编译结束以后。Hibernate依据你的配置启动,并产生一大堆的输出日志。在日志最后你会看到以下这行:
[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)
这是Hibernate运行的INSERT命令。问号代表JDBC的待绑定參数。假设想要看到绑定參数的值或者降低日志的长度, 检查你在log4j.properties文件中的设置。
如今我们想要列出全部已经被存储的event,所以我们添加一个条件分支选项到main方法中去。
if (args[0].equals("store")) { mgr.createAndStoreEvent("My Event", new Date()); } else if (args[0].equals("list")) { List events = mgr.listEvents(); for (int i = 0; i < events.size(); i++) { Event theEvent = (Event) events.get(i); System.out.println("Event: " + theEvent.getTitle() + " Time: " + theEvent.getDate()); } }
我们也添加一个新的listEvents()方法:
private List listEvents() { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); List result = session.createQuery("from Event").list(); tx.commit(); session.close(); return result; }
我们在这里是用一个HQL(Hibernate Query Language-Hibernate查询语言)查询语句来从数据库中 载入全部存在的Event。Hibernate会生成正确的SQL,发送到数据库并使用查询到的数据来生成Event对象。 当然你也能够使用HQL来创建更加复杂的查询。
假设你如今使用命令行參数-Daction=list来执行Ant,你会看到那些至今为止我们储存的Event。 假设你是一直一步步的尾随这个教程进行的,你或许会惊讶这个并不能工作 - 结果永远为空。原因是hbm2ddl.auto 打开了一个Hibernate的配置选项:这使得Hibernate会在每次执行的时候又一次创建数据库。
通过从配置里删除这个选项来禁止它。
执行了几次store之后,再执行list,你会看到结果出如今列表里。
另外,自己主动生成数据库表并导出在单元測试中是很实用的。
我们已经映射了一个持久化实体类到一个表上。让我们在这个基础上添加一些类之间的关联性。 首先我们往我们程序里面添加人(people)的概念。并存储他们所參与的一个Event列表。
(译者注:与Event一样。我们在后面的教程中将直接使用person来表示“人”而不是它的中文翻译)
最初的Person类是简单的:
public class Person { private Long id; private int age; private String firstname; private String lastname; Person() {} // Accessor methods for all properties, private setter for 'id' }
Create a new mapping file called Person.hbm.xml:
<hibernate-mapping> <class name="Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="increment"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> </class> </hibernate-mapping>
Finally, add the new mapping to Hibernate's configuration:
<mapping resource="Event.hbm.xml"/> <mapping resource="Person.hbm.xml"/>
我们如今将在这两个实体类之间创建一个关联。显然。person能够參与一系列Event。而Event也有不同的參加者(person)。 设计上面我们须要考虑的问题是关联的方向(directionality)。阶数(multiplicity)和集合(collection)的行为。
我们将向Person类添加一组Event。这样我们能够轻松的通过调用aPerson.getEvents() 得到一个Person所參与的Event列表,而不必运行一个显式的查询。
我们使用一个Java的集合类:一个Set,由于Set 不同意包含反复的元素并且排序和我们无关。
眼下为止我们设计了一个单向的,在一端有很多值与之相应的关联,通过Set来实现。 让我们为这个在Java类里编码并映射这个关联:
public class Person { private Set events = new HashSet(); public Set getEvents() { return events; } public void setEvents(Set events) { this.events = events; } }
在我们映射这个关联之前。先考虑这个关联另外一端。非常显然的,我们能够保持这个关联是单向的。假设我们希望这个关联是双向的, 我们能够在Event里创建另外一个集合,比如:anEvent.getParticipants()。
这是留给你的一个设计选项,可是从这个讨论中我们能够非常清楚的了解什么是关联的阶数(multiplicity):在这个关联的两端都是“多”。 我们叫这个为:多对多(many-to-many)关联。因此。我们使用Hibernate的many-to-many映射:
<class name="Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="increment"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> <set name="events" table="PERSON_EVENT"> <key column="PERSON_ID"/> <many-to-many column="EVENT_ID" class="Event"/> </set> </class>
Hibernate支持全部种类的集合映射,<set>是最普遍被使用的。
对于多对多(many-to-many)关联(或者叫n:m实体关系), 须要一个用来储存关联的表(association table)。表里面的每一行代表从一个person到一个event的一个关联。
表名是由set元素的table属性值配置的。关联里面的标识字段名。person的一端,是 由<key>元素定义,event一端的字段名是由<many-to-many>元素的 column属性定义的。你也必须告诉Hibernate集合中对象的类(也就是位于这个集合所代表的关联另外一端的类)。
这个映射的数据库表定义例如以下:
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | |_____________| |__________________| | PERSON | | | | | |_____________| | *EVENT_ID | <--> | *EVENT_ID | | | | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | |_____________| | FIRSTNAME | | LASTNAME | |_____________|
让我们把一些people和event放到EventManager的一个新方法中:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); Event anEvent = (Event) session.load(Event.class, eventId); aPerson.getEvents().add(anEvent); tx.commit(); HibernateUtil.closeSession(); }
在载入一个Person和一个Event之后,简单的使用普通的方法改动集合。
如你所见。没有显式的update()或者save(), Hibernate自己主动检測到集合已经被改动 并须要保存。这个叫做automatic dirty checking。你也能够尝试改动不论什么对象的name或者date的參数。 仅仅要他们处于persistent状态,也就是被绑定在某个Hibernate Session上(比如:他们 刚刚在一个单元操作从被载入或者保存),Hibernate监视不论什么改变并在后台隐式运行SQL。同步内存状态和数据库的过程,通常仅仅在 一个单元操作结束的时候发生。这个过程被叫做flushing。
你当然也能够在不同的单元操作里面载入person和event。或者在一个Session以外改动一个 不是处在持久化(persistent)状态下的对象(假设该对象以前以前被持久化。我们称这个状态为脱管(detached))。 在程序里,看起来像以下这样:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); Event anEvent = (Event) session.load(Event.class, eventId); tx.commit(); HibernateUtil.closeSession(); aPerson.getEvents().add(anEvent); // aPerson is detached Session session2 = HibernateUtil.currentSession(); Transaction tx2 = session.beginTransaction(); session2.update(aPerson); // Reattachment of aPerson tx2.commit(); HibernateUtil.closeSession(); }
对update的调用使一个脱管对象(detached object)又一次持久化。你能够说它被绑定到 一个新的单元操作上,所以不论什么你对它在脱管(detached)状态下所做的改动都会被保存到数据库里。
这个对我们当前的情形不是非常实用,可是它是非常重要的概念,你能够把它设计进你自己的程序中。如今,加进一个新的 选项到EventManager的main方法中,并从命令行执行它来完毕这个练习。假设你须要一个person和 一个event的标识符 - save()返回它。*******这最后一句看不明确
上面是一个关于两个同等地位的类间关联的样例。这是在两个实体之间。像前面所提到的那样,也存在其他的特别的类和类型。这些类和类型一般是“次要的”。 当中一些你已经看到过。好像int或者String。我们称呼这些类为值类型(value type), 它们的实例依赖(depend)在某个特定的实体上。
这些类型的实例没有自己的身份(identity),也不能在实体间共享 (比方两个person不能引用同一个firstname对象,即使他们有同样的名字)。当然,value types并不只在JDK中存在 (其实,在一个Hibernate程序中,全部的JDK类都被视为值类型)。你也能够写你自己的依赖类,比如Address, MonetaryAmount。
你也能够设计一个值类型的集合(collection of value types)。这个在概念上与实体的集合有非常大的不同,可是在Java里面看起来差点儿是一样的。
我们把一个值类型对象的集合增加Person。我们希望保存email地址。所以我们使用String。 而这次的集合类型又是Set:
private Set emailAddresses = new HashSet(); public Set getEmailAddresses() { return emailAddresses; } public void setEmailAddresses(Set emailAddresses) { this.emailAddresses = emailAddresses; }
Set的映射
<set name="emailAddresses" table="PERSON_EMAIL_ADDR"> <key column="PERSON_ID"/> <element type="string" column="EMAIL_ADDR"/> </set>
比較这次和较早先的映射,区别主要在element部分这次并没有包含对其他实体类型的引用,而是使用一个元素类型是 String的集合(这里使用小写的名字是向你表明它是一个Hibernate的映射类型或者类型转换器)。 和曾经一样,set的table參数决定用于集合的数据库表名。key元素 定义了在集合表中使用的外键。element元素的column參数定义实际保存String值 的字段名。
看一下改动后的数据库表定义。
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | ___________________ |_____________| |__________________| | PERSON | | | | | | | |_____________| | PERSON_EMAIL_ADDR | | *EVENT_ID | <--> | *EVENT_ID | | | |___________________| | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | | *EMAIL_ADDR | |_____________| | FIRSTNAME | |___________________| | LASTNAME | |_____________|
你能够看到集合表(collection table)的主键实际上是个复合主键,同一时候使用了2个字段。
这也暗示了对于同一个 person不能有反复的email地址,这正是Java里面使用Set时候所须要的语义(Set里元素不能反复)。
你如今能够试着把元素增加这个集合,就像我们在之前关联person和event的那样。
Java里面的代码是同样的。
以下我们将映射一个双向关联(bi-directional association)- 在Java里面让person和event能够从关联的 不论什么一端訪问还有一端。当然,数据库表定义没有改变。我们仍然须要多对多(many-to-many)的阶数(multiplicity)。
一个关系型数据库要比网络编程语言 更加灵活,所以它并不须要不论什么像导航方向(navigation direction)的东西 - 数据能够用不论什么可能的方式进行查看和获取。
首先,把一个參与者(person)的集合增加Event类中:
private Set participants = new HashSet(); public Set getParticipants() { return participants; } public void setParticipants(Set participants) { this.participants = participants; }
在Event.hbm.xml里面也映射这个关联。
<set name="participants" table="PERSON_EVENT" inverse="true"> <key column="EVENT_ID"/> <many-to-many column="PERSON_ID" class="Person"/> </set>
如你所见,2个映射文件中都有通常的set映射。注意key和many-to-many 里面的字段名在两个映射文件中是交换的。
这里最重要的不同是Event映射文件中set元素的 inverse="true"參数。
这个表示Hibernate须要在两个实体间查找关联信息的时候,应该使用关联的另外一端 - Person类。 这将会极大的帮助你理解双向关联是怎样在我们的两个实体间创建的。
首先,请牢记在心,Hibernate并不影响通常的Java语义。 在单向关联中。我们是如何在一个Person和一个Event之间创建联系的? 我们把一个Event的实例加到一个Person类内的Event集合里。所以,显然假设我们要让这个关联能够双向工作, 我们须要在另外一端做相同的事情 - 把Person加到一个Event类内的Person集合中。
这“在关联的两端设置联系”是绝对必要的并且你永远不应该忘记做它。
很多开发人员通过创建管理关联的方法来保证正确的设置了关联的两端,比方在Person里:
protected Set getEvents() { return events; } protected void setEvents(Set events) { this.events = events; } public void addToEvent(Event event) { this.getEvents().add(event); event.getParticipants().add(this); } public void removeFromEvent(Event event) { this.getEvents().remove(event); event.getParticipants().remove(this); }
注意如今对于集合的get和set方法的訪问控制级别是protected - 这同意在位于同一个包(package)中的类以及继承自这个类的子类 能够訪问这些方法,可是禁止其他的直接外部訪问。避免了集合的内容出现混乱。你应该尽可能的在集合所相应的另外一端也这样做。
inverse映射參数到底表示什么呢?对于你和对于Java来说。一个双向关联不过在两端简单的设置引用。
然而只这样 Hibernate并没有足够的信息去正确的产生INSERT和UPDATE语句(以避免违反数据库约束), 所以Hibernate须要一些帮助来正确的处理双向关联。把关联的一端设置为inverse将告诉Hibernate忽略关联的 这一端,把这端看成是另外一端的一个镜子(mirror)。这就是Hibernate所需的信息。Hibernate用它来处理怎样把把 一个数据导航模型映射到关系数据库表定义。
你只须要记住以下这个直观的规则:全部的双向关联须要有一端被设置为inverse。
在一个一对多(one-to-many)关联中 它必须是代表多(many)的那端。而在多对多(many-to-many)关联中。你能够随意选取一端。两端之间并没有区别。
这个教程覆盖了关于开发一个简单的Hibernate应用程序的几个基础方面。
假设你已经对Hibernate感到自信。继续浏览开发指南里你感兴趣的内容-那些会被问到的问题大多是事务处理 (第 12 章 事务和并发), 抓取(fetch)的效率 (第 20 章 提升性能 ),或者API的使用 (第 11 章 与对象共事)和查询的特性(文章 11.4 节日 “查询”)。
不要忘了去Hibernate看到很多其他网站(有针对性)课程