1. 程式人生 > >SpringBoot:SpringData JPA:進階查詢—JPQL/原生SQL查詢、分頁處理、部分欄位對映查詢

SpringBoot:SpringData JPA:進階查詢—JPQL/原生SQL查詢、分頁處理、部分欄位對映查詢

上一篇介紹了入門基礎篇SpringDataJPA訪問資料庫。本篇介紹SpringDataJPA進一步的定製化查詢,使用JPQL或者SQL進行查詢、部分欄位對映、分頁等。本文儘量以簡單的建模與程式碼進行展示操作,文章比較長,包含查詢的方方面面。如果能耐心看完這篇文章,你應該能使用SpringDataJPA應對大部分的持久層開發需求。如果你需要使用到動態條件查詢,請檢視下一篇部落格,專題介紹SpringDataJPA的動態查詢。

一、入門引導與準備


JPQL(JavaPersistence Query Language)是一種面向物件的查詢語言,它在框架中最終會翻譯成為sql進行查詢,如果不知JPQL請大家自行谷歌瞭解一下,如果你會SQL,瞭解這個應該不廢吹灰之力。

1.核心註解@Query介紹


使用SpringDataJPA進行JPQL/SQL一般查詢的核心是@Query註解,我們先來看看該註解

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@QueryAnnotation
@Documented
public @interface Query {
    String value() default "";
    String countQuery() default "";
    String countProjection() default "";
    boolean nativeQuery() default false;
    String name() default "";
    String countName() default "";
}


該註解使用的註解位置為方法、註解型別,一般我們用於註解方法即可。@QueryAnnotation標識這是一個查詢註解;
@Query註解中有6個引數,value引數是我們需要填入的JPQL/SQL查詢語句;nativeQuery引數是標識該查詢是否為原生SQL查詢,預設為false;countQuery引數為當你需要使用到分頁查詢時,可以自己定義(count查詢)計數查詢的語句,如果該項為空但是如果要用到分頁,那麼就使用預設的主sql條件來進行計數查詢;name引數為命名查詢需要使用到的引數,一般配配合@NamedQuery一起使用,這個在後面會說到;countName引數作用與countQuery相似,但是使用的是命名查詢的(count查詢)計數查詢語句;countProjection為涉及到投影部分欄位查詢時的計數查詢(count查詢);關於投影查詢,待會會說到。

有了@Query基礎後,我們就可以小試牛刀一把了,對於jar包依賴,我們用的依舊是上一節的依賴,程式碼如下:

2.準備實驗環境

<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>1.4.1.RELEASE</version>  
</parent>  
  
<properties>  
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
    <java.version>1.8</java.version>  
    <springBoot.groupId>org.springframework.boot</springBoot.groupId>  
</properties>  
  
<dependencies>  
    <!-- SpringBoot Start -->  
    <dependency>  
        <groupId>${springBoot.groupId}</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
    <!-- jpa -->  
    <dependency>  
        <groupId>${springBoot.groupId}</groupId>  
        <artifactId>spring-boot-starter-data-jpa</artifactId>  
    </dependency>  
    <dependency>  
        <groupId>${springBoot.groupId}</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
    </dependency>  
    <!-- mysql -->  
    <dependency>  
        <groupId>mysql</groupId>  
        <artifactId>mysql-connector-java</artifactId>  
    </dependency>  
    <dependency>  
        <groupId>junit</groupId>  
        <artifactId>junit</artifactId>  
        <version>4.12</version>  
    </dependency>  
</dependencies>


專案結構如下:

JpaConfiguration配置類與上篇的相同:

@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@EnableTransactionManagement(proxyTargetClass=true)
@EnableJpaRepositories(basePackages={"org.fage.**.repository"})
@EntityScan(basePackages={"org.fage.**.entity"})
public class JpaConfiguration {
    @Bean
    PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor(){
        return new  PersistenceExceptionTranslationPostProcessor();
    }
}


App類:

