跳到主要内容

Spring Data

Tips

Note

  • RepositoryMetadata

  • RepositoryInformation

  • RepositoryQuery

    • 抽象接口, 用于执行所有数据仓库操作
  • PartTree

    • 用于将方法名转换为查询方法
    • ^(find|read|get|query|stream|count|exists|delete|remove)((\p{Lu}.*?))??By
  • PersistenceExceptionTranslationInterceptor

    • 处理数据仓库调用过程中的异常
  • 查询方法处理链

    • SurroundingTransactionDetectorMethodInterceptor
      • 事务设置
    • ExposeInvocationInterceptor
    • DefaultMethodInvokingMethodInterceptor
      • 处理 default 方法
    • RepositoryFactorySupport$QueryExecutorMethodInterceptor

映射 - org.springframework.data.mapping

  • 处理不同 SpringData 持久化的中间层映射
  • PersistentEntity<T, P extends PersistentProperty<P>> extends Iterable<P>
    • 一个持久化对象
  • PersistentProperty
    • 一个持久化对象的属性
    • Association
      • 表示属性之间的关联
    • 主要实现
      • JpaPersistentPropertyImpl
        • JPA 持久化对象的属性
  • MutablePersistentEntity<T, P extends PersistentProperty<P>> extends PersistentEntity<T, P>
    • 包含了修改方法
  • IdentifierAccessor
  • PersistentPropertyAccessor
  • MappingContext
    • 映射上下文
    • 记录了所有已知的实体类型
    • PersistentPropertyPath
      • 表示一个持久属性的路径

MongoDB

// SpringData 查询日志
logging.level.org.springframework.data.mongodb.core.MongoTemplate: DEBUG

// MongoDB 的日志工具类 com.mongodb.diagnostics.logging.Loggers
// 前缀 org.mongodb.driver 例如 操作 operation
logging.level.org.mongodb.driver.operation: INFO
// 或者打开全量的
// 注意, 依然是看不到发送的查询
logging.level.org.mongodb.driver: INFO
  • MongoDB 的 Repository 实现
    • org.springframework.data.mongodb.repository.support.SimpleMongoRepository
    • org.springframework.data.mongodb.repository.support.QueryDslMongoRepository
  • MongoQueryMethod
    • 解析 Mongo 的 Query 注解
  • PartTreeMongoQuery
    • 通过方法解析后的 Query 实例
  • org.springframework.data.mongodb.core.query.Query
    • 实际查询
  • org.springframework.data.mongodb.repository.support.SpringDataMongodbQuery
    • QueryDSL Mongo 的查询对象

interface MyRepo{
// 只返回 tags 字段
// 注意: QueryDSL 的 Predicate 不会被使用
@Query(fields = "{'tags':1}")
List<PageDocument> findTagsBy(Predicate predicate);
// 建议基于 QueryDslMongoRepository 将查询对象上的 Path 参数暴露出来, 例如
List<T> findAll(Predicate predicate, Path<?>... paths);
}

Entity Versions

Spring Data 查询关键字

逻辑关键词关键词表达式
ANDAnd
OROr
AFTERAfter, IsAfter
BEFOREBefore, IsBefore
CONTAININGContaining, IsContaining, Contains
BETWEENBetween, IsBetween
ENDING_WITHEndingWith, IsEndingWith, EndsWith
EXISTSExists
FALSEFalse, IsFalse
GREATER_THANGreaterThan, IsGreaterThan
GREATER_THAN_EQUALSGreaterThanEqual, IsGreaterThanEqual
INIn, IsIn
ISIs, Equals, (or no keyword)
IS_NOT_NULLNotNull, IsNotNull
IS_NULLNull, IsNull
LESS_THANLessThan, IsLessThan
LESS_THAN_EQUALLessThanEqual, IsLessThanEqual
LIKELike, IsLike
NEARNear, IsNear
NOTNot, IsNot
NOT_INNotIn, IsNotIn
NOT_LIKENotLike, IsNotLike
REGEXRegex, MatchesRegex, Matches
STARTING_WITHStartingWith, IsStartingWith, StartsWith
TRUETrue, IsTrue
WITHINWithin, IsWithin

