Hibernate OGM 实战

发布者    |       Hibernate OGM

Hibernate OGM 已不再维护

本文是上文的延续.

我想先介绍一下JBoss 开发者框架,这是JBoss 社区推出的一个全新的项目,旨在帮助开发者更好的理解和使用JBoss的相关技术,它提供了大量的实例教程(50+),视频,文章的内容,迁移向导(从Java EE 5到EE 6,从Spring到Java EE)等,教你一点点的学会Java EE 6相关的各种技术,并且涵盖了 RESTHTML5 等新的热点技术.

本文即以kitchensink,一个 JBoss 开发者框架 提供的实例为基础展开.

首先,先让我介绍一下kitchensink吧,最新的源代码可以在这里找到.

这个实例主要演示了如下几种技术

  • Bean Validation 1.0
  • EJB 3.1
  • JAX-RS
  • JPA 2.0
  • JSF 2.0
  • CDI 1.0
  • Arquillian

想要运行这个实例的话,你需要 JDK 6/7,Maven 3 和 JBoss AS 7

注意,有一些依赖是只存在于JBoss 的maven 仓库中的,所以,可能需要对maven 的settings.xml文件做些配置,添加JBoss maven仓库,具体请参考这里这里

具体配置信息就不多说了,上面给出的链接很详细,这些也不是本文的重点,接下来就让我们看看代码

首先是persistence.xml,位于main/resources/META-INF/persistence.xml,都是标准的位置

    <persistence version="2.0"
       xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://java.sun.com/xml/ns/persistence
            http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
       <persistence-unit name="primary">
          <jta-data-source>java:jboss/datasources/KitchensinkQuickstartDS</jta-data-source>
          <properties>
             <property name="hibernate.hbm2ddl.auto" value="create-drop" />
             <property name="hibernate.show_sql" value="false" />
          </properties>
       </persistence-unit>
    </persistence>

可以看到,这个文件定义的很简单,就是定义了一个数据源和两个属性,注意,hibernate.hbm2ddl.auto=creat-drop,意思是在创建session factory的时候自动创建表结构,关闭session factory的时候会自动drop掉表.

同时,还可以看到main/resources目录中有import.sql这个文件,当使用hibernate创建表结构的时候,创建完成之后,hibernate会自动的导入import.sql文件,这样就可以添加一些初始数据.

另外,引用的数据源是定义在main/webapp/WEB-INF/kitchensink-quickstart-ds.xml文件中的.

然后再来看看本文关注的另外一个方面,实体定义

    @Entity
    @XmlRootElement
    @Table(uniqueConstraints = @UniqueConstraint(columnNames = "email"))
    public class Member implements Serializable {
       /** Default value included to remove warning. Remove or modify at will. **/
       private static final long serialVersionUID = 1L;
    
       @Id
       @GeneratedValue
       private Long id;
    
       @NotNull
       @Size(min = 1, max = 25)
       @Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces")
       private String name;
    
       @NotNull
       @NotEmpty
       @Email
       private String email;
    
       @NotNull
       @Size(min = 10, max = 12)
       @Digits(fraction = 0, integer = 12)
       @Column(name = "phone_number")
       private String phoneNumber;    
       // getters / setters
    } 

很简单的一个entity mapping,需要注意的是javax.xml.bind.annotation.XmlRootElement是JAXB里面的一个annotation,在这里可以把这个实体对象转化成xml表示

还有就是这个entity里面定义了一些BV的annotation,具体可以参考Hibernate Validator的文档.

Okay,本实例中用到的其它技术暂时不做介绍了,下面终于该进入正题了

上面介绍过了,这个实例使用的是JBoss AS7中的数据源(java:jboss/datasources/KitchensinkQuickstartDS), 那么,我们接下来就是看看如何做很少的更改,让这个实例使用Infinispan作为存储替换掉数据源中使用H2