@SpringBootApplication
@ComponentScan("org.fage.**")
public class App {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }
}


對於實體建模依舊用到上一篇所用的模型Department、User、Role,Department與User為一對多,User與Role為多對多,為了方便後面介紹投影,user多增加幾個欄位,程式碼如下:

 

@Entity
@Table(name = "user")
public class User implements Serializable {
 
    private static final long serialVersionUID = -7237729978037472653L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String password;
    @Column(name = "create_date")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Temporal(TemporalType.TIMESTAMP)
    private Date createDate;
    private String email;
    // 一對多對映
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
    // 多對多對映
    @ManyToMany @JsonBackReference
    @JoinTable(name = "user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
            @JoinColumn(name = "role_id") })
    private List<Role> roles;
//getter and setter .....
}
@Entity
@Table(name = "department")
public class Department implements Serializable {
    
    /**
     * 
     */
    private static final long serialVersionUID = 3743774627141615707L;
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    @OneToMany(mappedBy = "department")@JsonBackReference
    @JsonBackReferenceprivate List<User> users;
    //getter and setter
}
@Entity
@Table(name="role")
public class Role implements Serializable{
    
    /**
     * 
     */
    private static final long serialVersionUID = 1366815546093762449L;
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    //getter and setter
}


建模成功時,生成的表結構如下:

對於Repository:

@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long>{}

 

@Repository
public interface RoleRepository extends JpaRepository<Role, Long>{}

 

@Repository
public interface UserRepository extends JpaRepository<User, Long>{
}


如果以上程式碼有看不懂的地方,請移步到上一篇一覽基礎篇。至此,我們已經將環境整理好了,至於表中的資料插入,希望各位參考上一篇文章進行基礎的crud操作將表中資料進行填充,接下來介紹@Query查詢

二、使用JPQL查詢


1 .核心查詢與測試樣例


在UserRepository中增加以下方法:

//--------------JPQL查詢展示-------------//
 
    //展示位置引數繫結
    @Query(value = "from User u where u.name=?1 and u.password=?2")
    User findByNameAndPassword(String name, String password);
 
    //展示名字引數繫結
    @Query(value = "from User u where u.name=:name and u.email=:email")
    User findByNameAndEmail(@Param("name")String name, @Param("email")String email);
    
    //展示like模糊查詢
    @Query(value = "from User u where u.name like %:nameLike%")
    List<User> findByNameLike(@Param("nameLike")String nameLike);
    
    //展示時間間隔查詢
    @Query(value = "from User u where u.createDate between :start and :end")
    List<User> findByCreateDateBetween(@Param("start")Date start, @Param("end")Date end);
    
    //展示傳入集合引數查詢
    @Query(value = "from User u where u.name in :nameList")
    List<User> findByNameIn(@Param("nameList")Collection<String> nameList);
    
    //展示傳入Bean進行查詢(SPEL表示式查詢)
    @Query(value = "from User u where u.name=:#{#usr.name} and u.password=:#{#usr.password}")
    User findByNameAndPassword(@Param("usr")User usr);
    
    //展示使用Spring自帶分頁查詢
    @Query("from User u")
    Page<User> findAllPage(Pageable pageable);
    
    //展示帶有條件的分頁查詢
    @Query(value = "from User u where u.email like %:emailLike%")
    Page<User> findByEmailLike(Pageable pageable, @Param("emailLike")String emailLike);

TestClass的程式碼如下: 


@RunWith(SpringRunner.class)
@SpringBootTest
public class TestClass {
    final Logger logger = LoggerFactory.getLogger(TestClass.class);
    @Autowired
    UserRepository userRepository;
    
    @Test
    public void testfindByNameAndPassword(){
        userRepository.findByNameAndPassword("王大帥", "123");
    }
    
    @Test
    public void testFindByNameAndEmail(){
        userRepository.findByNameAndEmail("張大仙", "[email protected]");
    }
    
    @Test
    public void testFindByNameLike(){
        List<User> users = userRepository.findByNameLike("馬");
        logger.info(users.size() + "----");
    }
    
    @Test
    public void testFindByCreateDateBetween() throws ParseException{
        List<User> users = userRepository.findByCreateDateBetween(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2018-01-01 00:00:00"), new Date(System.currentTimeMillis()));
        logger.info(users.size() + "----");
    }
    
    @Test
    public void testFindByNameIn(){
        List<String> list = new ArrayList<String>();
        list.add("王大帥");
        list.add("李小三");
        userRepository.findByNameIn(list);
    }
    
    @Test
    public void testfindByNameAndPasswordEntity(){
        User u = new User();
        u.setName("李小三");
        u.setPassword("444");
        userRepository.findByNameAndPassword(u);
    }
    
    @Test
    public void testFindAllPage(){
        Pageable pageable = new PageRequest(0,5);
        Page<User> page = userRepository.findAllPage(pageable);
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(page); 
        logger.info(json);
    }
    @Test
    public void findByEmailLike(){
        Pageable pageable = new PageRequest(0,5,new Sort(Direction.ASC,"id"));
        userRepository.findByEmailLike(pageable, "@qq.com");
    }
}


至此,顯示了使用JPQL進行單表查詢的絕大多數操作,當你在實體設定了fetch=FetchType.LAZY 或者EAGER時,會有不同的自動連線查詢,鼓勵大家自行嘗試。以上查詢語句有必要對其中幾個進行解釋一下;
對於UserRepository中的第一與第二個方法,目的是為了比較與展示位置繫結與名字繫結的區別,相信根據名稱大家就能判別是什麼意思與區別了,位置繫結即是方法引數從左到右第123456...所在位置的引數與查詢語句中的第123456...進行對應。名字繫結即是查詢語句中的引數名稱與方法引數名稱一一對應;對於第三個與第四個查詢例子就不多說了;第五條查詢語句展示的是傳入集合進行in查詢;第六條查詢例子展示的是傳入bean進行查詢,該查詢使用的表示式是Spring的SPEL表示式;

2. 分頁與排序


最後兩條查詢語句展示的是進行分頁查詢、分頁並排序查詢,使用的計數查詢預設使用主查詢語句中的條件進行count, 當Repository介面的方法中含有Pageable引數時,那麼SpringData認為該查詢是需要分頁的;org.springframework.data.domain.Pageable是一個介面,介面中定義了分頁邏輯操作,它具有一個間接實現類為PageRequest,我們最需要關注的是PageRequest這個實現類的三個構造方法:

public class PageRequest extends AbstractPageRequest {
....
....
    public PageRequest(int page, int size) {
        this(page, size, null);
    }
    public PageRequest(int page, int size, Direction direction, String... properties) {
        this(page, size, new Sort(direction, properties));
    }
    public PageRequest(int page, int size, Sort sort) {
        super(page, size);
        this.sort = sort;
    }
....
....
}


page引數為頁碼(查第幾頁,從0頁開始),size為每頁顯示多少條記錄數;
Direction則是一個列舉,如果該引數被傳入則進行排序,常用的有Direction.ASC/Direction.DESC,即正序排與逆序排,如果排序,需要根據哪個欄位排序呢?properties是一個可變長引數,傳入相應欄位名稱即可根據該欄位排序。還有最後一個引數Sort,Sort這個類中有一個構造方法:public Sort(Direction direction, String... properties),沒錯,我不用說相信大家都已經懂了是幹什麼用的了。
Pageable與PageRequest的關係解釋完了,那麼就該介紹一下最後兩條查詢語句的返回值Page<T>是幹什麼用的了,讓我們看看倒數第二個測試方法返回的json串結果:

{ "content": [ 
{ "id": 1,"name": "王大帥","password": "123", "createDate": 1515312688000, "email": "[email protected]","department": { "id": 1, "name": "開發部"}},
{ "id": 2, "name": "張大仙", "password": "456", "createDate": 1515139947000, "email": "[email protected]", "department": {"id": 1, "name": "開發部" }},
{"id": 3, "name": "李小三","password": "789","createDate": 1514794375000, "email": "[email protected]","department": {"id": 1, "name": "開發部" }},
{"id": 4, "name": "馬上來","password": "444", "createDate": 1512116003000, "email": "[email protected]", "department": { "id": 1,"name": "開發部" } },
{ "id": 5, "name": "馬德華", "password": "555","createDate": 1515312825000,"email": "[email protected]","department": { "id": 1, "name": "開發部"} }],
  "last": true,
  "totalPages": 1,
  "totalElements": 5,
  "size": 5,
  "number": 0,
  "sort": null,
  "first": true,
  "numberOfElements": 5
}


跟蹤原始碼得到結論,Page<T>是一個介面,它的基類介面Slice<T>也是一個介面,而實現類Chunk實現了Slice,實現類PageImpl繼承了Chunk並且實現了Page介面。所以實際上Json輸出的字串是PageImpl的擁有的所有屬性(包括其父類Chunk)。content屬性是分頁得出的實體集合,型別為List,也就是上面json串中的content。last屬性表示是否為最後一頁,totalPages表示總頁數,totalElements表示總記錄數,size為每頁記錄數大小,number表示當前為第幾頁,numberOfElements表示當前頁所擁有的記錄數,first表示當前是否第一頁,sort為排序資訊。
到這裡,Page與Pageable都瞭解了。

3. 關聯查詢與部分欄位對映投影


接下來介紹使用JPQL進行關聯查詢與部分欄位對映。現在的查詢需求是,查出所有使用者的名字、使用者所屬部門、使用者的email、統計使用者所擁有的角色有多少個,然後將列表結果進行給前端顯示。有的朋友說,那我把關聯到的物件都拿出來不就完了。可是,實際開發中一個表下有幾十個欄位會很常見,如果全部都拿出來是沒有必要的,所以我們可以把需要的欄位拿出來就可以了,下面介紹兩種方法實現這種需求。

3.1 使用VO(view object)做對映與投影


我們在src/main/java中增加一個org.fage.vo包,該包下存放VO物件,我們在該包下建立一個UserOutputVO:

public class UserOutputVO {
    private String name;        //使用者的名字
    private String email;        //使用者的email
    private String departmentName;    //使用者所屬的部門
    private Long roleNum;        //該使用者擁有的角色數量
    
