一、入門引導與准備
JPQL(JavaPersistence Query Language)是一種面向對象的查詢語言,它在框架中最終會翻譯成為sql進行查詢,如果不知JPQL請大家自行谷歌了解一下,如果你會SQL,了解這個應該不廢吹灰之力。
1.核心注解@Query介紹
使用SpringDataJPA進行JPQL/SQL一般查詢的核心是@Query注解,我們先來看看該注解
-
-
-
-
-
public
-
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配置類與上篇的相同:
-
-
-
-
-
-
public class JpaConfiguration {
-
-
PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor(){
-
return new PersistenceExceptionTranslationPostProcessor();
-
}
-
}
App類:
-
-
-
public class App {
-
public static void main(String[] args) throws Exception {
-
SpringApplication.run(App.class, args);
-
}
-
}
對於實體建模依舊用到上一篇所用的模型Department、User、Role,Department與User為一對多,User與Role為多對多,為了方便后面介紹投影,user多增加幾個字段,代碼如下:
-
-
-
public class User implements Serializable {
-
-
private static final long serialVersionUID = -7237729978037472653L;
-
-
-
private Long id;
-
private String name;
-
private String password;
-
-
-
-
private Date createDate;
-
private String email;
-
// 一對多映射
-
-
-
private Department department;
-
// 多對多映射
-
-
-
-
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
-
}
-
-
-
public class Role implements Serializable{
-
-
/**
-
*
-
*/
-
private static final long serialVersionUID = 1366815546093762449L;
-
-
-
private Long id;
-
private String name;
-
-
//getter and setter
-
}
建模成功時,生成的表結構如下:
對於Repository:
-
-
public interface DepartmentRepository extends JpaRepository<Department, Long>{}
-
-
public interface RoleRepository extends JpaRepository<Role, Long>{}
-
-
public interface UserRepository extends JpaRepository<User, Long>{
-
}
如果以上代碼有看不懂的地方,請移步到上一篇一覽基礎篇。至此,我們已經將環境整理好了,至於表中的數據插入,希望各位參考上一篇文章進行基礎的crud操作將表中數據進行填充,接下來介紹@Query查詢
二、使用JPQL查詢
1 .核心查詢與測試樣例
在UserRepository中增加以下方法:
-
//--------------JPQL查詢展示-------------//
-
-
//展示位置參數綁定
-
-
User findByNameAndPassword(String name, String password);
-
-
//展示名字參數綁定
-
-
User findByNameAndEmail(@Param("name")String name, @Param("email")String email);
-
-
//展示like模糊查詢
-
-
List<User> findByNameLike(@Param("nameLike")String nameLike);
-
-
//展示時間間隔查詢
-
-
List<User> findByCreateDateBetween(@Param("start")Date start, @Param("end")Date end);
-
-
//展示傳入集合參數查詢
-
-
List<User> findByNameIn(@Param("nameList")Collection<String> nameList);
-
-
//展示傳入Bean進行查詢(SPEL表達式查詢)
-
-
User findByNameAndPassword(@Param("usr")User usr);
-
-
//展示使用Spring自帶分頁查詢
-
-
Page<User> findAllPage(Pageable pageable);
-
-
//展示帶有條件的分頁查詢
-
-
Page<User> findByEmailLike(Pageable pageable, @Param("emailLike")String emailLike);
TestClass的代碼如下:
-
-
-
public class TestClass {
-
final Logger logger = LoggerFactory.getLogger(TestClass.class);
-
-
UserRepository userRepository;
-
-
-
public void testfindByNameAndPassword(){
-
userRepository.findByNameAndPassword( "王大帥", "123");
-
}
-
-
-
public void testFindByNameAndEmail(){
-
userRepository.findByNameAndEmail( "張大仙", "2@qq.com");
-
}
-
-
-
public void testFindByNameLike(){
-
List<User> users = userRepository.findByNameLike( "馬");
-
logger.info(users.size() + "----");
-
}
-
-
-
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() + "----");
-
}
-
-
-
public void testFindByNameIn(){
-
List<String> list = new ArrayList<String>();
-
list.add( "王大帥");
-
list.add( "李小三");
-
userRepository.findByNameIn(list);
-
}
-
-
-
public void testfindByNameAndPasswordEntity(){
-
User u = new User();
-
u.setName( "李小三");
-
u.setPassword( "444");
-
userRepository.findByNameAndPassword(u);
-
}
-
-
-
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);
-
}
-
-
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": "1@qq.com","department": { "id": 1, "name": "開發部"}},
-
{ "id": 2, "name": "張大仙", "password": "456", "createDate": 1515139947000, "email": "2@qq.com", "department": {"id": 1, "name": "開發部" }},
-
{ "id": 3, "name": "李小三","password": "789","createDate": 1514794375000, "email": "3@qq.com","department": {"id": 1, "name": "開發部" }},
-
{ "id": 4, "name": "馬上來","password": "444", "createDate": 1512116003000, "email": "4@qq.com", "department": { "id": 1,"name": "開發部" } },
-
{ "id": 5, "name": "馬德華", "password": "555","createDate": 1515312825000,"email": "5@qq.com","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中創建查詢方法:
-
-
+ "left join u.department d left join u.roles r group by u.id")
-
Page<UserOutputVO> findUserOutputVOAllPage(Pageable pageable);
這里注意一下,VO中的構造方法參數一定要與查詢語句中的查詢字段類型相匹配(包括數量),如果不匹配就會報錯。以下是測試代碼:
-
-
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接口做映射與投影
-
public interface UserProjection {
-
String getName();
-
-
-
String getEmail();
-
-
String getDepartmentName();
-
-
Integer getRoleNum();
-
}
在UserRepository中創建查詢語句:
-
//故意將email別名為emailColumn,以便講解@Value的用法
-
-
+ "left join u.department d left join u.roles r group by u.id")
-
Page<UserProjection> findUserProjectionAllPage(Pageable pageable);
在TestClass中添加測試方法:
-
-
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.命名查詢
-
@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
-
-
}
-
-
public interface RoleRepository extends JpaRepository<Role, Long>{
-
-
Role findById(Long id);
-
-
Page<Role> findAllPage(Pageable pageable);
-
}
-
-
public void testFindRoleById(){
-
roleRepository.findById( 1l);
-
}
-
-
-
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。我們先來看一些簡單的查詢
-
-
User findByIdNative(@Param("id")Long id);
-
-
-
List<User> findAllNative();
看看測試代碼:
-
-
-
public void testFindByIdNative(){
-
User u = userRepository.findByIdNative( 1l);
-
logger.info(u.toString());
-
logger.info(u.getRoles().toString());
-
}
-
-
-
public void testFindAllNative(){
-
List<User> list = userRepository.findAllNative();
-
for(User u : list){
-
logger.info(u.toString());
-
}
-
}
結果發現當查所有字段的時候,確實能映射成功,並且fetch快加載、懶加載自動關聯也能正常使用。接下來我們換剛才使用JPQL時的查詢需求,看看用SQL時該怎么做。
2.投影與映射分頁查詢
查詢列表的需求依舊是剛才介紹使用JPQL時使用的需求(分頁查出所有用戶的名字、用戶所屬部門、用戶的email、統計用戶所擁有的角色有多少個),在UserRepository中創建代碼片段:
-
//展示原生查詢
-
-
+ "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(
-
-
//count語句
-
-
long countById();
在TestClass中創建測試代碼:
-
-
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的方式。總而言之,需要根據需求來應變使用的策略。
如果文中有不當的地方歡迎同學們提出建議與修改方案,但是請不要謾罵與辱罵。