首先,是添加依赖,因为我们这里是想要通过Hibernate OGM,把实体对象保存进Infinispan当中,所以只需要加入下面的依赖项即可

        <dependency>
            <groupId>org.hibernate.ogm</groupId>
            <artifactId>hibernate-ogm-core</artifactId>
            <version>4.0.0-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>        
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.6</version>
        </dependency>
        <dependency>
            <artifactId>infinispan-core</artifactId>
            <groupId>org.infinispan</groupId>
            <version>5.1.5.FINAL</version>
            <scope>provided</scope>
        </dependency>

接下来就是修改persistence.xml了,上文曾经提到过,Hibernate OGM本身也是一个JPA的实现,但是由于JBoss AS7默认集成的是Hibernate ORM 4,所以我们需要在persistence.xml中显式地声明我们希望使用Hibernate OGM作为JPA的实现。

     <persistence version="2.0"
       xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://java.sun.com/xml/ns/persistence
            http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
       <persistence-unit name="primary">
           <provider>org.hibernate.ogm.jpa.HibernateOgmPersistence</provider>
           <properties>
               <property name="hibernate.ogm.datastore.provider"
                         value="org.hibernate.ogm.datastore.infinispan.impl.InfinispanDatastoreProvider"/>
               <property name="hibernate.ogm.infinispan.configuration_resourcename" value="infinispan-ogm-config.xml"/>
           </properties>
           <class>org.jboss.as.quickstarts.kitchensink.model.Member</class>
       </persistence-unit>
    </persistence>

在这个更新过的文件中我们可以看到如下的变化

  • 使用org.hibernate.ogm.jpa.HibernateOgmPersistence作为JPA的实现
  • 去掉了datasource的引用,Hibernate OGM不使用RMDBS
  • 通过hibernate.ogm.datastore.provider指定使用infinispan作为data store
  • 通过hibernate.ogm.infinispan.configuration_resourcename属性指定infinispan的配置文件

src/main/webapp/WEB-INF/kitchensink-quickstart-ds.xml这个文件已经没有用了,可以删掉。

至此,配置方面就需要这么多的改动,很简单吧

现在,我们已经通过使用Hibernate OGM,把底层的存储从RMDBS切换成了Infinispan,但是,由于RMDBS和NO-SQL本质的不同,我们还需要做一些修改。

使用uuid作为主键

在使用Hibernate ORM的时候,我们通常会使用@GeneratedValue来得到数据库自动生成的id,并且,通常我们建议把id设置成long类型的以得到更好的性能。

可是,如果使用的是NO-SQL的话,如果是K-V类型的NO-SQL的话,他们是没有一个主键的概念的 (mongodb等文档型数据库是会提供自动生成的id的),所以为了统一,并且保证全局唯一,在Hibernate OGM中我们建议使用UUID作为主键生成策略,并且,在Hibernate ORM中早已提供了此种策略,我们在这里可以直接使用。

org.jboss.as.quickstarts.kitchensink.model.Member#id需要修改成如下的样子。

   @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

