1. 程式人生 > 實用技巧 >SpringBoot系列教程JPA使用姿勢

SpringBoot系列教程JPA使用姿勢

基本查詢

基本查詢也分為兩種,一種是 Spring Data 預設已經實現,一種是根據查詢的方法來自動解析成 SQL。

預先生成方法

Spring Boot Jpa 預設預先生成了一些基本的CURD的方法,例如:增、刪、改等等

1 繼承 JpaRepository

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

2 使用預設方法

@Test
public void testBaseQuery() throws Exception {
	User user=new User();
	userRepository.findAll();
	userRepository.findOne(1l);
	userRepository.save(user);
	userRepository.delete(user);
	userRepository.count();
	userRepository.exists(1l);
	// ...
}

就不解釋了根據方法名就看出意思來

自定義簡單查詢

自定義的簡單查詢就是根據方法名來自動生成 SQL,主要的語法是findXXBy,readAXXBy,queryXXBy,countXXBy,getXXBy後面跟屬性名稱:

User findByUserName(String userName);

也使用一些加一些關鍵字AndOr

User findByUserNameOrEmail(String username, String email);

修改、刪除、統計也是類似語法

Long deleteById(Long id);
Long countByUserName(String userName)

基本上 SQL 體系中的關鍵詞都可以使用,例如:LIKEIgnoreCaseOrderBy

List<User> findByEmailLike(String email);
User findByUserNameIgnoreCase(String userName);
List<User> findByUserNameOrderByEmailDesc(String email);

具體的關鍵字,使用方法和生產成SQL如下表所示

KeywordSampleJPQL 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(Collectionages) … where x.age in ?1
NotIn findByAgeNotIn(Collectionage) … 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

分頁查詢

分頁查詢在實際使用中非常普遍了,Spring Boot Jpa 已經幫我們實現了分頁的功能,在查詢的方法中,需要傳入引數Pageable,當查詢中有多個引數的時候Pageable建議做為最後一個引數傳入.

Page<User> findALL(Pageable pageable);
Page<User> findByUserName(String userName,Pageable pageable);

Pageable是 Spring 封裝的分頁實現類,使用的時候需要傳入頁數、每頁條數和排序規則

@Test
public void testPageQuery() throws Exception {
	int page=1,size=10;
	Sort sort = new Sort(Direction.DESC, "id");
    Pageable pageable = new PageRequest(page, size, sort);
    userRepository.findALL(pageable);
    userRepository.findByUserName("testName", pageable);
}

限制查詢

有時候我們只需要查詢前N個元素,或者支取前一個實體。

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

自定義SQL查詢

其實 Spring Data 覺大部分的 SQL 都可以根據方法名定義的方式來實現,但是由於某些原因我們想使用自定義的 SQL 來查詢,Spring Data 也是完美支援的;在 SQL 的查詢方法上面使用@Query註解,如涉及到刪除和修改在需要加上@Modifying.也可以根據需要新增@Transactional對事物的支援,查詢超時的設定等。

@Modifying
@Query("update User u set u.userName = ?1 where u.id = ?2")
int modifyByIdAndUserId(String  userName, Long id);
	
@Transactional
@Modifying
@Query("delete from User where id = ?1")
void deleteByUserId(Long id);
  
@Transactional(timeout = 10)
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);

多表查詢

多表查詢 Spring Boot Jpa 中有兩種實現方式,第一種是利用 Hibernate 的級聯查詢來實現,第二種是建立一個結果集的介面來接收連表查詢後的結果,這裡主要第二種方式。

首先需要定義一個結果集的介面類。

public interface HotelSummary {

	City getCity();

	String getName();

	Double getAverageRating();

	default Integer getAverageRatingRounded() {
		return getAverageRating() == null ? null : (int) Math.round(getAverageRating());
	}

}

查詢的方法返回型別設定為新建立的介面

@Query("select h.city as city, h.name as name, avg(r.rating) as averageRating "
		- "from Hotel h left outer join h.reviews r where h.city = ?1 group by h")
Page<HotelSummary> findByCity(City city, Pageable pageable);

@Query("select h.name as name, avg(r.rating) as averageRating "
		- "from Hotel h left outer join h.reviews r  group by h")
Page<HotelSummary> findByCity(Pageable pageable);

使用

Page<HotelSummary> hotels = this.hotelRepository.findByCity(new PageRequest(0, 10, Direction.ASC, "name"));
for(HotelSummary summay:hotels){
		System.out.println("Name" +summay.getName());
	}

在執行中 Spring 會給介面(HotelSummary)自動生產一個代理類來接收返回的結果,程式碼彙總使用getXX的形式來獲取

多資料來源的支援

同源資料庫的多源支援