Spring Data 返回结果类型

返回类型描述
void不返回值
原子类型Java 原子类型值
包装类型Java 包装类型值
TAn unique entity. Expects the query method to return one result at most. In case no result is found null is returned. More than one result will trigger an IncorrectResultSizeDataAccessException.
Iterator<T>An Iterator.
Collection<T>A Collection.
List<T>A List.
Optional<T>A Java 8 or Guava Optional. Expects the query method to return one result at most. In case no result is found Optional.empty()/Optional.absent() is returned. More than one result will trigger an IncorrectResultSizeDataAccessException.
Stream<T>A Java 8 Stream.
Future<T>A Future. Expects method to be annotated with @Async and requires Spring’s asynchronous method execution capability enabled.
CompletableFuture<T>A Java 8 CompletableFuture. Expects method to be annotated with @Async and requires Spring’s asynchronous method execution capability enabled.
ListenableFutureA org.springframework.util.concurrent.ListenableFuture. Expects method to be annotated with @Async and requires Spring’s asynchronous method execution capability enabled.
SliceA sized chunk of data with information whether there is more data available. Requires a Pageable method parameter.
Page<T>A Slice with additional information, e.g. the total number of results. Requires a Pageable method parameter.
GeoResult<T>A result entry with additional information, e.g. distance to a reference location.
GeoResults<T>A list of GeoResult<T> with additional information, e.g. average distance to a reference location.

空间坐标类型(GeoResult, GeoResults, GeoPage)只有在存储类型支持空间类型时返回

JPA Repository 方法语法

KeywordSampleJPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is,EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age ⇐ ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);


// 使用 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);

// 返回结果流
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();
// 只返回结果片段
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

// 选择单个返回,可返回 Optional
User findFirstByOrderByLastnameAsc();
Optional<User> findTopByOrderByAgeDesc();

// 异步操作
@Async
Future<User> findByFirstname(String firstname);
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
@Async
ListenableFuture<User> findOneByLastname(String lastname);

Long countByLastname(String lastname);
Long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);

单独使用 Repository

RepositoryFactorySupport factory =// Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

实现自定义接口

interface UserRepositoryCustom {
public void someCustomMethod(User user);
}
class UserRepositoryImpl implements UserRepositoryCustom {

public void someCustomMethod(User user) {
// Your custom implementation
}
}
interface UserRepository extends CrudRepository<User, Long>, UserRepositoryCustom {

// Declare query methods here
}

为所有 Repository 添加自定义方法

@NoRepositoryBean
public interface MyRepository<T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID> {

void sharedCustomMethod(ID id);
}

public class MyRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID> {

private final EntityManager entityManager;

// 构造函数必须要要有这样的依赖注入
public MyRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);

// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}

public void sharedCustomMethod(ID id) {
// implementation goes here
}
}

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration {}

在 Query 中使用 SpEL 来书写通用的 SQL

@MappedSuperclass
public abstract class AbstractMappedType {
String attribute;
}

@Entity
public class ConcreteType extends AbstractMappedType {}

@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType> extends Repository<T, Long> {
@Query("select t from #{#entityName} t where t.attribute = ?1")
List<T> findAllByAttribute(String attribute);
}

public interface ConcreteRepository extends MappedTypeRepository<ConcreteType> {}

Spring Web 集成

直接使用实体和特殊参数

@Controller
@RequestMapping("/users")
public class UserController {

@RequestMapping("/{id}")
public String showUserForm(@PathVariable("id") User user, Model model) {

model.addAttribute("user", user);
return "userForm";
}
}
  • 通过 DomainClassConverter 实现参数实体的注入
  • 通过 HandlerMethodArgumentResolver 实现特殊参数(Pageable,Sort)的注入