注意,改变了id的类型之后,我们还需要修改org.jboss.as.quickstarts.kitchensink.rest.MemberResourceRESTService#lookupMemberById

        @GET
    	@Path("/{id:[0-9][0-9,\\-]*}")
        @Produces(MediaType.APPLICATION_JSON)
        public Member lookupMemberById(@PathParam("id") String id) {
            Member member = repository.findById(id);
            if (member == null) {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
            return member;
        }

这个方法提供了一个通过REST接口来查找Member的功能,但是因为我们已经把id的类型改成了String,所以我们需要对@Path做一些修改,让它能够接受字符 --@Path("/{id:[0-9,a-z][0-9,a-z,\\-]*}")

查询

因为Hibernate OGM还是一个很年轻的项目,有一些功能还没有完全的实现,例如,我们在JPA/Hibernate中经常使用的Criteria查询,但是幸好,Hibernate OGM和Hibernate Search有很好的集成,我们可以使用Hibernate Search来完成这部分工作。

org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findByEmail方法是通过email来查询一个Member,内部实现是通过Criteria来查询的。

org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findAllOrderedByName方法是查询所有的Member并且按照name排序,同样是使用的Criteria。

这两个方法的实现很简单,相信大家都能看懂。

那么,我们现在就需要使用Hibernate Search替换掉这两个方法。

首先,把Hibernate Search相关的依赖添加进pom当中

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>4.2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-engine</artifactId>
            <version>4.2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-analyzers</artifactId>
            <version>4.2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-infinispan</artifactId>
            <version>4.2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-lucene-directory</artifactId>
            <version>5.1.5.FINAL</version>
        </dependency>

这里所依赖的org.hibernate:hibernate-search-infinispanorg.infinispan:infinispan-lucene-directory提供了一个Lucene的Directory的实现,可以把Lucene的index保存在infinispan当中,从而实现比保存在文件系统当中更好的性能和可扩展性(因为Infinispan是一个分布式的数据网格系统)。

在persistence.xml当中,我们需要添加上两个Hibernate Search的属性 这里所依赖的org.hibernate:hibernate-search-infinispanorg.infinispan:infinispan-lucene-directory提供了一个Lucene的Directory的实现,可以把Lucene的index保存在infinispan当中,从而实现比保存在文件系统当中更好的性能和可扩展性(因为Infinispan是一个分布式的数据网格系统)。

在persistence.xml当中,我们需要添加上两个Hibernate Search的属性

<property name="hibernate.search.default.directory_provider" value="infinispan"/>
    <property name="hibernate.search.infinispan.configuration_resourcename" value="infinispan.xml"/>
第一个hibernate.search.default.directory_provider告诉Hibernate Search使用Infinispan作为Lucene Index Directory,第二个指定了Hibernate Search所使用的Infinispan的配置文件。

接下来,我们需要对org.jboss.as.quickstarts.kitchensink.model.Member做一些改动,添加上Hibernate Search所需要的Annotation.

   @Entity
    @XmlRootElement
    @Indexed
    @Table(uniqueConstraints = @UniqueConstraint(columnNames = "email"))
    @Proxy(lazy = false)
    public class Member implements Serializable {
       /** Default value included to remove warning. Remove or modify at will. **/
       private static final long serialVersionUID = 1L;
    
       @Id
       @GeneratedValue(generator = "uuid")
       @GenericGenerator(name = "uuid", strategy = "uuid2")
       private String id;
    
       @NotNull
       @Size(min = 1, max = 25)
       @Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces")
       @Fields({
    		   @Field(analyze = Analyze.NO, norms = Norms.NO, store = Store.YES, name = "sortableStoredName"),
    		   @Field(analyze = Analyze.YES, norms = Norms.YES)
       })
       private String name;
    
       @NotNull
       @NotEmpty
       @Email
       @Field(analyze = Analyze.NO)
       private String email;
    
       @NotNull
       @Size(min = 10, max = 12)
       @Digits(fraction = 0, integer = 12)
       @Column(name = "phone_number")
       @Field(analyze = Analyze.NO)
       private String phoneNumber;

第一个是@Indexed,它告诉Hibernate Search,这个实体类是需要被索引的,还有@Field (在name和phoneNumber属性上)告诉Hibernate Search这两个属性是需要被索引的,具体信息请参考Hibernate Search 文档

想要在保存一个实体对象的时候,让Hibernate Search自动索引的话,我们需要使用org.hibernate.search.jpa.FullTextEntityManager来替换javax.persistence.EntityManager.

org.jboss.as.quickstarts.kitchensink.service.MemberRegistration#register这个方法调用了EntityManager#persist,那么我们需要做的就是把这个类中的@Inject private EntityManager em;换成@Inject private FullTextEntityManager em;这里,之前的EntityManager和现在的FullTextEntityManager都是由CDI负责自动注入的,可是,CDI并不知道如何创建Hibernate Search所特有的FullTextEntityManager,所以,为了让自动注入工作,我们需要一个Producer。

而这个Producer,在我们的实例当中就是org.jboss.as.quickstarts.kitchensink.util.Resources,它已经存在了,用来提供EntityManager的注入和Logger的注入(org.jboss.as.quickstarts.kitchensink.util.Resources#produceLog)

我们只需要添加Hibernate Search的内容 (CDI相关内容可以参考Weld文档)

	@Produces
	public FullTextEntityManager getFullTextEntityManager() {
		return Search.getFullTextEntityManager( em );
	}

	@Produces
	@ApplicationScoped
	public SearchFactory getSearchFactory() {
		return getFullTextEntityManager().getSearchFactory();
	}

	@Produces
	@ApplicationScoped
	public QueryBuilder getMemberQueryBuilder() {
		return getSearchFactory().buildQueryBuilder().forEntity( Member.class ).get();
	}

Okay,现在万事俱备,我们可以使用Hibernate Search来替换org.jboss.as.quickstarts.kitchensink.data.MemberRepository中那两个使用了Criteria的方法了

注意,org.jboss.as.quickstarts.kitchensink.data.CriteriaMemberRepository#findById是需要把参数的类型从long改成String的,除此之外,Hibernate OGM是支持直接使用id进行查询的,所以不需要修改。

org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findByEmail方法是通过email来查询一个Member,内部实现是通过Criteria来查询的。

org.jboss.as.quickstarts.kitchensink.data.MemberRepository#findAllOrderedByName方法是查询所有的Member并且按照name排序,同样是使用的Criteria。

@Inject
	private QueryBuilder queryBuilder;

	@Inject
	private FullTextEntityManager em;

	@Override
	public Member findById(String id) {
		return em.find( Member.class, id );
	}

	@Override
	public Member findByEmail(String email) {
		Query luceneQuery = queryBuilder
				.keyword()
				.onField( "email" )
				.matching( email )
				.createQuery();
		List resultList = em.createFullTextQuery( luceneQuery )
				.initializeObjectsWith( ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID )
				.getResultList();
		if ( resultList.size() > 0 ) {
			return (Member) resultList.get( 0 );
		}
		else {
			return null;
		}
	}

	@Override
	public List<Member> findAllOrderedByName() {
		Query luceneQuery = queryBuilder
				.all()
				.createQuery();
		List resultList = em.createFullTextQuery( luceneQuery )
				.initializeObjectsWith( ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID )
				.setSort( new Sort( new SortField( "sortableStoredName", SortField.STRING_VAL ) ) )
				.getResultList();
		return resultList;
	}

可以看到,在这个新的类中,我们首先使用CDI自动注入了Hibernate Search的QueryBuilder和FullTextEntityManager (参见上面修改后的org.jboss.as.quickstarts.kitchensink.util.Resources工厂类)

在findById方法中,FullTextEntityManager#find实际上是代理给Hibernate OGM来处理的。

而在其余两个方法中,则是完全使用Hibernate Search的Query API创建了查询条件,然后交过Lucene来搜索的,还记得我们上面修改了Member类,在它被保存的时候创建Lucene索引的吧 (另,上面提到过,这个索引也是保存在infinispan当中的)

部署到JBoss AS 7

方便的是,我的同事Hardy已经准备好了这样一个修改过的项目,你可以直接从前面的链接中下载到本文中所介绍到的项目的源代码。

这是一个maven项目,所以你需要先安装好maven,Hardy还很贴心的在pom里面使用了cargo插件,所以你不需要下载JBoss AS 7了(尽管我还是推荐你下载一个看看,很值得的),cargo会自动帮你下载,并且完成部署等事情。

  • 编译$ mvn clean package
  • 运行$ mvn cargo:run
  • 测试(基于Arquillian)$mvn test

程序运行后,您可以访问http://127.0.0.1:8080/ogm-kitchensink来查看具体效果。

此外,如果您尝试输入一个不合法的名字、电话号码或电子邮件地址,您将看到错误提示。这就是Bean Validation自动为您提供的输入校验。看看代码,还记得类中属性上定义的Bean Validation Annotations吗?(下面代码中我只保留了这部分注解)org.jboss.as.quickstarts.kitchensink.model.Member类中的属性上定义的Bean Validation Annotations吗?(下面代码中我只保留了这部分的annotation)

        @NotNull
	@Size(min = 1, max = 50)
	@Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces")
	private String name;
	
	@NotNull
	@NotEmpty
	@Email
	private String email;

        @NotNull
	@Size(min = 10, max = 12)
	@Digits(fraction = 0, integer = 12)
	private String phoneNumber;

好的,现在您已经在JBoss AS7上运行了一个使用Hibernate OGM + NO-SQL的程序了。

另外,JBoss AS 7的启动速度非常快。

23:27:10,050 INFO  [org.jboss.as] (Controller Boot Thread) JBAS015874: 
JBoss AS 7.1.1.Final "Brontes" started in 2009ms - Started 133 of 208 services (74 services are passive or on-demand)

上面的日志是在我的机器上启动的速度,应该和Tomcat也差不多吧,但JBoss AS7是一个完整的通过Java EE6认证的应用服务器,而Tomcat只是一个servlet container。

Hardy还非常贴心的提供了一个小工具来帮助我们自动插入数据,这个工具就在项目根目录下,名为member-generator.rb。member-generator.rb

要使用这个工具,您首先需要安装(gem install)以下gem:

  • gem install httparty
  • gem install nokogiri
  • gem install choice

然后,您可以通过执行以下命令来创建一些测试数据。

ruby member-generator.rb -a https://#:8080/ogm-kitchensink -c 20

部署到OpenShift

OpenShift是Redhat提供的一个PaaS服务,其背后的技术已经开源,可以在这里找到源代码和众多的实例项目。

OpenShift同时提供免费的服务和付费的服务。对于我们简单的试玩来说,免费的账户已经足够了。但如果是正式上线项目,还是推荐使用带支持的付费服务。

OpenShift支持的平台包括

  • Java
  • Ruby
  • Node.js
  • Python
  • PHP
  • Perl

并且它还提供了开箱即用的数据库支持,包括RMDBS和NO-SQL

  • Mysql
  • PostgreSQL
  • Mongodb

例如,我们可以在OpenShift上轻松部署一个自己的WordPress或Drupal程序。

对于我们Java程序员来说,OpenShift最重要的是它内置了JBoss AS7(及其商业版本JBoss EAP6)的支持。因此,对于想要尝试Java EE6的开发者来说,这是一个非常好的消息,您可以直接使用这个服务。

首先,第一步没有别的,先到这里创建一个免费的账户。

然后从这里下载客户端,Linux / Mac / Windows都有对应的客户端供下载。

接下来,您需要创建一个domain,命令为rhc domain create -n ${my domain name},之后,您所有部署到OpenShift上的应用都会是http://${app name}-${domain name}.rhcloud.com的格式(免费账户可以创建3个应用程序,并且您可以使用自己的域名)。

然后创建应用,我们想要把这个程序部署到JBoss AS7上,使用以下命令rhc-create-app -a ${your app name} -t jbossas-7 --nogit,命令完成后,您会从输出中看到有一个git repo的地址。现在,您可以把这个地址作为一个remote添加到之前clone出来的ogm-kitchensink项目当中。

git remote add openshift ${repo-url}

然后,把ogm-kitchensink 推送到OpenShift提供的git repo当中。

git push -f openshift master

最后,您就可以访问您的应用了,地址如同之前所说的,是

http://${your app name}-${your domain name}.rhcloud.com

我创建了一个demo,可以访问http://ogm-stliu.rhcloud.com


回到顶部