SpringDataJPA的使用
JPA是什么?
JPA(Java Persistence API)是Sun官方提出的Java持久化規范. 為Java開發人員提供了一種對象/關聯映射工具來管理Java應用中的關系數據. 它的出現是為了簡化現有的持久化開發工作和整合ORM技術. 結束各個ORM框架各自為營的局面.
JPA僅僅是一套規范,不是一套產品, 也就是說Hibernate, TopLink等是實現了JPA規范的一套產品.
Spring Data JPA
Spring Data JPA是Spring基於ORM框架、JPA規范的基礎上封裝的一套JPA應用框架,是基於Hibernate之上構建的JPA使用解決方案,用極簡的代碼實現了對數據庫的訪問和操作,包括了增、刪、改、查等在內的常用功能.
實踐
- 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 添加配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://39.105.167.131:3306/smile_boot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: Nrblwbb7$
jpa:
properties:
hibernate:
hbm2ddl:
auto: create
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
format_sql: true
show-sql: true
hibernate.hbm2ddl.auto 參數的作用主要用於:自動創建、更新、驗證數據庫表結構,有四個值。
- create:每次加載 Hibernate 時都會刪除上一次生成的表,然后根據 model 類再重新來生成新表,哪怕兩次沒有任何改變也要這樣執行,這就是導致數據庫表數據丟失的一個重要原因。
- create-drop:每次加載 Hibernate 時根據 model 類生成表,但是 sessionFactory 一關閉,表就自動刪除。
- update:最常用的屬性,第一次加載 Hibernate 時根據 model 類會自動建立起表的結構(前提是先建立好數據庫),以后加載 Hibernate 時根據 model 類自動更新表結構,即使表結構改變了,但表中的行仍然存在,不會刪除以前的行。要注意的是當部署到服務器后,表結構是不會被馬上建立起來的,是要等應用第一次運行起來后才會。
- validate :每次加載 Hibernate 時,驗證創建數據庫表結構,只會和數據庫中的表進行比較,不會創建新表,但是會插入新值。
配置文件中:
- dialect 主要是指定生成表名的存儲引擎為 InnoDB
- show-sql 是否在日志中打印出自動生成的 SQL,方便調試的時候查看
- 編寫代碼
實體類:
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
private String userName;
@Column(nullable = false)
private String passWord;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = true, unique = true)
private String nickName;
@Column(nullable = false)
private String regTime;
public User(String userName, String passWord, String email, String nickName, String regTime) {
this.userName = userName;
this.passWord = passWord;
this.email = email;
this.nickName = nickName;
this.regTime = regTime;
}
public User() {
}
// getter and setter
}
注解:
- @Entity(name="EntityName") 必須,用來標注一個數據庫對應的實體,數據庫中創建的表名默認和類名一致。其中,name 為可選,對應數據庫中一個表,使用此注解標記 Pojo 是一個 JPA 實體。
- @Table(name="",catalog="",schema="") 可選,用來標注一個數據庫對應的實體,數據庫中創建的表名默認和類名一致。通常和 @Entity 配合使用,只能標注在實體的 class 定義處,表示實體對應的數據庫表的信息。
- @Id 必須,@Id 定義了映射到數據庫表的主鍵的屬性,一個實體只能有一個屬性被映射為主鍵。
- @GeneratedValue(strategy=GenerationType,generator="") 可選,strategy: 表示主鍵生成策略,有 AUTO、INDENTITY、SEQUENCE 和 TABLE 4 種,分別表示讓 ORM 框架自動選擇,generator: 表示主鍵生成器的名稱。
- @Column(name = "user_code", nullable = false, length=32) 可選,@Column 描述了數據庫表中該字段的詳細定義,這對於根據 JPA 注解生成數據庫表結構的工具。name: 表示數據庫表中該字段的名稱,默認情形屬性名稱一致;nullable: 表示該字段是否允許為 null,默認為 true;unique: 表示該字段是否是唯一標識,默認為 false;length: 表示該字段的大小,僅對 String 類型的字段有效。
- @Transient可選,@Transient 表示該屬性並非一個到數據庫表的字段的映射,ORM 框架將忽略該屬性。
- @Enumerated 可選,使用枚舉的時候,我們希望數據庫中存儲的是枚舉對應的 String 類型,而不是枚舉的索引值,需要在屬性上面添加 @Enumerated(EnumType.STRING) 注解。
基本都是hibernate的注解
4. Repository構建
public interface UserRepository extends JpaRepository<User,Long> {
User findByUserNameOrEmail(String userName, String email);
User findByUserName(String userName);
}
因為是繼承,所以父類有的方法全部繼承,可以查看父類的源碼來看看有哪些方法.
- 測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTest {
@Resource
private UserRepository userRepository;
@Test
public void test(){
Date data = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formattedDate = dateFormat.format(data);
userRepository.save(new User("aa","aa123456","aa@126.com","aa",formattedDate));
userRepository.save(new User("bb","bb123456","bb@126.com","bb",formattedDate));
userRepository.save(new User("cc","cc123456","cc@126.com","cc",formattedDate));
Assert.assertEquals(3,userRepository.findAll().size());
Assert.assertEquals("bb",userRepository.findByUserNameOrEmail("bb","bb@126.com").getNickName());
userRepository.delete(userRepository.findByUserName("aa"));
}
}
查詢語句
在jpa中查詢分為兩類,一類是繼承了父類的方法的基本查詢,另一類是自定義查詢.
基本查詢
圖中黑色就是自定義的,灰色的就是從父類繼承的.
自定義查詢
Spring Data JPA 可以根據接口方法名來實現數據庫操作,主要的語法是 findXXBy、readAXXBy、queryXXBy、countXXBy、getXXBy 后面跟屬性名稱,利用這個功能僅需要在定義的 Repository 中添加對應的方法名即可,使用時 Spring Boot 會自動幫我們實現.
根據用戶名查詢用戶:
User findByUserName(String userName);
也可以加一些關鍵字 And、or:
User findByUserNameOrEmail(String username,String email);
修改、刪除、統計也是類似語法:
Long deleteById(Long id);
Long countByUserName(String userName)
基本上 SQL 體系中的關鍵詞都可以使用,如 LIKE 、IgnoreCase、OrderBy:
List<User> findByEmailLike(String email);
User findByUserNameIgnoreCase(String userName);
List<User> findByUserNameOrderByEmailDesc(String email);
關鍵字的使用和生產SQL:
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
TRUE | findByActiveTrue() | … where x.active = true |
FALSE | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
自定義SQL查詢
- 在UserRepository中增加方法:
/**
* @Author Smith
* @Description 自定義Sql查詢.(這個本來是HQL的寫法,我的運行不了,改成了本地的SQL)
* @Date 10:18 2019/1/24
* @Param
* @return org.springframework.data.domain.Page<com.jpa.springdatajpa.model.User>
**/
@Query(value = "select * from user",nativeQuery = true)
Page<User> findALL(Pageable pageable);
/**
* @Author Smith
* @Description 原生SQL的寫法,?1表示方法參數中的順序
* @Date 10:20 2019/1/24
* @Param
* @return org.springframework.data.domain.Page<com.jpa.springdatajpa.model.User>
**/
@Query(value = "select * from user where nick_name = ?1",nativeQuery = true)
Page<User> findByNickName(String nickName, Pageable pageable);
/**
* @Author Smith
* @Description 修改,添加事務的支持
* @Date 10:21 2019/1/24
* @Param
* @return int
**/
@Transactional(timeout = 10)
@Modifying
@Query("update User set userName = ?1 where id = ?2")
int modifyById(String userName, Long id);
/**
* @Author Smith
* @Description 刪除
* @Date 10:22 2019/1/24
* @Param
* @return void
**/
@Transactional
@Modifying
@Query("delete from User where id = ?1")
@Override
void deleteById(Long id);
測試
@Test
public void testFindALL(){
int page = 1;
int size = 1;
Sort sort = new Sort(Sort.Direction.DESC,"id");
Pageable pageable = PageRequest.of(page,size,sort);
Page<User> all = userRepository.findALL(pageable);
Assert.assertEquals(1,all.getContent().size());
Assert.assertEquals(2,all.getTotalPages());
}
@Test
public void testFindByNickName(){
int page = 0;
int size = 1;
Sort sort = new Sort(Sort.Direction.DESC,"id");
Pageable pageable = PageRequest.of(page,size,sort);
Page<User> all = userRepository.findByNickName("bb",pageable);
Assert.assertEquals(1,all.getContent().size());
Assert.assertEquals(1,all.getTotalPages());
}
需要注意的是Pageable分頁的使用,其余的基本沒什么需要注意的.
限制查詢
只需要查詢前 N 個元素,或者只取前一個實體。
User findFirstByOrderByNickNameAsc();
User findTopByOrderByIdDesc();
Page<User> queryFirst10ByNickName(String nickName, Pageable pageable);
List<User> findFirst10ByNickName(String nickName, Sort sort);
List<User> findTop10ByNickName(String nickName, Pageable pageable);
這沒有做測試
復雜查詢
在某些情況下查詢條件很多,需要不斷拼接屬性,方法名會顯得很長,這個時候就要使用JpaSpecificationExecutor 接口了.
概念:
- Root
root,代表了可以查詢和操作的實體對象的根,開一個通過 get("屬性名") 來獲取對應的值。 - CriteriaQuery query,代表一個 specific 的頂層查詢對象,它包含着查詢的各個部分,比如 select 、from、where、group by、order by 等。
- CriteriaBuilder cb,來構建 CritiaQuery 的構建器對象,其實就相當於條件或者是條件組合,並以 Predicate 的形式返回。
實體:
@Entity
public class UserDetail {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
private Long userId;
private Integer age;
private String realName;
private String status;
private String hobby;
private String introduction;
private String lastLoginIp;
// getter/setter
repository:
public interface UserDetailRepository extends JpaSpecificationExecutor<UserDetail>,
JpaRepository<UserDetail, Long> {
}
service和serviceImpl
public interface UserDetailService {
public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable pageable);
}
@Service
public class UserDetailServiceImpl implements UserDetailService {
@Resource
private UserDetailRepository userDetailRepository;
@Override
public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable pageable){
return userDetailRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
//equal 示例
if (!StringUtils.isNullOrEmpty(detailParam.getIntroduction())){
predicates.add(cb.equal(root.get("introduction"),detailParam.getIntroduction()));
}
//like 示例
if (!StringUtils.isNullOrEmpty(detailParam.getRealName())){
predicates.add(cb.like(root.get("realName"),"%"+detailParam.getRealName()+"%"));
}
//between 示例
if (detailParam.getMinAge()!=null && detailParam.getMaxAge()!=null) {
Predicate agePredicate = cb.between(root.get("age"), detailParam.getMinAge(), detailParam.getMaxAge());
predicates.add(agePredicate);
}
//greaterThan 大於等於示例
/*if (detailParam.getMinAge()!=null){
predicates.add(cb.greaterThan(root.get("age"),detailParam.getMinAge()));
}*/
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}, pageable);
}
}
測試:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDetailTest {
@Resource
private UserDetailService userDetailService;
@Test
public void testFindByCondition() {
int page=0,size=10;
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(page, size, sort);
UserDetailParam param=new UserDetailParam();
param.setIntroduction("程序員");
param.setMinAge(10);
param.setMaxAge(30);
Page<UserDetail> page1=userDetailService.findByCondition(param,pageable);
for (UserDetail userDetail:page1){
System.out.println("userDetail: "+userDetail.toString());
}
}
}
在我本地測試失敗了,報了個mysql的混合字符集的錯,找了會發現使用的方言的問題,可以從數據庫看到生成表的排序規則是latin1_swedish_ci
,所以報錯.解決方案是:新建一個配置類:
public class MysqlConfig extends MySQL5Dialect {
@Override
public String getTableTypeString() {
return " ENGINE=InnoDB DEFAULT CHARSET=utf8";
}
}
在配置文件中進行修改
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://39.105.167.131:3306/smile_boot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: Nrblwbb7$
jpa:
properties:
hibernate:
hbm2ddl:
auto: create
# 注意這行,為自己的配置文件的路徑
dialect: com.jpa.springdatajpa.config.MysqlConfig
format_sql: true
show-sql: true
這樣生成的表就是utf8_general_ci了,問題就解決了.
多表查詢
新建實體類:
public interface UserInfo {
String getUserName();
String getEmail();
String getHobby();
String getIntroduction();
}
repository方法:
@Query("select u.userName as userName, u.email as email, d.introduction as introduction , d.hobby as hobby from User u , UserDetail d " +
"where u.id=d.userId and d.hobby = ?1 ")
List<UserInfo> findUserInfo(String hobby);
測試:
@Test
public void testUserInfo() {
List<UserInfo> userInfos=userDetailRepository.findUserInfo("釣魚");
for (UserInfo userInfo:userInfos){
System.out.println("userInfo: "+userInfo.getUserName()+"-"+userInfo.getEmail()+"-"+userInfo.getHobby()+"-"+userInfo.getIntroduction());
}
}
上面就是關於springdatajpa在springboot中的使用了.
源碼鏈接: https://github.com/MissWangLove/SpringBoot