使用多个分页参数

以下两个参数分别通过 foo_pagebar_page 指定

public String showUsers(Model model,
@Qualifier("foo") Pageable first,
@Qualifier("bar") Pageable second) {}

Spring HATEOAS 支持分页

@Controller
class PersonController {

@Autowired PersonRepository repository;

@RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity<PagedResources<Person>> persons(Pageable pageable,
PagedResourcesAssembler assembler) {

Page<Person> persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
}

使用 Querydsl 实现查询参数绑定

@Controller
class UserController {

@Autowired UserRepository repository;

@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

model.addAttribute("users", repository.findAll(predicate, pageable));

return "index";
}
}

/* 定制化绑定方式 */
interface UserRepository extends CrudRepository<User, String>,
QueryDslPredicateExecutor<User>,
QuerydslBinderCustomizer<QUser> {

@Override
default public void customize(QuerydslBindings bindings, QUser user) {

bindings.bind(user.username).first((path, value) -> path.contains(value))
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value));
bindings.excluding(user.password);
}
}

使用 Hibernate 实现软删除

@Entity
@Where("deleted = 0") // Hibernate 注解
class User{
String name;
Integer deleted;
}

自动生成创建时间字段和更新时间字段

// ==================== #1 =======================

@EntityListeners({AuditingEntityListener.class})
@MappedSuperclass
@Data
@ToString
@EqualsAndHashCode
public abstract class AbstractEntity implements Serializable { }

// ==================== #2 =======================

@Entity
@Table(name = "entities")
public class Entity {
...

private Date created;
private Date updated;

@PrePersist
protected void onCreate() {
created = new Date();
}

@PreUpdate
protected void onUpdate() {
updated = new Date();
}
}

// =================== #3 ========================

@MappedSuperclass
public abstract class AbstractTimestampEntity {
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created", nullable = false)
private Date created;

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "updated", nullable = false)
private Date updated;

@PrePersist
protected void onCreate() {
updated = created = new Date();
}

@PreUpdate
protected void onUpdate() {
updated = new Date();
}
}

@Entity
@Table(name = "campaign")
public class Campaign extends AbstractTimestampEntity implements Serializable {
// ...
}
// =================== #4 ========================

@EqualsAndHashCode(callSuper = true)
@Entity
@Data
@Accessors(chain = true)
@EntityListeners(AuditingEntityListener.class)
public class User extends AbstractPersistable<Long> {
String username;
String address;

@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;

@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
}

链接断开异常

DataAccessResourceFailureException: could not prepare statement; nested exception is org.hibernate.exception.JDBCConnectionException: could not prepare statement
Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 48,054,727 milliseconds ago.  The last packet sent successfully to the server was 48,054,727 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.

为 MySQL 使用 &autoReconnect=true&failOverReadOnly=false&maxReconnects=10 链接参数

Not a managed type

@SpringBootApplication
@EntityScan(basePackageClasses = MyEntityPackage.class)
class MyApp{

}

使用 QueryDSL 构建动态查询

public interface BaseRepository<T, ID extends Serializable>
extends JpaRepository<T, ID>, JpaSpecificationExecutor<T>, QueryDslPredicateExecutor<T> {
}
public interface UserRepository extends BaseRepository<User,Long> {
}
PathBuilder builder = new PathBuilder<>(entityType, Preconditions.checkNotNull(CaseFormat.UPPER_CAMEL.converterTo(CaseFormat.LOWER_CAMEL).convert(entityType.getSimpleName())));

public Page<T> query(Map<String, Object> params, Pageable pageable) {
BooleanExpression[] expressions = new BooleanExpression[params.size()];
int i = 0;
for (Map.Entry<String, Object> entry : params.entrySet()) {
expressions[i++] = builder.get(entry.getKey()).eq(entry.getValue());
Expressions.path(entityType, entry.getKey());
}
return repository.findAll(BooleanExpression.allOf(expressions), pageable);
}