    public UserOutputVO(String name, String email, String departmentName, Long roleNum) {
        super();
        this.name = name;
        this.email = email;
        this.departmentName = departmentName;
        this.roleNum = roleNum;
    }
    public UserOutputVO() {
        super();
    }
    //getter and setter and toString
    ...
}


在UserRepository中建立查詢方法:

@Query(value = "select new org.fage.vo.UserOutputVO(u.name, u.email, d.name as departmentName, count(r.id) as roleNum) from User u "
            + "left join u.department d left join u.roles r group by u.id")
    Page<UserOutputVO> findUserOutputVOAllPage(Pageable pageable);


這裡注意一下,VO中的構造方法引數一定要與查詢語句中的查詢欄位型別相匹配(包括數量),如果不匹配就會報錯。以下是測試程式碼:

@Test
    public void testFindUserOutputVOAllPage(){
        Pageable pageable = new PageRequest(0,5);
        Page<UserOutputVO> page = userRepository.findUserOutputVOAllPage(pageable);
        List<UserOutputVO> list = page.getContent();
        for(UserOutputVO vo : list)
            logger.info(vo.toString());
    }


輸出結果:

對於連線查詢,有join、left join 、right join,與sql的類似,但是唯一需要注意的地方就是建模的關係要能連線起來,因為只有這樣才能使用“.”進行連線;就像你想的那樣,它是類似物件導航的,與sql的表連線有些使用上的不同,但是最終的連線結果是相同的。

3.2 使用projection介面做對映與投影


依然使用的是上面查詢VO的需求進行查詢,換成projection的方式,在org.fage.vo中建立一個介面:

public interface UserProjection {
    String getName();
    
