2017年12月27日 星期三

Java Spring - Persisting data with object-relational mapping

  • 在persisting data中,JDBC就像自行車,對於份內的工作能夠很好地完成並且在一些特定場合表現出色;但隨著application越來越複雜,需求也變得複雜
    • 我們需要將object的property mapping到database上
    • 需要自動生成query, statement
    • 可以不用再寫問號
      • lazy loading: 允許在需要的時候獲得data
      • eager fetching: 與lazy loading相對。預先取得,可藉由一個操作將全部從db取出,節省多次查詢的成本
      • cascading: 關聯刪除。
    • 一些可用的framework提供這樣的服務。這些服務的通用名稱是object-relational mapping, ORM
    • 在persistent layer使用ORM,可以節省數千行的code和大量的開發時間
    • ORM可以把你的注意力從容易出錯的SQL轉向如何實現應用程序的真正需求
    • Spring對不同的ORM的support很類似。一旦掌握其中一種對ORM的support,可以很輕鬆的切換到另一種framework 
  • Hibernate
  • Declaring a Hibernate session factory
    • 使用Hibernate的主要interface是org.hibernate.Session
    • Session interface提供基本的data access功能,如save, update, delete, load object
    • 透過Session interface,application的Repository能夠滿足所有的persistence needs
    • 要得到一個object,結鈾Hibernate SessionFactory interface的implementation
      • 其他像Hibernate session open, close, manage與是在SessionFactory中負責
    • 在Spring中,要透過Spring的某一個Hibernate Session factory bean來獲取Hibernate SessionFactory,以下提供三個Session factory bean
      • org.springframework.orm.hibernate3.LocalSessionFactoryBean
      • org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean
      • org.springframework.orm.hibernate4.LocalSessionFactoryBean
    • 上述都是Spring FactoryBean interface的implementaion,會產生SessionFactory,能夠wire到任何SessionFactory的property中
    • 如果使用的是Hibernate3.2或更高,而且使用XML mapping,則需要定義Spring的org.springframework.orm.hibernate3的LocalSessionFactoryBean
    • @Bean
      public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
       LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
       sfb.setDataSource(dataSource);
       sfb.setMappingResources(new String[] {
        "Spitter.hbm.xml"
       });
       Properties props = new Properties();
       props.setProperty("dialect", "org.hibernate.dialect.H2Dialect");
       sfb.setHibernateProperties(props);
       return sfb;
      }
      
      • datasource: wired
      • mappingResources: 一個或多個Hibernate mapping file,定義了application的persistence strategy
      • HibernateProeprties: Hibernate的操作細節
    • 如果希望使用annotated方式來定義persistence strategy,而且還沒使用Hibernate4,則需要使用AnnotationSessionFactoryBean來代替LocalSessionFactoryBean
    • @Bean
      public AnnotationSessionFactoryBean sessionFactory(DataSource ds) {
        AnnotationSessionFactoryBean sfb = new AnnotationSessionFactoryBean();
        sfb.setDataSource(ds);
        sfb.setPackagesToScan(new String[] { "com.habuma.spittr.domain" });
        Properties props = new Properties();
        props.setProperty("dialect", "org.hibernate.dialect.H2Dialect");
        sfb.setHibernateProperties(props);
        return sfb;
      }
      
      • 使用packageToSacne告訴Spring scan一個或多個package找出scope class,這些class通常都通過annotation的方式表明要使用Hibernate進行persistence
      • 這些class可以使用的annotation包含JPA的@Entity或@MappedSuperclass以及Hibernate的@Entity
      • 也可以使用annotatedClass property將applicaiont中所有persistence class以完整名稱方式列出
      • sfb.setAnnotatedClasses(
                  new Class<?>[] { Spitter.class, Spittle.class }
        );// 可以準確的指定少量的scope class
        
    • 配置玩Hibernate的Session factory bean以後,就可以開始建自己的Repository class
  • Building Spring-free Hibernate
    • 在早期,寫Repository class會涉及到使用Spring的HibernateTemplate。HibernateTemplate可以保證每個transaction只使用一個Hibernate session,但缺點是Repository implementation會和Spring coupling 
    • 目前最佳實作方式是不再使用HibernateTemplate,而是使用contextual session。透過這個方式,會直接將Hibernate SessionFactory wired到Repository內,並使用之獲取Session
    • public HibernateSpitterRepository(SessionFactory sessionFactory) {
       this.sessionFactory = sessionFactory; // injects
      }
      private Session currentSession() {
       return sessionFactory.getCurrentSession();
      }
      public long count() {
       return findAll().size();
      }
      public Spitter save(Spitter spitter) {
       Serializable id = currentSession().save(spitter);
       return new Spitter((Long) id,
        spitter.getUsername(),
        spitter.getPassword(),
        spitter.getFullName(),
        spitter.getEmail(),
        spitter.isUpdateByEmail());
      }
      public Spitter findOne(long id) {
       return (Spitter) currentSession().get(Spitter.class, id);
      }
      public Spitter findByUsername(String username) {
       return (Spitter) currentSession()
        .createCriteria(Spitter.class)
        .add(Restrictions.eq("username", username))
        .list().get(0);
      }
      public List <Spitter> findAll() {
       return (List <Spitter> ) currentSession()
        .createCriteria(Spitter.class).list();
      }
      }
      
      • 使用@Injects讓Spring自動將sessionFactory injects到HibernateSpitterRepository
      • 在class上使用了@Repositry
        • @Repository是String的另一種stereotype annotation,能像其他annotation一樣被scan到,這樣就不用明確聲明HibernateSpitterRepository bean。也就是幫助減少顯示配置
        • template class有一個任務就是捕捉platform相關exception,再使用Spring統一unchecked exception的形式throw。如果使用的是Hibernate context session而不是Hibernate template,exception的轉換會如何處理?
          • 為了不使用template Hibernate Repository加入exception handling,只需要在Spring application context加上PersistenceExceptionTranslationPostProcessor bean
          • @Bean
            public BeanPostProcessor persistenceTranslation() {
              return new PersistenceExceptionTranslationPostProcessor();
            }
            
          • 此為一個bean post-processor,會在所有擁有@Repositroy annotation的class上添加一個advisor。這樣就會捕捉任何platform相關exception,並以Spring unchecked data access形式重新拋出
      • 這樣,Hibernate的Repository就完成了。開發時,沒有依賴Spring的特定class。
  • Spring and the Java Persistence API (JPA)(
    • JPA基於POJO persistence mechanism,從Hibernate和Java Data Object(JDO)上借鑑很多理念並加入Java5的annotation特性
    • 在Spring使用JPA的第一步是要在Spring application context中將entity manager factory按照bean的形式進行配置
  • Configuring an entity manager factory
    • 基於JPA的application需要使用EntityManagerFactory來得到EntityManager instance。JPA有兩種entity manager
      • Application-managed: 當application再向entity manager factory request entity manager時,factory會建立一個entry manager。這種方式適合不運行在JavaEE container中的獨立應用程式。取得的EntityManager是透過EntityManagerFactory建立。entity manager factory由LocalEntityManagerFactoryBean產生
      • Container-managed: entity manager factory由JavaEE建立及管理,application部會管理entity manager factory。instance manager直接透過injects JNDI獲取。適合用在Java EE container。取得的是透過PersistenceProvider 的createContainerEntityManagerFactory() method得到的。entity manager factory由LocalContainerEntityManagerFactoryBean產生
    • 以上兩種entity manager implement同一個EntityManager interface,區別在於EntityManager的create及manager方式
    • 值得注意的不相同處在於兩者在Spring application context中的配置
  • Configuring Application-managed  JPA
    • 對Application-managed entity factory來說,決大部分的配置來源是名叫persistence.xml的file。這個文件必須為在classpath下的META-INF下。
    • persistence.xml的作用在於定義一個或多個persistence unit。
    • persistence unit是同一個datasource下的一個或多個persistence class
    • <persistence xmlns="http://java.sun.com/xml/ns/persistence"
            version="1.0">
          <persistence-unit name="spitterPU">
            <class>com.habuma.spittr.domain.Spitter</class>
            <class>com.habuma.spittr.domain.Spittle</class>
            <properties>
              <property name="toplink.jdbc.driver"
                  value="org.hsqldb.jdbcDriver" />
              <property name="toplink.jdbc.url" value=
                  "jdbc:hsqldb:hsql://localhost/spitter/spitter" />
              <property name="toplink.jdbc.user"
                  value="sa" />
              <property name="toplink.jdbc.password"
                  value="" />
            </properties>
          </persistence-unit>
        </persistence>
      
    • 透過以下的@Bean在Spring中declare LocalEntityManagerFactoryBean
    • @Bean
      public LocalEntityManagerFactoryBean entityManagerFactoryBean() {
        LocalEntityManagerFactoryBean emfb
            = new LocalEntityManagerFactoryBean();
        emfb.setPersistenceUnitName("spitterPU"); // persistence-unit name
        return emfb;
      }
      
    • 在application manager的場景下,完全由application本身負責獲取EntityManagerFactory,這是透過JPA的PersistenceProvider做到的。如果每次eequest都要定義persistence-unit,則code會迅速膨脹。透過將其配置在persistence.xml,JPA就能夠在這個特定的位置找到persistence-unit的定義
    • 但若借助於Spring對JPA的support,就不需要再直接處理PersistenceProvider。因此,再將配置message放在persistence.xml就不太正確。事實上,這樣做妨礙在Spring中配置的EntityManagerFactory。因此可以關注Container-managed JPA
  • Configuring Container-managed  JPA
    • 當run在container時,可以使用container提供的information來生成EntityManagerFactory。
    • 可以將data source配置在Spring application context中,而不是在persistence.xml。
    • 如下,儘管datasource可以在persistence.xml進行配置,但這邊的property指定的datasource有更高的優先等級
    • @Bean
      public LocalContainerEntityManagerFactoryBean entityManagerFactory(
              DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
        LocalContainerEntityManagerFactoryBean emfb =
            new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setJpaVendorAdapter(jpaVendorAdapter); // 指名所使用的是哪一個factory的JPA implementation
        return emfb;
      }
      
    • 本例中的JPAVendorAdapter使用HibernateJPAVendorAdapter,因此
    • @Bean
      public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setDatabase("HSQL");
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.HSQLDialect");
        return adapter;
      }
      
    • database還支援:
      • DB2
      • DERBY
      • H2
      • HSQL
      • INFORMIX
      • MYSQL
      • ORACLE
      • POSTGRESQL
      • SQLSERVER
      • SYBASE
    • persistence.xml主要作用在於識別persistence-unit entity。但是Spring3.1開始,能夠在LocalContainerEntityManagerFactoryBean直接設定packgesToScan property
    • @Bean
      public LocalContainerEntityManagerFactoryBean entityManagerFactory(
              DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
        LocalContainerEntityManagerFactoryBean emfb =
            new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setJpaVendorAdapter(jpaVendorAdapter);
        emfb.setPackagesToScan("com.habuma.spittr.domain");
        return emfb;
      }
      
      • 如此會scan com.habuma.spittr.domain中的package,找出帶有@Entity的class。因此,沒有必要在persistence.xml declare
      • 同時,因為dataSource也是injects到LocalContainerEntityManagerFactoryBean,因此也沒有必要在persistence.xml再定義
      • 結論是:persistence.xml沒有存在的必要了,可以只讓LocalContainerEntityManagerFactoryBean完成這些事情
  • 從JNDI獲取entity manager factory
    • 如果將Spring application deploy在application server中,EntityManagerFactory可能已經建好,而且未在JNDI中等待查詢使用。
    • 這種情況下,可以使用Spring jee namespace的 <jee:jndi- lookup>來得到對EntityManagerFactory的引用
    • <jee:jndi-lookup id="emf" jndi-name="persistence/spitterPU" />
      
    • 也可以使用java configuration
    • @Bean
      public JndiObjectFactoryBean entityManagerFactory() {}
      JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
        jndiObjectFB.setJndiName("jdbc/SpittrDS");
        return jndiObjectFB;
      }
      
    • 雖然沒有return EntityManagerFactory,但結果就是一個EntityManagerFactory bean。因為return的jndiObjectFB其實就是FactoryBean的interface implementation,因此能夠建立EntityManagerFactory
    • 接下來就可以開始寫repository
  • JPA-based repository
    • 有鑒於 pure JPA勝於templated-based JPA,因此主要在於構建不依賴Spring的JPA repository
    • package com.habuma.spittr.persistence;
      import java.util.List;
      import javax.persistence.EntityManagerFactory;
      import javax.persistence.PersistenceUnit;
      import org.springframework.dao.DataAccessException;
      import org.springframework.stereotype.Repository;
      import org.springframework.transaction.annotation.Transactional;
      import com.habuma.spittr.domain.Spitter;
      import com.habuma.spittr.domain.Spittle;
      @Repository
      @Transactional
      public class JpaSpitterRepository implements SpitterRepository {
       @PersistenceUnit
       private EntityManagerFactory emf; // injects EntityManagerFactory
       public void addSpitter(Spitter spitter) {
        emf.createEntityManager().persist(spitter); //createEntityManager,並使用entity manager }
       public Spitter getSpitterById(long id) {
        return emf.createEntityManager().find(Spitter.class, id);
       }
       public void saveSpitter(Spitter spitter) {
        emf.createEntityManager().merge(spitter);
       }...
      }
      
      • @PersistenceUnit: Spring會將EntityManagerFactory注入到@Repository中
      • 有個問題是,每次都會呼叫emf.createEntityManager(),表示使用repository的method都會建立一個新的EntityManager。如果可以先準備好EntityManager,可能會更加方便
      • 因為EntityManager不是thread-safe,所以一般來說不適合injects到Repository這樣的singleton bean中。
      package com.habuma.spittr.persistence;
      import java.util.List;
      import javax.persistence.EntityManager;
      import javax.persistence.PersistenceContext;
      import org.springframework.dao.DataAccessException;
      import org.springframework.stereotype.Repository;
      import org.springframework.transaction.annotation.Transactional;
      import com.habuma.spittr.domain.Spitter;
      import com.habuma.spittr.domain.Spittle;
      @Repository
      @Transactional
      public class JpaSpitterRepository implements SpitterRepository {
      @PersistenceContext //會使用proxy
        private EntityManager em; //Inject EntityManager 
        public void addSpitter(Spitter spitter) {
          em.persist(spitter); // Use EntityManager
      }
        public Spitter getSpitterById(long id) {
          return em.find(Spitter.class, id);
      }
        public void saveSpitter(Spitter spitter) {
          em.merge(spitter);
      } ...
      }
      
      • 可能會擔心thread-safe的問題
      • 這邊的真相其實是,EntityManager注入的是一個proxy,真正的EntityManager是和目前關聯的那一個,如果不存在則會建立。如此就能使用thread-safe的方式使用entity manager
    • @PersistenceUnit&@PersistenceContext是由JPA規範的。使用這些annotation之前必須先配置Spring的PersistenceAnnotationBeanPostProcessor。如果已經使用了<context:annotation-config>和<context:component-scan>就不用擔心,因為會自動register PersistenceAnnotationBeanPostProcessor bean。否則需要直接配置
    • @Bean
      public PersistenceAnnotationBeanPostProcessor paPostProcessor() {
        return new PersistenceAnnotationBeanPostProcessor();
      }
      
    • @Transactional: 表示這個Repository中的persistence method是在transactional context中執行
    • @Repository: 作用和開發Hibernate context session的Repository是醫治的。由於沒有使用template class處理exception,所以需要加上@Repository,這樣PersistenceExceptionTranslationPostProcessor就知道要將這個bean產生的exception轉換成Spring的統一data access exception
    • @Bean
      public BeanPostProcessor persistenceTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
      }
      
      • 對於JPA或是Hibernate,persistenceTranslation都不是強求的。
  • Automatic JPA repositories with Spring Data
    • 看一下addSpitter()
    • public void addSpitter(Spitter spitter) {
          entityManager.persist(spitter);
      }
      
    • 事實上,除了persistence 的Spitter object不同以外,其他都一樣。所有repository的method都是common。
    • Spring Data JPA終結這種bolierplate,不需要一遍遍的寫相同的Repository implementation。Spring Data能夠只寫Repository interface,不需要implementation class
    • 再看一下SpitterRepository interface
    • public interface SpitterRepository 
      extends JpaRepository<Spitter, Long>{
      }
      • 編寫Spring Data JPA Repository的關鍵在於從interface挑選一個extends
      • 這裡,SpitterRepository extends Spring Data JPA的JpaRepository
      • 通過這種方式,JpaRepository進行了parameterized,所以他能夠知道這是一個用來persisting Spitter object的repository
      • 且Spitter的ID是ling,且還extends 18個執行persisting操作的common method(CRUD..)
      • 使用Spring Data來做這些事情。做的方法就是提出request
    • 為了要求Spring Data create SpitterRepository的implementation,需要在Spring配置中加一個element
    • XML啟用Spring Data JPA所需要加的內容:
    • <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:jpa="http://www.springframework.org/schema/data/jpa"
        xsi:schemaLocation="http://www.springframework.org/schema/data/jpa
      http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd">
          <jpa:repositories base-package="com.habuma.spittr.db" />
        ...
      </beans>
      
      • <jpa:repositories>就像是<context:component-scan>,需要指定進行掃描的base-package。不過<context:component-scan>會掃描package來找帶有@Component的class,<jpa:repositories>會掃描base-pacakge找出extends Spring Data JPA repository interface的所有interface。如果發現了extends Repository的interface,會自動產生這個interface的implementation 
    • Java config
    • @Configuration
      @EnableJpaRepositories(basePackages="com.habuma.spittr.db")
      public class JpaConfiguration {
      ... }
      
    • 當Spring Data找到SpitterRepository後,就會create SpitterRepository的implementation class。其中包含extends JpaRepository, PagingAndSortingRepository, and CrudRepository等18個method。注意這個implementation class是在application start up的時候generated也就是Spring application context created的時候建立的。並不是在build-time時code generation,也不是在 interface’s methods are called產生的
    • Spring Data JOPA很棒的一點在於它能夠替Spitter提供18個便利的method,無需寫任何的persistence code。如果需求超過,有幾種方式添加自訂義method
  • 定義customer query method
    • public interface SpitterRepository
             extends JpaRepository<Spitter, Long> {
          Spitter findByUsername(String username);
        }
      
    • 本質上,Spring Data定義了一組小心domain-specific language DSL,在這裡,persistence detail都是透過repository method的signature來描述的
    • Spring data知道這個方分是要找到Spitter,method名稱findByUsername()也就是需要根據username property找到Spitter,而username由parameter傳到method中。又因為findByUsername需要return一個Spitter object,而不是collection,因此只會找一個username匹配的Spitter object
    • SpringData判斷method的方法,如findByUsername匹配到下面
      • verb(Spring Data允許(get, read, find)這三個都是query, count(return 數目)): find
      • subject(是由interface參數化決定的,如果是以Distinct為開頭,則會返回不重複者): Spitter(implied)
      • By
      • predicate(會有一個或多個限制結果的條件):Username
        •  IsAfter, After, IsGreaterThan, GreaterThan  IsGreaterThanEqual, GreaterThanEqual
        •  IsBefore, Before, IsLessThan, LessThan
        •  IsLessThanEqual, LessThanEqual
        •  IsBetween, Between  IsNull, Null
        •  IsNotNull, NotNull  IsIn, In
        •  IsNotIn, NotIn
        •  IsStartingWith, StartingWith, StartsWith  IsEndingWith, EndingWith, EndsWith
        •  IsContaining, Containing, Contains
        •  IsLike, Like
        •  IsNotLike, NotLike
        •  IsTrue, True
        •  IsFalse, False
        •  Is, Equals
        •  IsNot, Not
    • 又,readSpitterByFirstnameOrLastname()也可以匹配
      • verb: read
      • subject: Spitter
      • By
      • predicate:FirstnameOrLastname
    • 如果要在firstname及lastname忽略大小寫
    • List<Spitter> readByFirstnameIgnoringCaseOrLastnameIgnoresCase(
                          String first, String last);
      
    • 可使用排序
    • List<Spitter> readByFirstnameOrLastnameOrderByLastnameAscFirstnameDesc( String first, String last);
      
    • 其他範例
    • List<Pet> findPetsByBreedIn(List<String> breed)
      int countProductsByDiscontinuedTrue()
      List<Order> findByShippingDateBetween(Date start, Date end)
      
    • 問題:
      • 有時候通過method name表達很煩瑣,甚至無法實現
    • 解法:
      • 使用@Query
  • Declaring custom queries
    • 假設想要找E-mail是G-mail的Spitter,有個方式是定義findByEmailLike(),傳入"%gmail.com"
    • 更好的方法是定義findAllGmailSpitters() method
    • List<Spitter> findAllGmailSpitters();
      
    • 因為SpinrData無法實現,所以會拋出錯誤。方法就是加入@Query,為Spring Data執行query。
    • @Query("select s from Spitter s where s.email like '%gmail.com'")
      List<Spitter> findAllGmailSpitters();
      
      • 注意,依然不需要寫method的implementation 
    • 當使用命名約定特別長的時候,就可以使用這個annotation
    • 問題:
      • 限於單一個JPA query,如果需要更複雜功能,無法在一個簡單查詢中處理怎麼辦
    • 解法:
      • 混合自定義功能
  • Mixing in custom functionality
    • 直接使用EntityManager
    • 如果需要做的事情無法透過Spring Data JPA,則必須在一個比Spring Data JPA更低的層級上使用JPA。
    • 當Spring Data JPA為repository interface生成implementation 的實好,會找名字和interface相同,並且加上Impl suffix 的class,如果存在,就會將他的method和Spring Data JPA生成的method合併再一起
    • 對于SpitterRepository,要找的class就是SpitterRepositoryImpl
    • 假設需要在SpitterRepository加一個method,找到Spitters發表1000則訊息以上,設定為Elite status,且@Query及約定的方法命名都無法使用時:
    • public class SpitterRepositoryImpl implements SpitterSweeper {
        @PersistenceContext
        private EntityManager em;
        public int eliteSweep() {
          String update =
              "UPDATE Spitter spitter " +
              "SET spitter.status = 'Elite' " +
              "WHERE spitter.status = 'Newbie' " +
              "AND spitter.id IN (" +
              "SELECT s FROM Spitter s WHERE (" +
              "  SELECT COUNT(spittles) FROM s.spittles spittles) > 10000" +
              ")";
          return em.createQuery(update).executeUpdate();
        }
      }
      
      • 注意:沒有implement SpitterRepository
    • SpitterSweeper
    • public interface SpitterSweeper{
          int eliteSweep();
      }
      
    • 接著讓他SpitterRepository extends SpitterSweeper
    • public interface SpitterRepository
             extends JpaRepository<Spitter, Long>,
                     SpitterSweeper {
      .... }
      
    • 後綴加上Imp是默認做法,如果要其他的suffix,則在配置@EnableJpaRepositories時加上設定
    • @EnableJpaRepositories(
                basePackages="com.habuma.spittr.db",
                repositoryImplementationPostfix="Helper")
      
    • XML:
    •  <jpa:repositories base-package="com.habuma.spittr.db"
                  repository-impl-postfix="Helper" />
      
    • 如此就能將suffix設定為Helper,Spring Data JPA會找名叫SpitterRepositoryHelper的class,並將他匹配到SpitterRepository interface

沒有留言:

張貼留言