日常專案中因為使用的分散式開發模式,不同的服務有不同的資料來源,常常需要在一個專案中使用多個數據源,因此需要配置 Spring Boot Jpa 對多資料來源的使用,一般分一下為三步:

  • 1 配置多資料來源
  • 2 不同源的實體類放入不同包路徑
  • 3 宣告不同的包路徑下使用不同的資料來源、事務支援

異構資料庫多源支援

比如我們的專案中,即需要對 mysql 的支援,也需要對 Mongodb 的查詢等。

實體類宣告@Entity關係型資料庫支援型別、宣告@Document為 Mongodb 支援型別,不同的資料來源使用不同的實體就可以了

interface PersonRepository extends Repository<Person, Long> {
 …
}

@Entity
public class Person {
  …
}

interface UserRepository extends Repository<User, Long> {
 …
}

@Document
public class User {
  …
}

但是,如果 User 使用者既使用 Mysql 也使用 Mongodb 呢,也可以做混合使用

interface JpaPersonRepository extends Repository<Person, Long> {
 …
}

interface MongoDBPersonRepository extends Repository<Person, Long> {
 …
}

@Entity
@Document
public class Person {
  …
}

也可以通過對不同的包路徑進行宣告,比如 A 包路徑下使用 mysql,B 包路徑下使用 MongoDB

@EnableJpaRepositories(basePackages = "com.neo.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.neo.repositories.mongo")
interface Configuration { }

其它

使用列舉

使用列舉的時候,我們希望資料庫中儲存的是列舉對應的 String 型別,而不是列舉的索引值,需要在屬性上面新增@Enumerated(EnumType.STRING)註解

@Enumerated(EnumType.STRING) 
@Column(nullable = true)
private UserType type;

不需要和資料庫對映的屬性

正常情況下我們在實體類上加入註解@Entity,就會讓實體類和表相關連如果其中某個屬性我們不需要和資料庫來關聯只是在展示的時候做計算,只需要加上@Transient屬性既可。

@Transient
private String  userName;

https://www.cnblogs.com/ityouknow/p/5891443.html

I. 環境準備

在開始之前,當然得先準備好基礎環境,如安裝測試使用mysql,建立SpringBoot專案工程,設定好配置資訊等,關於搭建專案的詳情可以參考前一篇文章

下面簡單的看一下演示新增記錄的過程中,需要的配置

1. 表準備

沿用前一篇的表,結構如下

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '使用者名稱',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '錢',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 專案配置

配置資訊,與之前有一點點區別,我們新增了更詳細的日誌列印;本篇主要目標集中在新增記錄的使用姿勢,對於配置說明,後面單獨進行說明

## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
## jpa相關配置
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

3. 資料準備

資料修改嘛,所以我們先向表裡面插入兩條資料,用於後面的操作

INSERT INTO `money` (`id`, `name`, `money`, `is_deleted`, `create_at`, `update_at`)
VALUES
	(21, 'jpa 修改->一灰灰', 1212, 0, '2019-06-22 21:41:13', '2019-06-22 21:41:13'),
	(22, 'jpa 修改->一灰灰', 6666, 0, '2019-06-22 21:41:13', '2019-06-22 21:41:13');

II. Update使用教程

下面開始進入正題,為方便初看的同學(沒空或者沒興趣瞅前面幾個博文的同學)會有部分內容和前面的博文相同,看過的請無視

1. 表關聯POJO

前面插入篇已經介紹了POJO的逐步建立過程,已經對應的註解含義,下面直接貼出成果

@Data
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")

    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "money")
    private Long money;

    @Column(name = "is_deleted")
    private Byte isDeleted;

    @Column(name = "create_at")
    @CreatedDate
    private Timestamp createAt;

    @Column(name = "update_at")
    @CreatedDate
    private Timestamp updateAt;
}

上面類中的幾個註解,說明如下

  • @Data屬於lombok註解,與jpa無關,自動生成getter/setter/equals/hashcode/tostring等方法
  • @Entity,@Tablejpa註解,表示這個類與db的表關聯,具體匹配的是表money
  • @Id@GeneratedValue作用與自增主鍵
  • @Column表明這個屬性與表中的某列對應
  • @CreateDate根據當前時間來生成預設的時間戳

2. Repository API宣告

接下來我們新建一個api繼承自CurdRepository,然後通過這個api來與資料庫打交道

public interface MoneyUpdateRepository extends CrudRepository<MoneyPO, Integer> {
}

3. 使用姿勢

a. save

在前面一篇插入博文中,我們知道當POJO的id存在時,呼叫save方法可能有兩種情況

  • 若db中這個id對應的欄位不存在,則插入
  • 若db中這個id對應的欄位存在,則更新

我們來試一下更新的效果,下面的程式碼演示了兩塊,一個是當po中的所有成員值有效,更新其中的一個時,會怎樣;另外一個演示的是部分更新時會怎樣(name為空,表示我不希望更新name)

