jpa是ORM映射框架,更多詳情,請戳:apring-data-jpa官網:http://spring.io/projects/spring-data-jpa,以及一篇優秀的博客:http://www.javashuo.com/article/p-sxgnhhih-s.html,這裏只是記錄項目實現。html
jpa查詢方法大體可分爲JPA命名查詢,@Query查詢,EntityManager對象查詢java
JPA命名查詢mysql
interface PersonRepositoryextendsRepository<User, Long>{ List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress,String lastname); // 去重查詢 List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname,String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname,String firstname) // 忽略大小寫 List<Person> findByLastnameIgnoreCase(String lastname); List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname,String firstname); // 排序查詢 List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname);} }
使用Top和First限制查詢的結果大小
User findFirstByOrderByLastnameAsc(); User findTopByOrderByAgeDesc(); Page<User> queryFirst10ByLastname(String lastname,Pageable pageable); Slice<User> findTop3ByLastname(String lastname,Pageable pageable); List<User> findFirst10ByLastname(String lastname,Sort sort); List<User> findTop10ByLastname(String lastname,Pageable pageable);
統計查詢git
interface UserRepositoryextendsCrudRepository<User,Long>{ long countByLastname(String lastname); }
分頁查詢
Page<User>findAll(PageRequest.of(1,20));
下表描述了JPA支持的關鍵字以及包含該關鍵字的JPA命名查詢方法:github
關鍵字spring |
示例sql |
SQL數據庫 |
And緩存 |
findByLastnameAndFirstnamespringboot |
… where x.lastname = ?1 and x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
… where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals |
findByFirstname,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<Age> ages) |
… where x.age in ?1 |
NotIn |
findByAgeNotIn(Collection<Age> ages) |
… 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) |
@Query查詢
普通查詢
public interface UserRepositoryextendsJpaRepository<User,Long>{ @Query(value ="SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery =true) User findByEmailAddress(String emailAddress); }
排序查詢
public interface UserRepositoryextendsJpaRepository<User, Long>{ @Query("select u from User u where u.lastname like ?1%") List<User> findByAndSort(String lastname,Sort sort); }
分頁查詢
public interface UserRepositoryextendsJpaRepository<User,Long>{ @Query(value ="SELECT * FROM USERS WHERE LASTNAME = ?1", countQuery ="SELECT count(*) FROM USERS WHERE LASTNAME = ?1", nativeQuery =true) Page<User> findByLastname(String lastname,Pageable pageable); }
使用命名參數
public interface UserRepositoryextendsJpaRepository<User,Long>{ @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname") User findByLastnameOrFirstname(@Param("lastname")String lastname,@Param("firstname")String firstname); }
刪除,若是但願自動清除EntityManager,能夠將@Modifying註釋的clearautomatic屬性設置爲true。
interface UserRepositoryextendsRepository<User, Long> { @Modifying @Transactional @Query("delete from User u where user.role.id = ?1") void deleteInBulkByRoleId(long roleId); }
EntityManager對象查詢
@PersistenceContext private EntityManager em; private void pageTest() { //SQL String sql = "select * from tb_user t where t.username like :name"; //設置SQL、映射實體,以及設置值,返回一個Query對象 Query query = em.createNativeQuery(sql, TbUser.class).setParameter("name", "huanzi%"); //分頁、排序信息,並設置,page從0開始 PageRequest pageRequest = PageRequest.of(0, 10, new Sort(Sort.Direction.ASC, "id")); query.setFirstResult((int) pageRequest.getOffset()); query.setMaxResults(pageRequest.getPageSize()); //獲取分頁結果 Page page = PageableExecutionUtils.getPage(query.getResultList(), pageRequest, () -> { //設置countQuerySQL語句 Query countQuery = em.createNativeQuery("select count(1) from ( " + ((NativeQueryImpl) query).getQueryString() + " ) count_table"); //設置countQuerySQL參數 query.getParameters().forEach(parameter -> countQuery.setParameter(parameter.getName(), query.getParameterValue(parameter.getName()))); //返回一個總數 return Long.valueOf(countQuery.getResultList().get(0).toString()); }); //組裝返回值 //總數 long total = page.getTotalElements(); System.out.println(total); //分頁信息 Pageable pageable = page.getPageable(); System.out.println(pageable); //數據集合 List<TbUser> content = page.getContent(); System.out.println(content.size()); System.out.println(content); }
maven引包
<!--添加springdata-jpa依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--添加MySQL驅動依賴 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--lombok插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
applicaction.yml
#注意:在yml文件中添加value值時,value前面須要加一個空格 #2.0.0的配置切換爲servlet.path而不是"-" server: port: 10086 #端口號 servlet: context-path: /springboot #訪問根路徑 spring: thymeleaf: cache: false #關閉頁面緩存 prefix: classpath:/view/ #thymeleaf訪問根路徑 mode: LEGACYHTML5 datasource: #數據庫相關 url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver jpa: show-sql: true mvc: date-format: yyyy-MM-dd HH:mm:ss #mvc接收參數時對日期進行格式化 jackson: date-format: yyyy-MM-dd HH:mm:ss #jackson對響應回去的日期參數進行格式化 time-zone: GMT+8
實體類與表數據
tb_user
tb_description
/** * 用戶類 */ @Entity @Table(name = "tb_user") @Data public class User implements Serializable { @Id @GeneratedValue(strategy= GenerationType.IDENTITY) //IDENTITY 自增 private Integer id; @Column(name = "username")//命名相同或駝峯標識(與數據庫下劃線映射)能夠不用寫 private String username; private String password; private Date created; private String descriptionId; @OneToOne @JoinColumn(name = "descriptionId",referencedColumnName = "id", insertable = false, updatable = false) @NotFound(action= NotFoundAction.IGNORE) //用戶描述信息 private Description description; }
/** * 用戶描述類 */ @Entity @Table(name = "tb_description") @Data public class Description implements Serializable { @Id @GeneratedValue(strategy= GenerationType.IDENTITY) //IDENTITY 自增 private Integer id; private String userId; private String description; }
通信對象
/** * 統一返回對象 */ @Data public class Result<T> implements Serializable { /** * 通訊數據 */ private T data; /** * 通訊狀態 */ private boolean flag = true; /** * 通訊描述 */ private String msg = ""; /** * 經過靜態方法獲取實例 */ public static <T> Result<T> of(T data) { return new Result<>(data); } public static <T> Result<T> of(T data, boolean flag) { return new Result<>(data, flag); } public static <T> Result<T> of(T data, boolean flag, String msg) { return new Result<>(data, flag, msg); } @Deprecated public Result() { } private Result(T data) { this.data = data; } private Result(T data, boolean flag) { this.data = data; this.flag = flag; } private Result(T data, boolean flag, String msg) { this.data = data; this.flag = flag; this.msg = msg; } }
分頁對象
/** * 分頁對象(參考JqGrid插件) */ @Data public class PageInfo<M> { private int page;//當前頁碼 private int pageSize;//頁面大小 private String sidx;//排序字段 private String sord;//排序方式 private List<M> rows;//分頁結果 private int records;//總記錄數 private int total;//總頁數 /** * 獲取統一分頁對象 */ public static <M> PageInfo<M> of(Page page, Class<M> entityModelClass) { int records = (int) page.getTotalElements(); int pageSize = page.getSize(); int total = records % pageSize == 0 ? records / pageSize : records / pageSize + 1; PageInfo<M> pageInfo = new PageInfo<>(); pageInfo.setPage(page.getNumber() + 1);//頁碼 pageInfo.setPageSize(pageSize);//頁面大小 pageInfo.setRows(CopyUtil.copyList(page.getContent(), entityModelClass));//分頁結果 pageInfo.setRecords(records);//總記錄數 pageInfo.setTotal(total);//總頁數 return pageInfo; } /** * 獲取JPA的分頁對象 */ public static Page readPage(Query query, Pageable pageable, Query countQuery) { if (pageable.isPaged()) { query.setFirstResult((int) pageable.getOffset()); query.setMaxResults(pageable.getPageSize()); } return PageableExecutionUtils.getPage(query.getResultList(), pageable, () -> executeCountQuery(countQuery)); } private static Long executeCountQuery(Query countQuery) { Assert.notNull(countQuery, "TypedQuery must not be null!"); List<Number> totals = countQuery.getResultList(); Long total = 0L; for (Number number : totals) { if (number != null) { total += number.longValue(); } } return total; } }
/** * 分頁條件(參考JqGrid插件) */ @Data public class PageCondition { private int page = 1;//當前頁碼 private int rows = 10;//頁面大小 private String sidx;//排序字段 private String sord;//排序方式 /** * 獲取JPA的分頁查詢對象 */ public Pageable getPageable() { //處理非法頁碼 if (page < 0) { page = 1; } //處理非法頁面大小 if (rows < 0) { rows = 10; } return PageRequest.of(page - 1, rows); } }
2019-09-27補充:以前的這個分頁信息對象不是很好,如今更新一下
一、int改爲Integer
二、非法參數處理增強
三、使用@JsonInclude註解,減小數據傳輸,例如:
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.util.StringUtils; /** * 分頁條件(參考JqGrid插件) */ @Data //當屬性的值爲空(null或者"")時,不進行序列化,能夠減小數據傳輸 @JsonInclude(JsonInclude.Include.NON_EMPTY) public class PageCondition { private Integer page;//當前頁碼 private Integer rows;//頁面大小 private String sidx;//排序字段 private String sord;//排序方式 /** * 獲取JPA的分頁查詢對象 */ @JsonIgnore public Pageable getPageable() { //處理非法頁碼 if (StringUtils.isEmpty(page) || page < 0) { page = 1; } //處理非法頁面大小 if (StringUtils.isEmpty(rows) || rows < 0) { rows = 10; } return PageRequest.of(page - 1, rows); } }
UserController
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping("/getAllUser") public ModelAndView getAllUser(){ Result result=userService.getAllUser(); ModelAndView mv=new ModelAndView(); mv.addObject("userList",result.getData()); mv.setViewName("index.html"); return mv; } @RequestMapping("page") public Result<PageInfo<User>> page(User entity, PageCondition pageCondition) { return userService.page(entity,pageCondition); } @RequestMapping("list") public Result<List<User>> list(User entity) { return userService.list(entity); } @RequestMapping("get/{id}") public Result<User> get(@PathVariable("id") Integer id) { return userService.get(id); } @RequestMapping("save") public Result<User> save(User entity) { return userService.save(entity); } @RequestMapping("delete/{id}") public Result<Integer> delete(@PathVariable("id") Integer id){ return userService.delete(id); } }
UserService
public interface UserService{ Result<PageInfo<User>> page(User entity, PageCondition pageCondition); Result<List<User>> list(User entity); Result<User> get(Integer id); Result<User> save(User entity); Result<Integer> delete(Integer id); Result getAllUser(); }
@Service
@Transactional public class UserServiceImpl implements UserService { @Autowired private UserRepository userRepository; @Override public Result<PageInfo<User>> page(User entity ,PageCondition pageCondition) { Page<User> page = userRepository.findAll(Example.of(CopyUtil.copy(entity, User.class)), pageCondition.getPageable()); int records = (int) page.getTotalElements(); int pageSize = page.getSize(); int total = records % pageSize == 0 ? records / pageSize : records / pageSize + 1; PageInfo<User> pageInfo = new PageInfo<>(); pageInfo.setPage(page.getNumber() + 1);//頁碼 pageInfo.setPageSize(pageSize);//頁面大小 pageInfo.setRows(page.getContent());//分頁結果 pageInfo.setRecords(records);//總記錄數 pageInfo.setTotal(total);//總頁數 return Result.of(pageInfo); } @Override public Result<List<User>> list(User entity) { List<User> entityList = userRepository.findAll(Example.of(entity)); return Result.of(entityList); } @Override public Result<User> get(Integer id) { Optional<User> optionalE = userRepository.findById(id); if (!optionalE.isPresent()) { throw new RuntimeException("ID不存在!"); } return Result.of(optionalE.get()); } @Override public Result<User> save(User entity) { User user = userRepository.save(entity); return Result.of(user); } @Override public Result<Integer> delete(Integer id) { userRepository.deleteById(id); return Result.of(id); } @Override public Result getAllUser() { List<User> userList = userRepository.getAllUser(); if(userList != null && userList.size()>0){ return Result.of(userList); }else { return Result.of(userList,false,"獲取失敗!"); } } }
UserRepository
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> { @Query(value = "from User") //HQL // @Query(value = "select * from tb_user",nativeQuery = true)//原生SQL List<User> getAllUser(); }
get接口
http://localhost:10086/springboot/user/get/1
list接口
http://localhost:10086/springboot/user/list
http://localhost:10086/springboot/user/list?username=張三
page接口
http://localhost:10086/springboot/user/page?page=1&rows=10
http://localhost:10086/springboot/user/page?page=1&rows=10&username=張三
save接口(插入跟更新)
沒有id或id不存在,爲插入,http://localhost:10086/springboot/user/save?username=張麻子&password=123
id已存在,則爲更新,注意:這裏的更新是你的字段是什麼jpa就幫你存什麼,若是想要實現只更新接參對象有值的字段,應該先用id去同步數據,再更新,http://localhost:10086/springboot/user/save?id=1&username=張三1&password=666
delete接口
http://localhost:10086/springboot/user/delete/6
自定義Dao層方法
http://localhost:10086/springboot/user/getAllUser
JPA牛逼!
注意:
<!--繼承信息--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> </parent>
<!--繼承信息--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath/> </parent>
2.0以後是大版本升級,使用了1.8JDK,許多方法API都發生了改變,其中就碰到了一個坑,以前寫得不規範,Service中已經@Transactional了一次,Repository的自定義SQL又@Transactional一次,當咱們service直接調公用JPA方法,後面又調咱們自定義的Dao層方法時,因爲事務傳播,後面的事務不進行提交,並且也不報錯
有同窗發現咱們少貼了部分代碼,在這裏補充一下,CopyUtil類是咱們自定義的一個實體類型轉換的工具類,用於將實體模型與實體的轉換,在 SpringBoot系列——Spring-Data-JPA(升級版)中,咱們已經對該工具類進行了升級,升級以後支持複雜對象的轉換
/** * 實體類型轉換的工具類 */ public class CopyUtil { /** * 類型轉換:實體模型<->實體 * <p> * 例如:List<DataModel> <--> List<Data> */ public static <T> List<T> copyList(List list, Class<T> target) { List<T> newList = new ArrayList<>(); for (Object entity : list) { newList.add(copy(entity, target)); } return newList; } /** * 類型轉換:實體模型 <->實體 * <p> * 例如:DataModel <--> Data */ public static <T> T copy(Object origin, Class<T> target) { T t = null; try { t = target.newInstance(); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } BeanUtils.copyProperties(origin, t); return t; } }
2019-06-21補充:jpa有個坑,默認狀況下(數據庫是oracle),調用super.方法,事務會排在最後提交,所以形成我update的數據後面又被super的事務update回來
(PS:若是你看到這裏,就要注意正確使用super.方法,super.方法應該要放在最後調用,避免入坑,事實上,關於jpa的事務管理還有不少坑,這個咱們之後再好好聊)
例如:
//先調用父類的保存方法 super.save(); //執行自定義update語句 my.update();
雖然代碼先執行super.save(),但這個保存事務會在最後執行提交
咱們期待的事務順序應該是這樣
最後我是直接使用在service層注入dao層,直接調用dao.save(),事務處理就與咱們期待的同樣了
可能有些同窗看到會有些疑問,看到我兩個操做都有操做同一個表,爲何不合併成一次操做呢,直接修改對象值,調用super.save()方法進行保存不就好了嗎,還要直接寫update?? 這是由於save方法咱們進行了特殊處理,jpa原生save方法是傳入的對象的屬性值是什麼就幫咱們保存什麼,這並不符合咱們的常規保存操做,我傳入一個對象,裏面哪些屬性有值就幫咱們保存哪些值,而咱們的自定義update恰好就是有設置某個字段的值爲空,調用父類的save方法不會幫咱們設置,因此才須要單獨寫一個update
2019-10-24補充:客戶忽然反饋,網站訪問出現訪問響應時間過久、進而崩潰的狀況,查看日誌發現是數據庫鏈接超時,仔細一看執行的SQL不對勁
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30014ms.
select * from super_search_ucid where 1 = 1 and game_id = '6' AND 7645=IF((ORD(MID((SELECT DISTINCT(IFNULL(CAST(grantee AS CHAR),CHAR(32))) FROM information_schema.USER_PRIVILEGES LIMIT 0,1),2,1)) > 3008),SLEEP(5),7645) AND 'HhsP'='HhsP' and role_list like '%SP琉%' and props_list like '%MR琉璃%' and ( division like '%官服安卓IOS通用%' )
這個SQL被SQL注入攻擊,這個就是對方注入進來的字符串
6' AND 7645=IF((ORD(MID((SELECT DISTINCT(IFNULL(CAST(grantee AS CHAR),CHAR(32))) FROM information_schema.USER_PRIVILEGES LIMIT 0,1),2,1)) > 3008),SLEEP(5),7645) AND 'HhsP'='HhsP
他在瘋狂的查詢信息庫的用戶權限表,企圖搞事情
經實測,這個SQL在生產上執行耗時一百多秒都沒響應,致使數據庫鏈接池的鏈接線程一直被佔用,沒有空閒的能夠被調用致使鏈接超時
這個接口是查詢接口,咱們在Security的配置中配置的是無需登陸就能夠訪問,並且因爲須要分頁,同時還須要等值查詢等各類操做,因此我採用的是文章前面提到的 EntityManager對象查詢 方法,動態拼接SQL,並且當時並無想到須要防範SQL注入,這才落下了那麼大的一個坑,所幸並無形成重大影響
防範、解決:
一、動態拼接的SQL必定要記得作轉義
/** * sql轉義 */ public static String escapeSql(String str) { if (str == null) { return null; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char src = str.charAt(i); switch (src) { case '\'': sb.append("''");// hibernate轉義多個單引號必須用兩個單引號 break; case '\"': case '\\': sb.append('\\'); default: sb.append(src); break; } } return sb.toString(); }
二、切記不要再使用select * 了,一個是查詢耗時久,一個是若是不當心被SQL注入還容易泄露數據,能夠參考以前寫的博客《利用反射跟自定義註解拼接實體對象的查詢SQL》,拼接全字段查詢語句
代碼已經開源、託管到個人GitHub、碼雲: