SpringDataJPA
Spring Data JPA
干啥的呢
它是spring data 项目的一个子模块,spring data 用于标准化持久层代码,减化持久层代码,屏蔽持久层技术上的差异
咋安装呢
<dependencies> <dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>
有上面的依赖就可以了
怎么用呢
使用@EnableJpaRepositories修饰一个类,默认扫描这个在所在的包和子包,默认生成Query的 策略是 CREATE_IF_NOT_FOUND(优先查找Qeury),没有 就通过方法名字生成Query
所以接口方法的名字是有一定规则的
The mechanism strips the prefixes
find…By
,read…By
,query…By
,count…By
, andget…By
from the method and starts parsing the rest of it. The introducing clause can contain further expressions, such as aDistinct
to set a distinct flag on the query to be created
就是先去掉一些固定的前缀(find,get,query,count,distinct...),(distinct不知道是通过主键支重还是通过sql语句去重,需要测试),然后解析后面的部分,解析中可以支持这此操作符 and,or,between,lessthan,greaterthan,like,orderby,asc,desc等等,其他的还可能根据你作用的数据库不同而不同,(这特么就是要把java翻译一下,幸好编程不针对汉字)
interface PersonRepository extends Repository<Person, Long> { List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
方法名和参数的解析,有意思
假如我们创建如下的查询:findByUserDepUuid(),框架在解析该方法时,首先剔除findBy,然后对剩下的属性进行解析,假设查询实体为Doc。
先判断userDepUuid (根据POJO(Plain Ordinary Java Object简单java对象,实际就是普通java bean规范),首字母变为小写。)是否是查询实体的一个属性,如果是根据该属性进行查询;如果没有该属性,继续第二步。
从右往左截取第一个大写字母开头的字符串(此处为Uuid),然后检查剩下的字符串是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后假设 user为查询实体的一个属性。
接着处理剩下部分(DepUuid),先判断user所对应的类型是否有depUuid属性,如果有,则表示该方法最终是根据 “Doc.user.depUuid” 的取值进行查询;否则继续按照步骤 2的规则从右往左截取,最终表示根据“Doc.user.dep.uuid” 的值进行查询。
可能会存在一种特殊情况,比如 Doc包含一个user的属性,也有一个 userDep 属性,此时会存在混淆。可以明确在属性之间加上 "_"以显式表达意图,比如"findByUser_DepUuid()" 或者"findByUserDep_uuid()"。(但是官方推荐驼峰方式)
Property expressions can refer only to a direct property of the managed entity, as shown in the preceding example
脑瓜子..
分页
Page<User> findByLastname(String lastname, Pageable pageable);Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
分页参数,排队参数不能为空,如是不排序可以用
Sort.unsorted()
andPageable.unpaged()
.
Sort sort = Sort.by("firstname").ascending() .and(Sort.by("lastname").descending());
TypedSort<Person> person = Sort.sort(Person.class);TypedSort<Person> sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
自定义接口实现
https://www.cnblogs.com/JackQiang/p/13188414.html
在大多数的情况瞎,这两种方式是可以满足我们的需求,但是如果存在更加复杂的需要呢?可能在这个时候我们会吐槽没有mybaties那么灵活了,在Jpa中也可以实现的,我们可以通过自定义的方式来进行实现
- 创建自定义的接口
public interface BookCustomRepository {
/**
* 查询所有书列表
* @return
*/
List<Book> listBook();}
- 自定接口实现
@Repositorypublic class BookCustomRepositoryImpl implements BookCustomRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public List<Book> listBook() {
//在这里就可以自由发挥了,可以根据自己需求来实现
String sql = "select * from book";
BeanPropertyRowMapper<Book> rowMapper = new BeanPropertyRowMapper<>(Book.class);
List<Book> bookList = jdbcTemplate.query(sql, rowMapper);
return bookList;
}
}
官方文档有这么一句话:
The most important part of the class name that corresponds to the fragment interface is the
Impl
postfix.
你品,你细品,而且这个后缀是可以自定义的,通过EnableJpaRepositories
中的一个属性EnableJpaRepositories
- 继承自定接口
/*** @Description:
* @Author: JackQ
* @CreateDate: 2020/6/22 11:06
*/
@Repository
public interface BookRepository extends org.springframework.data.repository.Repository<Book,Long>, BookCustomRepository {
/**
* 通过书名查找书
*
* @param name
* @return
*/
Book findBookByName(String name);
/**
* 更新名称和作者信息
* @param name
* @param author
* @param id
*/
@Transactional(rollbackFor = Exception.class)
@Modifying
@Query("update Book set name = ?1,author = ?2 where id = ?3")
void updateNameAndAuthorById(String name,String author,long id);
}
- 测试,在其他的地方就可以调用自定方法中的实现了
@Test public void listBooks(){
List<Book> books = bookService.listBooks();
books.stream().forEach(book -> System.out.println(book.toString()));
}
//结果
//Book(id=1, name=平凡的世界, price=30.2, author=路遥)
//Book(id=2, name=人生, price=60.0, author=路遥)
//Book(id=3, name=情书, price=38.0, author=岩井俊二)
自定义实现优先级别
在一个repository中可能存在继承多个接口,那么就可能存在相同的实现方法;自定义 implementations 的优先级高于基本 implementation 和 repository 方面。如果两个片段提供相同的方法签名,则此 规则 允许您覆盖 base repository 和 aspect 方法并解决歧义。 Repository 片段不限于在单个 repository 接口中使用。多个 repositories 可以使用片段接口,让您可以跨不同的 repositories 重用自定义。
interface CustomizedSave<T> { <S extends T> S save(S entity);
}
class CustomizedSaveImpl<T> implements CustomizedSave<T> {
public <S extends T> S save(S entity) {
// Your custom implementation
}
}
自定义BaseRepository
- 先自定义一个
class MyRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
// 一定要调用super()
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
然后
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
与MVC 的融合 (这个真牛)
@Configuration@EnableWebMvc
@EnableSpringDataWebSupport // 加入这个配置
class WebConfiguration {}
这就配置完了,然后他可惜在请求过来后通过请求参数和controller方法参数类型直接查数据
@Controller@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}") // 请求中只传了id,但你等到的是这个user的所有数据
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
有一个前提是你的环境中一定要实现
CrudRepository
才行
但要是这么用的话,可能后接手的没有jpa+mvc 经验的人可能会跳楼吧
这是分割线,以上都是spring-data的通用玩法
下面是jpa 特有的玩法
配置
@Configuration@EnableJpaRepositories
@EnableTransactionManagement
class ApplicationConfig {
// 数据源
// TODO 这里应该可以使用自己实现的动态数据源
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
// 这里需要创建持久化提供者,用的是hibernate
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.acme.domain"); // 需扫描entity的包
factory.setDataSource(dataSource());
return factory;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
保存Entites
CrudRepository.save(…)
这个方法会自动判断是insert,还是update,是通过entityManager.persist() 和entityManager.merge()来完成
判断Strategies
- 如果entity有没有id (默认),那么认为是一个新的entity,否则认为不是一个新的
- 如是一个entity implenments
Persistable
接口,会通过调用isNew来判断 - 另一个官方说是很少用的方式是,不写了,可能写很多代码
查询方法
1. 通过方法名生成查询
public interface UserRepository extends Repository<User, Long> { List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
生成的语句 select u from User u where u.emailAddress = ?1 and u.lastname = ?2
(这应该是jpql),
下面举个大例
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
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 ages)
… where x.age in ?1
NotIn
findByAgeNotIn(Collection 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)
2. 使用注解NamedNativeQuery,NamedQeury
@NamedNativeQuery
写sql
@NamedQeury
写jpql(Java Persistence query language)
在数据类上
@Entity@NamedQuery(name = "User.findByEmailAddress",
query = "select u from User u where u.emailAddress = ?1")
public class User {
}
在Repository类虽定义这个方法
public interface UserRepository extends JpaRepository<User, Long> { List<User> findByLastname(String lastname);
User findByEmailAddress(String emailAddress);
}
这时候在调用UserRepository.finxByEmailAddress
时会执行注解中的jpql ,而不是按照方法生成,?1
代表第一个参数
这种方式适合小规模使用
使用@query
这个注解用在Repository接口定义的方法上,这个注解的优先级高于Entity上的注解和接口方法生成的jpql
public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.emailAddress like %?1")
User findByEmailAddress(String emailAddress);
}
像例子中,可以使用like
支持原生sql
public interface UserRepository extends JpaRepository<User, Long> { @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
}
分页支持
public interface UserRepository extends JpaRepository<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);
}
原生sql 不支持动态生成countsql,所以一定要自己写
排队支持
public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.lastname like ?1%")
List<User> findByAndSort(String lastname, Sort sort);
@Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}
repo.findByAndSort("lannister", new Sort("firstname"));
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)"));
repo.findByAsArrayAndSort("bolton", new Sort("fn_len"));
具名参数支持
默认是使用 参数位置到入参的,也支持通过参数名入参,使用@param注解
public interface UserRepository extends JpaRepository<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);
}
如果 编译时带有 -parameters 参数,可以使用参数名,不使用@param,需要java8以上
在查询语句中可以使用sqel
@Entitypublic class User {
@Id
@GeneratedValue
Long id;
String lastname;
}
public interface UserRepository extends JpaRepository<User,Long> {
@Query("select u from #{#entityName} u where u.lastname = ?1")
List<User> findByLastname(String lastname);
}
这里的 #{#entityName} 代表@Entity定义的名字,如果没有定义,就使用类简名
这个功能有两个作用,1. 类名变了,重构了,这里也会根着变,2. 在定义一些通用方法时,不知道子类的 domain 类的类型
下面这个例子使用方法参数
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
principal.emailAddress 先不用管
对like的支持
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
因为like参数可能不安全,所以spel表达式提供了 清洗功能
@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")List<User> findContainingEscaped(String namePart);
escapeCharacter方法返回数据库的通配符,一般是_和%
更新
@Modifying@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
官网对说,只有@Query注解更新需要@Modifying 注解因为要更新entityManager中的缓存
https://my.oschina.net/u/1762727/blog/2960653
关联查询
这东西可有意思了,我先写一下相关注解
NamedEntityGraphs,NamedEntityGraph,NamedAttributeNode,NamedSubgraph,JoinTable,JoinColumn,ForeignKey,OneToOne,OneToMany,ManyToMany...
OneToOne,OneToMany,ManyToMany
这些都好理解,含义与表的关联关系一样,只不过是使用java注释的方式体现
ManyToMany
它有4个属性,
targetEntity: 可以不写,但是如果使用的是一个窗口,且没有泛型,就要有这个属性了
cascade : 级联关系类型
fetch :拉取关联数据的时期 , LAZY OR EARLY
mappedBy:这个地方写另一方关联自己的属性
因为是多对多关系,一般的两个表之间都会有一个中间表,ManyTyMany 只能表示两类对象的关联关系,但具体的关联方式需要借助
JoinTable,JoinColumn
两注解来描述JoinTable
代表两个实体表的关联表,这个注解的所有属性都不是必填,默认生成sql的规则是 关联外键为两个实体类表名(@Table或者类名)拼接下划线拼接类的id属性名,关联表名为两个table用下划线拼接(如果记不住不要紧,把sql打印出来就可以看见)
属性 作用
name
表名
joinColumns
主表关联列名
inverseJoinColumns
关联表关联列名
foreignKey
外键生成时的属性
inverseForeignKey
外键生成时的属性
uniqueConstraints
生成唯一索引属性
index
生成index属性
后面几个为生成表时使用的属性,主要joinColumns 和inverseJoinColumns不要写错了
JoinColumn
代表关联表的列,比如 一个User 有 多个 Pet ,库中pet表有一个列叫user_id,那么JoinColumn 就写(name="userId")
OneToMany与 ManyToMany一样的用法,它可以与JoinColumn一组合使用
NamedEntityGraphs,NamedEntityGraph,NamedAttributeNode,NamedSubgraph 是用来优化 关联查询的(N+1)问题 ,上面的注解都是先查主对象,后查关联对象,使用这些注解可以让你产生关联查询语句一次性查询关联对象
@Entity
@NamedEntityGraphs(@NamedEntityGraph(name = "user.address", attributeNodes = @NamedAttributeNode(value = "addresses")))
public class User {
@ManyToMany()
@JoinTable(name = "user_address", joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = @JoinColumn(name = "addr_id"))
private Set<Address> addresses;
}
在Repository中
@Query("select u from #{#entityName} u where u.id = ?1")
@EntityGraph(value = "user.address", type = EntityGraph.EntityGraphType.LOAD)
User queryById(Long id);
EntityGraph可以单独使用
@Query("select u from #{#entityName} u where u.id = ?1") @EntityGraph(attributePaths = { "addresses" ,"pets"})
User queryById2(Long id);
addresses和pets代表User类中的两个属性,而且要用Set来代表集合关系,如果用List会出现问题,这个在官方github上有说到
https://github.com/introproventures/graphql-jpa-query/issues/2
https://stackoverflow.com/questions/4334970/hibernate-throws-multiplebagfetchexception-cannot-simultaneously-fetch-multipl
但是 Set 与List是有区别的,这个如果使用jpa 暂时没有好的办法
另一个方法是 在其中一个属性上使用 @OrderColumn(name = "id")
这里的name 指的是 关联表中的一个属性,但是这个方法也存在一定的问题,这会有可能出现重名列,比如两个关联表中都有user_id。
所以请避免这种数据库设计,也不应该出现,这种映射最后得到的数据也是错误的,会有一个表中的数据重复
这也是为什么把List改为Set就可以使用的原因吧(我猜)
查询子集(projection )
先定义一个接口,要获取哪些属性,就写哪些属性的get方法
public interface SubProperty { String getName();
}
再定义查询接口
@Query("select u.name as name from #{#entityName} u where u.id = ?1") SubProperty getName(Long id);
这里有一个特殊的一方,如果使用@Query,那么就要定义别名,要不查回来的数据虽然能查到你想要的结果,但是jpa 不能给你包装在你想要的对象中,因为他把查回来的结果集放在一个map中,key是null ,面不是name
如果你查找的数据复杂或者是多条,也可以在接口中定义其他接口方法,查询接口也可以返回数据集合
除了使用接口形式,还可以使用类,但是使用类不能嵌套,不能代理,而且你能获取哪些属性取决于你暴露的构造方法,而且如果你有两个以上构造方法就会异常,并且还要重写hashCode和equals方法,还不能使用Query
官方推荐使用Lombok ,呵呵,如果不用得改800遍
可以动态的产生子集
interface PersonRepository extends Repository<Person, UUID> { <T> Collection<T> findByLastname(String lastname, Class<T> type);
}
这个挺妙
存储过程(TODO)
官网只介绍了一个 out的情况
@Procedure("plus1inout")Integer pl(Integer arg);
@Procedure("plus1inout")Integer explicitlyNamedPlus1inout(Integer arg);
在网上找到的多个out的解决办法
先定义接口
public interface ProcedureRepository {
ProcedureEntity p2(Long in);
}
再具体实现
@Repositorypublic class ProcedureRepositoryImpl implements ProcedureRepository, org.springframework.data.repository.Repository<ProcedureEntity, Long> {
private final EntityManager entityManager;
public ProcedureRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public ProcedureEntity p2(Long in) {
final String inName = "arg";
final String outName = "res";
final String outName2 = "res2";
StoredProcedureQuery query = entityManager.createStoredProcedureQuery("plus1inout2");
query.registerStoredProcedureParameter(inName, Long.class, ParameterMode.IN);
query.setParameter(inName, in);
query.registerStoredProcedureParameter(outName, Long.class, ParameterMode.OUT);
query.registerStoredProcedureParameter(outName2, Long.class, ParameterMode.OUT);
query.execute();
Long out1 = (Long) query.getOutputParameterValue(outName);
Long out2 = (Long) query.getOutputParameterValue(outName2);
ProcedureEntity entity = new ProcedureEntity();
entity.setRes(out1);
entity.setRes2(out2);
return entity;
}
}
这里是主要调用存储过程的部分,有两个注意的部分 ,1.实现类的名字,2.要有@Repository,3.实现那两个类,这部分可以查看称之为接口实现那部分, 这样做的好处是可以屏蔽掉持久层代码,不加深其他业务层对持久 层的依赖,除了存储过程调用,编程式条件查询也应该是这种套路
Specifications ,编程式的条件查询
Repository 需要继承JpaSpecificationExecutor
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
然后可以调用类似下面这样的方法
List<T> findAll(Specification<T> spec);
参数为Specification类型的方法
Specification的构造方法如下
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get(Customer_.createdAt), date);
}
};
}
public static Specification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {
return new Specification<Customer>() {
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// build query here
}
};
}
}public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
这些静态方法可以看做是一些工场方法,这个看自己的逻辑了
TODO 这里要好好看看 jpa,但不知道去哪能找到比较有体系的教程(中文)
以上是 SpringDataJPA 的全部内容, 来源链接: utcz.com/z/518749.html