Hibernate OGM 实战

Posted by    |       Hibernate OGM

本文是上文的延续.

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

本文即以kitchensink, 一个 JBoss Developer Framework 提供的实例为基础展开.

首先, 先让我介绍一下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), 那么, 我们接下来就是看看如何做很少的更改, 让这个实例使用insinispan做为存储替换掉数据源中使用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 来看看具体的效果.

另外, 如果你尝试输入一个不合法的名字, 电话号码或者email地址的话, 你会看到错误提示, 这就是Bean Validation所自动帮你提供的输入校验, 来看看代码, 还记得 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;

Okay, 现在你已经有了一个使用Hibernate OGM + NO-SQL的程序跑在 JBoss AS7上面了

另外, 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

要使用这个工具的话, 你首先需要安装(gem install)如下的gem:

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

然后就可以通过执行下面的命令来创建一些测试数据了.

ruby member-generator.rb -a http://localhost:8080/ogm-kitchensink -c 20

部属到 Openshift

openshift是Redhat所提供了一个Paas服务, 它背后的技术已经开源, 可以在这里找到其源代码和众多的实例项目.

openshift同时提供免费的服务和付费的服务, 对于我们简单的玩玩来讲, 免费的账户已经足够了, 但是如果你的项目是一个正式上线的项目, 还是推荐使用收费的带有支持的服务的.

openshift所支持的平台包括:

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

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

  • Mysql
  • PostgreSQL
  • Mongodb

例如我们可以在常见部属一个自己的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.


Back to top