public void simpleUpdateById() {
    MoneyPO record = moneyUpdateRepository.findById(21).get();
    // 直接修改這個record的內容
    record.setMoney(3333L);
    moneyUpdateRepository.save(record);

    record = moneyUpdateRepository.findById(21).get();
    System.out.println("after updateMoney record: " + record);


    record.setName(null);
    record.setMoney(6666L);
    moneyUpdateRepository.save(record);
    
    record = moneyUpdateRepository.findById(21).get();
    System.out.println("after updateMoney record: " + record);
}

輸出結果發現,前面執行成功,後面執行失敗

上面為第一個執行結果,從拼接的sql可以知道,是全量的修改;輸出結果也如我們預期

後面將name設定為空之後,再次更新,發現丟擲異常,如下,這個是因為我們的db限制,欄位不允許有null的存在

從拼接的sql上看,我們知道是因為每個成員都作為了update sql家族的一員,在insert這一篇中我們也遇到了類似的問題,當時是在POJO上添加註解@DynamicInsert,根據實際需要選擇插入,那麼在更新這裡是不是也有類似的註解呢

@Data
@DynamicUpdate
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
}

在pojo上添加註解@DynamicUpdate之後,再次進行嘗試,結果如下

居然還是失敗了,從輸出的sql來看,居然把namemoney都當成了sql的一部分,難道是因為我們呼叫了setter方法的原因麼。待著猜測,再來一次

MoneyPO toUpdate = new MoneyPO();
toUpdate.setId(21);
toUpdate.setMoney(6666L);
moneyUpdateRepository.save(toUpdate);
record = moneyUpdateRepository.findById(21).get();
System.out.println("after updateMoney record: " + record);

輸出結果如下,看來我們上面的猜測並不對,拼接sql應該是根據哪個欄位發生了變化,就把哪個做為sql的一部分來做的

上面這個使用姿勢看完之後,會有一個明顯的感覺,這個更新的支援,必須先獲取目標物件再進行修改,很難滿足我們的日常業務場景;

b. 查詢更新

根據某個條件來更新對應的資料,這個就比較常見了,在jpa中,沒有找到根據方法名來支撐這種場景的方式,但是發現了另外一個有意思的東西--jql

直接在方法方面,添加註解,註解內部寫sql

/**
 * 根據金錢來修改狀態
 *
 * @param money
 * @param state
 */
@Modifying
@Query("update MoneyPO m set m.isDeleted=?2 where  m.money=?1")
void updateStateByMoney(Long money, Byte state);

/**
 * 表示式計算
 *
 * @param id
 * @param money
 */
@Modifying
@Query("update MoneyPO m set m.money=m.money + ?2 where m.id=?1")
void addMoneyById(Integer id, Long money);

上面就是一個查詢更新的case,注意兩個註解

  • @Modifying這個必須有,告訴框架我們執行的是更新/刪除操作
  • @Query內部是正常的sql語句,但是需要注意的是表名,不是實際的表,而是我們前面定義的POJO

然後來測試一下使用

public void updateByQuery() {
    // 通過查詢修改
    moneyUpdateRepository.updateStateByMoney(6666L, (byte) 0x01);

    MoneyPO record = moneyUpdateRepository.findById(21).get();
    System.out.println("after update record: " + record);


    moneyUpdateRepository.addMoneyById(21, 3333L);
    record = moneyUpdateRepository.findById(21).get();
    System.out.println("after addMoney record: " + record);
}

執行上面的程式碼,悲催的發現居然報錯了Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query

從堆疊的描述上來看,更新/刪除必須要開啟事務,那麼事務是什麼東西呢?下面推薦幾篇博文

關於jpa中更多事務相關的,後面再引入,我們先回到本文主題,如何解決問題: 在呼叫上面的方法上面新增事務註解即可

@Transactional
public void testUpdate() {
    simpleUpdateById();
    updateByQuery();
}

再次執行之後,結果如下

看上面的結果,發現money+3333之後的輸出結果居然還是6666;但是我們再看db的結果,已經是9999了,為什麼會這樣呢?

上面這個問題比較有意思了,初步猜測與事物已經jpa的內部快取機制有關係,至於具體是不是這樣,有必要專門開坑來驗證一二

4. 小結

利用JPA實現表資料的更新,上面主要介紹了兩種方式, save + jql

save

通過save更新時,需要指定id來實現單條記錄的修改

jql

語法與sql差不多,配合兩個註解@Modifying,@Query來使用,下面是一個例項,兩點需要注意

  • 表名是我們定義的與db中表關聯的POJO
  • 引數傳遞格式為?index, index為引數位置
@Modifying
@Query("update MoneyPO m set m.isDeleted=?2 where  m.money=?1")
void updateStateByMoney(Long money, Byte state);

資料修改和刪除,需要顯示宣告事務,否則會報錯,一個是在呼叫的方法上添加註解@Transactional,或者直接在repository api的介面上添加註解@Transactional