Spring Data JPA

  • 配置相關bean
    • 數據源
      <context:property-placeholder ignore-resource-not-found="true"  location="classpath:jdbc-${spring.profiles.active}.properties"/>  
      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"  init-method="init"   destroy-method="close">
          <!-- 基本屬性 url、user、password -->
          <property name="url" value="${ds.url}"/>
          <property name="username" value="${ds.username}"/>
          <property name="password" value="${ds.password}"/>
          <!-- 配置初始化大小、最小、最大 -->
          <property name="initialSize" value="5"/>
          <property name="minIdle" value="5"/>
          <property name="maxActive" value="50"/>
          <!-- 配置獲取鏈接等待超時的時間 -->
          <property name="maxWait" value="60000"/>
          <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
          <property name="timeBetweenEvictionRunsMillis" value="60000"/>
          <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
          <property name="minEvictableIdleTimeMillis" value="300000"/>
          <property name="validationQuery" value="SELECT 'x'"/>
          <property name="testWhileIdle" value="true"/>
          <property name="testOnBorrow" value="false"/>
          <property name="testOnReturn" value="false"/>
          <!-- 打開removeAbandoned功能 -->
          <property name="removeAbandoned" value="true"/>
          <property name="removeAbandonedTimeout" value="1800"/>
           <property name = "logAbandoned" value = "true"/>
           <!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小,mysql 不使用 -->
          <property name="poolPreparedStatements" value="false"/>
          <!-- 配置監控統計攔截的filters -->
          <property name="filters" value="stat"/>
          <!-- 慢查詢sql打印 -->
          <property name="connectionProperties" value="druid.stat.slowSqlMillis=200;druid.stat.logSlowSql=true"/>
      </bean>  
    • 實體掃描並生成Repository
      <bean id="hibernateJpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
          <property name="databasePlatform" value="org.hibernate.dialect.MySQL5Dialect"/>
      </bean> 
      <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
          <property name="dataSource" ref="dataSource"/>
          <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter"/>
          <property name="packagesToScan" value="com.qingbo.sapling"/>
          <property name="jpaProperties">
              <props>
                  <!-- 命名規則 My_NAME->MyName 自動更新數據庫表結構(僅適用於開發階段,正式運行後最好是手動維護數據庫表結構) -->
                  <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
                  <prop key="hibernate.hbm2ddl.auto">update</prop>
                  <prop key="hibernate.show_sql">true</prop>
                  <prop key="hibernate.format_sql">true</prop>
              </props>
          </property>
      </bean> 
      <!-- 自動尋找Repository接口並提供實現類以支持@Autowired注入,包名支持通配符,repository-impl-postfix="impl" -->
      <jpa:repositories base-package="com.qingbo.sapling" />       
    • 事務,校驗,測試時須要加事務註解@Transactional,網站運行須要配置OpenEntityManagerInViewFilter 
      <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
          <property name="entityManagerFactory" ref="entityManagerFactory"/>
      </bean>  
      <!-- proxy-target-class爲true時@Transactional須要註解實現類的方法,而false則使用java的基於接口的代理 -->

      <tx:annotation-driven  proxy-target-class="true"/>
       
      <!-- 自動搜索providerClass,也可指定org.hibernate.validator.HibernateValidator ,需提供資源文件classpath:ValidationMessages.properties  -->
      <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>  
    • maven插件任務配置pom.xml,(這個是可選的,會自動生成不少類,可用於支持 QueryDslPredicateExecutor 查詢接口 )
      <!--querydsl-->
      <plugin>
          <groupId>com.mysema.maven</groupId>
          <artifactId>apt-maven-plugin</artifactId>
          <version>1.1.0</version>
          <configuration>
              <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
          </configuration>
          <dependencies>
              <dependency>
                  <groupId>com.mysema.querydsl</groupId>
                  <artifactId>querydsl-apt</artifactId>
                  <version>3.3.1</version>
              </dependency>
          </dependencies>
          <executions>
              <execution>
                  <phase>generate-sources</phase>
                  <goals>
                      <goal>process</goal>
                  </goals>
                  <configuration>
                      <outputDirectory>target/generated-sources/annotations</outputDirectory>
                  </configuration>
              </execution>
          </executions>
      </plugin>  
    • 網站配置web.xml
      <filter>  
            <filter-name>sessionFilter</filter-name>  
            <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>  
            <init-param>  
                <param-name>singleSession</param-name>  
                <param-value>false</param-value>  
            </init-param>
      </filter>  
      <filter-mapping>  
            <filter-name>sessionFilter</filter-name>  
            <url-pattern>*.html</url-pattern>  
      </filter-mapping>  
  • dao持久層接口Repository,數據庫表實體類也在這一層
    • 核心查詢接口:能夠實現如下接口,也能夠註解@RepositoryDefinition編寫本身的接口
      CrudRepository,提供基於鍵值ID和實體Entity的增Create刪Delete改Update查Retrieve,T findOne(ID id);
      PagingAndSortingRepository,提供分頁和排序功能,Page<T> findAll(Pageable pageable)
      JpaRepository,增長批量操做和刷新輸出功能,void flush();
      JpaSpecificationExecutor,複雜查詢接口(持久層查詢分頁),Page<T> findAll(Specification<T> spec, Pageable pageable);
      QueryDslPredicateExecutor ,querydsl查詢接口支持,
      使用樣例:
      interface UserRepository extends PagingAndSortingRepository<User, Long> //普通分頁排序,能夠添加本身的findBy命名查詢接口
      , JpaSpecificationExecutor<User>  //支持Criteria複查查詢
      , QueryDslPredicateExecutor<User>   //支持querydsl方式查詢
    • 簡單查詢方法命名規範,如下方法只需定義接口無需本身寫實現代碼,
      支持方法前綴findBy+find+readBy+read+getBy+get,
      支持屬性名及AndOr拼接,
      支持屬性操做Between+LessThan+GreaterThan+IsNull+[Is]NotNull+Like+NotLike+OrderBy+Desc+Not+In+NotIn(支持集合數組或可變參數),
      支持屬性嵌套findByAddressZipCode(ZipCode)可查詢address.zipCode(支持findByAddress_ZipCode以避免歧義addressCode.zip),

      其餘判斷詞還有:findByStartDateAfter,findByEndDateBefore,findByNameStartingWith,findByAgeOrderByName,findByActiveTrue
      支持排序和分頁:Page<User> findByName(String name, Pageable pageable);//也支持Sort sort參數
      List<User> findByName(String, Sort sort);//支持返回List結果,避免構建Page而進行其餘查詢(count計算總數等),同時須要再定義count接口
      支持擴展自定義接口:extends MyInterfaceCustom, Repository,而後實現類MyInterfaceCustomImpl,注入@PersistenceContext EntityManager em; 自定義接口方法查庫
      支持自定義全局接口:MyRepository extends JpaRepository,MyRepositoryImpl extends SimpleJpaRepository implements MyRepository,而後定義MyRepositoryFactoryBean extends JpaRepositoryFactoryBean提供MyRepositoryFactory extends JpaRepositoryFactory,在getTargetRepository方法裏返回new MyRepositoryImpl,最後在repositories配置factory-class便可
    • 命名查詢,這些最好少用,代碼裏面最好不要有sql類語句
      • 配置文件META-INF/orm.xml
        <named-query name="User.findByLastname">
            <query>select u from User u where u.lastname = ?1</query>
        </named-query> 
      • 註解實體類查詢
        @Entity  @ NamedQuery(name = "User.findByEmailAddress", query = "select u from User u where u.emailAddress = ?1")
        public class User {
        }  
        public interface UserRepository extends JpaRepository<User, Long> {
            List<User> findByLastname(String lastname); //spring會優先查找NamedQuery,其次分析方法名
        }  
      • 註解接口方法,支持簡單統計sql查詢語句(複雜條件統計還得研究JPA查詢條件)
        public interface UserRepository extends JpaRepository<User, Long> {
            @Query("select u from User u where u.emailAddress = ?1")
            User findByEmailAddress(String emailAddress);
            @Query(value="select count(ts_user_id) from ts_user group by register_type limit ?1, ?2", nativeQuery=true)
            List registerPage(int offset, int length);  
        }  
      • 使用命名參數
        public interface UserRepository extends JpaRepository<User, Long> {
            @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
            User findByLastnameOrFirstname(@Param("lastname") String lastname,  @Param ( "firstname" ) String firstname);
        }  
      • 定義更新方法,注意這裏參數序號也許比上面命名參數更方便
        public class Snippet {
            @Modifying @Transactional  @Query ( "update User u set u.firstname = ?1 where u.lastname = ?2" )
            int setFixedFirstnameFor(String firstname, String lastname);
        }  
    • 複雜查詢條件
      Specification,複雜查詢條件封裝,Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); 
      Predicate,條件組合
      Root,查詢根能夠有多個
      CriteriaQuery,提供獲得查詢根的方法
      CriteriaBuilder,構件查詢對象,CriteriaQuery<Object> createQuery();
    • 簡單查詢代碼,直接使用Root+CriteriaQuery+CriteriaBuilder
      Specification<TsUser> spec = new Specification<TsUser>() {
          @Override
          public Predicate toPredicate(Root<TsUser> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
              List<Predicate> predicates = new ArrayList<Predicate>();
              if(StringUtils.isNotBlank(userSearch.getUserName())) predicates.add(cb.like(root.<String>get("userName"), "%"+userSearch.getUserName()+"%"));
               query.where(predicates.toArray( new  Predicate[0]));
              return null;
          }
      };  
    • 封裝查詢代碼,使用Criteria封裝多個條件並返回Predicate判斷
      Specification<TsUser> spec = new Specification<TsUser>() {
          @Override
          public Predicate toPredicate(Root<TsUser> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
              Criteria<TsUser> c = new Criteria<TsUser>();
              c.add(Restrictions.like("userName", userSearch.getUserName())); // UserSearch是前端封裝的查詢表單對象
      c.add(Restrictions.between("createdAt", DateUtil.parse(costSearch.getDateFrom()), DateUtil.parse(costSearch.getDateTo()))); //區間查詢  
              c.add(Restrictions.like("tsUserreg.userAlias", userSearch.getUserAlias())); //查詢其餘相關表字段
               c.add(Restrictions. eq ( "status" , userSearch.getStatus())); // 精確查詢
              return c.toPredicate(root, query, cb);
          }
      };  
      //再次簡化,原來Criteria封裝並無使用Root+CriteriaQuery+CriteriaBuilder,因此添加spec()函數生成Specification查詢條件
      Criteria<TsUser> c = new Criteria<>();
      c.add(Restrictions.like("userName", userSearch.getUserName()));
      c.add(Restrictions. like ( "tsAccount.paymentAccountId" , userSearch.getPaymentAccountId()));
      c.add(Restrictions.eq("tsUserreg.roleIds", userSearch.getRole()));//實際上應該封裝find_in_set函數,暫時僅支持單一角色
      Pageable pageable =  new  PageRequest(pager.getCurrentPage()-1, pager.getPageSize(), Direction. DESC "id" );
      Page<User> users = userService.userPage(c.spec(), pageable);  
      Restrictions.java片斷
      public static SimpleExpression eq(String fieldName, Object value) {  
          if(StringUtils.isEmpty(value))return null;  
          return new SimpleExpression (fieldName, value, Operator.EQ);  
      } 
      public   static  BetweenExpression between(String fieldName, Object value, Object value2) {
          if(StringUtils.isEmpty(value) && StringUtils.isEmpty(value2)) return null;
          return new BetweenExpression(fieldName, value, value2);
      }   
      AbstractExpression,計算查詢路徑(支持屬性嵌套,例如address.zipCode)
      public abstract class AbstractExpression implements Criterion {
          Path getExpression(Root<?> root, String fieldName) {
              Path expression = null;  
              if(fieldName.contains(".")){  
                  String[] names = StringUtils.split(fieldName, ".");  
                  expression = root.get(names[0]);  
                  for (int i = 1; i < names.length; i++) {  
                      expression = expression.get(names[i]);  
                  }  
              }else expression = root.get(fieldName);  }
              return expression;
          }
      } 
      SimpleExpression,支持Equal、Like、LessThan等條件
      public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder) {  
           Path expression = getExpression(root,  fieldName );  
           switch  ( operator ) {  
          case EQ:  
              return builder.equal(expression, value);  
           case   LIKE :  
              return builder.like((Expression<String>) expression, "%" + value + "%");    
          }  
      }  
      BetweenExpression,支持區間查詢
      public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
          Path expression = getExpression(root, fieldName);
          if(StringUtils.isEmpty(value)) {
              return builder.greaterThanOrEqualTo(expression, (Comparable)value);
          }else if(StringUtils.isEmpty(value2)) {
              return builder.lessThanOrEqualTo(expression, (Comparable)value2);
          }else {
              return builder.between(expression, (Comparable)value, (Comparable)value2);
          }
      }   
      Criteria,
      public class Criteria<T> implements Specification<T>{  
          private List<Criterion> criterions = new ArrayList<Criterion>();  
          public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {  
              if (!criterions.isEmpty()) {  
                  List<Predicate> predicates = new ArrayList<Predicate>();  
                  for(Criterion c : criterions){  
                      predicates.add(c.toPredicate(root, query,builder));  
                  }  
                  // 將全部條件用 and 聯合起來  
                  if (predicates.size() > 0) {  
                      return builder.and(predicates.toArray(new Predicate[predicates.size()]));  
                  }  
              }  
              return builder.conjunction();   // 沒有條件時至關於true
          }  
          public void add(Criterion criterion){  
              if(criterion!=null){  
                  criterions.add(criterion);  
              }  
          }  
          public Specification<T> spec() {
              return new Specification<T>() {
                  public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                      return Criteria.this.toPredicate(root, query, cb);
                  }
              };
          }  
      }  
    • querydsl查詢接口支持,查詢根、字段都對象化了(querydsl maven-plugin任務生成的類),不像Criteria封裝有實體類屬性名稱硬編碼
      QTask $ = QTask.task;
      BooleanExpression titleStartWith = $.title.startsWith("Study");
      BooleanExpression descEndWith = $.description.endsWith("org/");
      Iterable<Task> result = taskDao.findAll(titleStartWith.and(descEndWith));
  • domain領域層,領域對象是業務核心層,與持久層鬆散耦合(須要領域對象與表實體類之間轉換);
    • 查詢分頁,domain領域層接口只是銜接service業務層和dao持久層,將數據庫表對象TsUser轉換爲領域對象User
      public Page<User> userPage(Specification<TsUser> spec, Pageable pageable) {
          Page<TsUser> page = userRepository.findAll(spec, pageable);
          List<User> content = new ArrayList<User>();
          Iterator<TsUser> iterator = page.iterator();
          while(iterator.hasNext()) {
              content.add(User.formUser(iterator.next()));
          }
          return new PageImpl<User>(content, pageable, page.getTotalElements());
      }  
    • Audit審計,記錄實體建立和修改的管理帳戶,
      • domain領域對象需實現接口Auditable<U, ID>(U是審計類型,一般是用戶名或用戶實體類),void setCreatedBy(final U createdBy);
      • orm.xml註冊監聽器
        <persistence-unit-metadata>
            <persistence-unit-defaults>
                <entity-listeners>
                     <entity-listener class = "org.springframework.data.jpa.domain.support.AuditingEntityListener"  />
                </entity-listeners>
            </persistence-unit-defaults>
        </persistence-unit-metadata> 
      •  激活AuditorAware的自定義實現,其方法T getCurrentAuditor();返回的就是審計的用戶(能夠是String用戶名或User用戶對象,是setCreatedBy(U)的參數)
        <jpa:auditing auditor-aware-ref="yourAuditorAwareBean" /> 
  • service業務層,面向前端頁面封裝表單對象,定義接口並調用domain層來實現(須要表單對象與領域對象之間轉換),建議每一個web項目都有各自的service層(如front-service,admin-service等)
    • 查詢分頁,須要構建Specification查詢條件,將領域對象User轉換爲頁面表單對象UserItem
      Specification<TsUser> spec = new Specification<TsUser>() {
          @Override
          public Predicate toPredicate(Root<TsUser> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
              Criteria<TsUser> c = new Criteria<TsUser>();
              c.add(Restrictions.like("userName", userSearch.getUserName()));
               c.add(Restrictions. eq ( "tsUserreg.status" , userSearch.getStatus()));
              return c.toPredicate(root, query, cb);
          }
      };  
      Page<User> users = userService.userPage(spec, pageable);  
      List<UserItem> userItems = new ArrayList<UserItem>();
      for (User user : users.getContent()) {
          UserItem userItem = UserItem.formUserItem(user);
           userItems.add(userItem);
      }  
  • web控制層(按項目劃分如front-web,admin-web等)
    • 前端開發人員負責內容
      頁面:jsp或vm
      控制:Controller
      業務接口及頁面對象:front-service,UserItem等,自定義接口並提供假數據展現到頁面
    • 後臺開發人員負責內容
      負責domain和dao層設計開發(若是前端開發人員面向domain層開發,則造成緊耦合,會影響項目進度)
      負責實現front-service等服務項目的接口實現,提供數據庫數據
  • 數據校驗和異常處理
    • web層數據校驗:spring會自動校驗@Valid參數並將結果存入BindingResult對象,UserItem等對象須要使用@NotNull等校驗註解
      UserController.userUpdate(@Valid UserItem, BindingResult bindingResult, Model model){ if(bindingResult.hasErrors()) return NOT_VALID_PAGE; }
    • service+domain數據校驗
      建議用AOP實現,全局拋校驗異常,而後轉異常處理邏輯
    • 異常處理,
      @Service("excetionResolver")
      public class ExceptionResolver extends DefaultHandlerExceptionResolver {
          @Override
          protected ModelAndView doResolveException(HttpServletRequest request,
                  HttpServletResponse response, Object handler, Exception ex) {
              if(ex instanceof AuthorizationException) {//未受權或過時則拒絕訪問,校驗失敗異常可相似處理
                  if(request.getRequestURI().contains("admin/home.html")) {
                      return new ModelAndView("redirect:login.html");
                  }
                  return new ModelAndView("admin/denied");
              } else   if (ex  instanceof  AuthenticationException) {//錯誤密碼超過5次等,認證錯誤
                  return new ModelAndView("redirect:login.html");
              }
              // return   new  ModelAndView( "admin/error" );//其餘業務異常,顯示錯誤頁面
              return null;//不認識的異常直接拋出,保證service底層異常的事務能夠回滾
          }
      }  


相關文章
相關標籤/搜索