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