    @Value("#{target.emailColumn}")//當別名與該getXXX名稱不一致時,可以使用該註解調整
    String getEmail();
    
    String getDepartmentName();
    
    Integer getRoleNum();
}


在UserRepository中建立查詢語句:

//故意將email別名為emailColumn,以便講解@Value的用法
    @Query(value = "select u.name as name, u.email as emailColumn, d.name as departmentName, count(r.id) as roleNum from User u "
            + "left join u.department d left join u.roles r group by u.id")
    Page<UserProjection> findUserProjectionAllPage(Pageable pageable);


在TestClass中新增測試方法:

@Test
    public void testFindUserProjectionAllPage(){
        Page<UserProjection> page = userRepository.findUserProjectionAllPage(new PageRequest(0,5));
        Collection<UserProjection> list = page.getContent();
        for(UserProjection up : list){
            logger.info(up.getName());
            logger.info(up.getEmail());
            logger.info(up.getDepartmentName());
            logger.info(up.getRoleNum()+"");
        }
    }


測試結果是成功的。在這裡需要注意幾點約束,Projection介面中必須以“getXXX”來命名方法,關於“XXX”則是要與查詢語句中的別名相對應,注意觀察上面的Projection介面與查詢語句就發現了。不難發現,有一個別名為emailColumn,與Projection介面中的getEmail方法並不對應,這種時候可以使用@Value{"${target.xxx}"}註解來調整,注意其中的target不能省略,可以把target看成用別名查出來的臨時物件,這樣就好理解了。
兩種方式都可以,對於到底哪種方式好,這取決於你的需求。

4.命名查詢

如果以上查詢例項都弄懂了,那麼命名查詢也是類似的,換湯不換藥;這裡簡單的只舉兩個例子,需求變更時請大家自行嘗試。
命名查詢的核心註解是@NamedQueries 與 @NamedQuery;@NamedQueries中只有一個value引數,該引數是@NamedQuery的陣列。@NamedQuery註解我們需要關注兩個引數,query引數也就是需要寫入查詢語句的地方;name引數則是給該查詢命名,命名方式一般約定為  “實體類名.實體對應的repository的查詢方法名”,如果看不懂沒關係,請看下面的例子。
在Role實體中增加以下程式碼:

@Entity
@Table(name="role")
@NamedQueries({
    @NamedQuery(name = "Role.findById", query = "from Role r where r.id=?1"),
    @NamedQuery(name = "Role.findAllPage", query = "from Role r")
    //...更多的@NamedQuery
    })
public class Role implements Serializable{
    
    private static final long serialVersionUID = 1366815546093762449L;
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    public Role(){
        super();
    }
    
    public Role(String name){
        this.name = name;
    }
    //getter and setter
    
}


對應的RoleRepository程式碼:

@Repository
public interface RoleRepository extends JpaRepository<Role, Long>{
    
    Role findById(Long id);
    
    Page<Role> findAllPage(Pageable pageable);
}


相應的測試程式碼:

@Test
    public void testFindRoleById(){
        roleRepository.findById(1l);
    }
    
    @Test
    public void testFindRoleAllPage(){
        roleRepository.findAll(new PageRequest(0,5));
    }


以上就是命名查詢的常用方式。

5. JPQL方式總結

還是比較建議使用JPQL方式,因為SpringDataJPA各方面(比如分頁排序)、動態查詢等等都支援得比較好,Spring的SPEL表示式還可以擴充套件到SpringSecurity與SpringDataJPA高階的session使用者查詢方式,後續部落格會有對SpringSecurity的介紹,等到那時候在一起講解。

三、使用原生SQL查詢

有些時候,JPQL使用不當會導致轉化成的sql並不如理想的簡潔與優化,所以在特定場合還是得用到原生SQL查詢的,比如當你想優化sql時等等。

1 .一般查詢


使用原生查詢時用的也是@Query註解,此時nativeQuery引數應該設定為true。我們先來看一些簡單的查詢


  

 @Query(value = "select * from user u where u.id=:id", nativeQuery = true)
    User findByIdNative(@Param("id")Long id);
    
    @Query(value = "select * from user", nativeQuery = true)
    List<User> findAllNative();


看看測試程式碼:

@Test
    @Transactional
    public void testFindByIdNative(){
        User u = userRepository.findByIdNative(1l);
        logger.info(u.toString());
        logger.info(u.getRoles().toString());
    }
    
    @Test
    public void testFindAllNative(){
        List<User> list = userRepository.findAllNative();
        for(User u : list){
            logger.info(u.toString());
        }
    }


結果發現當查所有欄位的時候,確實能對映成功,並且fetch快載入、懶載入自動關聯也能正常使用。接下來我們換剛才使用JPQL時的查詢需求,看看用SQL時該怎麼做。

2.投影與對映分頁查詢

查詢列表的需求依舊是剛才介紹使用JPQL時使用的需求(分頁查出所有使用者的名字、使用者所屬部門、使用者的email、統計使用者所擁有的角色有多少個),在UserRepository中建立程式碼片段:

 

//展示原生查詢
    @Query(value = "select u.name as name, u.email as emailColumn, d.name as departmentName, count(ur.role_id) as roleNum from user u "
            + "left join department d on d.id=u.department_id left join user_role ur on u.id=ur.user_id group by u.id limit :start,:size",
            nativeQuery = true)
    List<Object[]> findUserProjectionAllPageNative(@Param("start")int start, @Param("size")int size);
 
    //count語句
    @Query(value = "select count(u.id) from user u", nativeQuery = true)
    long countById();


在TestClass中建立測試程式碼:

@Test
    public void testFindUserProjectionAllPageNative(){
        Pageable pageable = new PageRequest(0,5);
        List<Object []> content = userRepository.findUserProjectionAllPageNative(pageable.getOffset(), pageable.getPageSize());
        long total = userRepository.countById();
        //檢視一下查詢結果
        logger.info(content.size() + "");
        for(Object[] o : content){
            logger.info("名字:" + o[0].toString());
            logger.info("email:" + o[1].toString());
            logger.info("所屬部門" + o[2].toString());
            logger.info("角色數量" + o[3].toString());
        }
        //如果需要的話,自行封裝分頁資訊
        Page<Object[]> page = new PageImpl<Object[]>(content, pageable, total);
        System.out.println(page);
    }

解釋一下上面程式碼,由於是原生查詢不支援動態分頁,Page分頁我們只能自己做了,但是依舊使用的是Spring的Page;pageable.getOffset()與pageable.getPageSize()分別對應limit ?, ?的第一與第二個問號。原生查詢得出來的List是包函一堆被封裝成Object的物件陣列,每個object陣列可以通過陣列索引拿出值,也就與需要查的欄位一一對應。如果你需要存入VO再帶回給前端,那麼你可以自行封裝。對於PageImpl,我們使用了public PageImpl(List<T> content, Pageable pageable, long total) 這個構造方法,第一個引數是查詢到的結果,第二個就不用說了,第三個引數是對主sql的count查詢。當前端需要顯示分頁時,可以這樣進行手動分頁。


3.SQL方式總結

當你需要進行sql優化時,可能用原生sql方式會更好。但是一般需求時候用JPQL還是比較方便的,畢竟這樣比較省事,拿資料總是需要分頁的,有時候只需要拿幾個欄位也是這樣。

四、總結


當你在接到一般需求時,使用JPQL的方式其實已經足夠用了。但是如果對sql需要優化的時候,你也可以使用SQL的方式。總而言之,需要根據需求來應變使用的策略。

原文參考:https://blog.csdn.net/phapha1996/article/details/78994395