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, and get…By from the method and starts parsing the rest of it. The introducing clause can contain further expressions, such as a Distinct 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。

  1. 先判断userDepUuid (根据POJO(Plain Ordinary Java Object简单java对象,实际就是普通java bean规范),首字母变为小写。)是否是查询实体的一个属性,如果是根据该属性进行查询;如果没有该属性,继续第二步。

  2. 从右往左截取第一个大写字母开头的字符串(此处为Uuid),然后检查剩下的字符串是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后假设 user为查询实体的一个属性。

  3. 接着处理剩下部分(DepUuid),先判断user所对应的类型是否有depUuid属性,如果有,则表示该方法最终是根据 “Doc.user.depUuid” 的取值进行查询;否则继续按照步骤 2的规则从右往左截取,最终表示根据“Doc.user.dep.uuid” 的值进行查询。

  4. 可能会存在一种特殊情况,比如 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() and Pageable.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中也可以实现的,我们可以通过自定义的方式来进行实现

  1. 创建自定义的接口

public interface BookCustomRepository {

/**

* 查询所有书列表

* @return

*/

List<Book> listBook();}

  1. 自定接口实现

@Repository

public 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

  1. 继承自定接口

/**

* @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);

}

  1. 测试,在其他的地方就可以调用自定方法中的实现了

	@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

  1. 先自定义一个

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

}

}

  1. 然后

    @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

@Entity

public 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注释的方式体现

  1. ManyToMany

    它有4个属性,

    targetEntity: 可以不写,但是如果使用的是一个窗口,且没有泛型,就要有这个属性了

    cascade : 级联关系类型

    fetch :拉取关联数据的时期 , LAZY OR EARLY

    mappedBy:这个地方写另一方关联自己的属性

    因为是多对多关系,一般的两个表之间都会有一个中间表,ManyTyMany 只能表示两类对象的关联关系,但具体的关联方式需要借助 JoinTable,JoinColumn 两注解来描述

  2. JoinTable

代表两个实体表的关联表,这个注解的所有属性都不是必填,默认生成sql的规则是 关联外键为两个实体类表名(@Table或者类名)拼接下划线拼接类的id属性名,关联表名为两个table用下划线拼接(如果记不住不要紧,把sql打印出来就可以看见)

属性

作用

name

表名

joinColumns

主表关联列名

inverseJoinColumns

关联表关联列名

foreignKey

外键生成时的属性

inverseForeignKey

外键生成时的属性

uniqueConstraints

生成唯一索引属性

index

生成index属性

后面几个为生成表时使用的属性,主要joinColumns 和inverseJoinColumns不要写错了

  1. JoinColumn

    代表关联表的列,比如 一个User 有 多个 Pet ,库中pet表有一个列叫user_id,那么JoinColumn 就写(name="userId")

  2. OneToMany与 ManyToMany一样的用法,它可以与JoinColumn一组合使用

  3. 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);

  4. 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的解决办法

  1. 先定义接口

    public interface ProcedureRepository {

    ProcedureEntity p2(Long in);

    }

  2. 再具体实现

@Repository

public 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 ,编程式的条件查询

  1. Repository 需要继承JpaSpecificationExecutor

    	public interface CustomerRepository extends CrudRepository<Customer, 	Long>, JpaSpecificationExecutor {

    }

  2. 然后可以调用类似下面这样的方法

    List<T> findAll(Specification<T> spec);

    参数为Specification类型的方法

  3. 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

回到顶部