问候,这是有关Spring Data JPA的第二篇文章。 第一部分完全专注于水下耙,以及经验丰富的耙的技巧。 在这一部分中,我们将讨论如何根据您的需求优化框架。 此处描述的所有示例均可用。
计数
让我们从一个简单而同时又常见的任务开始:加载实体时,有必要有选择地下载其“女儿”。 考虑一个简单的例子:
@Entity public class Child { @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent; } @Entity public class Parent { @Id private Long id; }
在我们的示例中,子实体是懒惰的:我们不想在接收Child
时加载不必要的数据(并在SQL查询中联接另一个表)。 但是在某些情况下,在我们的应用程序中我们肯定知道我们将同时需要孩子和他的父母。 如果您让实体懒惰,我们将收到2个单独的请求。 如果您通过删除FetchType.LAZY
应用快速加载,则两个实体将始终在第一个请求时加载(我们不希望这样做)。
JPQL提供了一个很好的解决方案-这是fetch
关键字:
public interface ChildRepository extends JpaRepository<Child, Long> { @Query("select c from Child c join fetch c.parent where c.id = :id") Child findByIdFetchParent(@Param("id") Long id); }
该请求简单明了,但有缺点:
关键在于我们的援助:
@Entity @NamedEntityGraphs(value = { @NamedEntityGraph( name = Child.PARENT, attributeNodes = @NamedAttributeNode("parent") ) }) public class Child { public static final String PARENT = "Child[parent]"; @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent; }
该图本身很容易描述;使用时会遇到困难。 Spring Data JPA在其页面上建议这样做(适用于我们的案例):
public interface GroupRepository extends JpaRepository<GroupInfo, String> { @EntityGraph(value = Child.PARENT) @Query("select c from Child c where c.id = :id") Child findByIdFetchParent(@Param("id") Long id); }
在这里,我们看到了所有相同的问题(除了书面请求变得更加简单)。 您可以在微调的帮助下一口气结束它们。 创建您自己的接口,我们将使用它来构建存储库,而不是使用盒装的JpaRepository
:
@NoRepositoryBean public interface BaseJpaRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { T findById(ID id, String graphName); }
现在执行:
public class BaseJpaRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements BaseJpaRepository<T, ID> { private final JpaEntityInformation<T, ?> entityInfo; private final EntityManager entityManager; public BaseJpaRepositoryImpl(JpaEntityInformation<T, ?> ei, EntityManager em) { super(ei, em); this.entityInfo = ei; this.entityManager = em; } @Override public T findById(ID id, String graphName) { Assert.notNull(id, "The given id must not be null!");
现在,Spring不得不使用BaseJpaRepositoryImpl
作为应用程序所有存储库的基础:
@EnableJpaRepositories(repositoryBaseClass = BaseJpaRepositoryImpl.class) public class AppConfig { }
现在,从BaseJpaRepository
继承的所有存储库中都可以使用我们的方法。
这种方法有一个缺点,那就是可以放一个非常肥的猪。
试着为自己思考问题在于,Hibernate(至少在编写本文时)与图的名称和图本身不匹配。 因此,当我们执行类似的操作时,可能会出现运行时错误
Optional<MyEntity> entity = repository.findById(id, NON_EXISTING_GRAPH);
您可以使用测试来检查解决方案的运行状况:
@Sql("/ChildRepositoryGraphTest.sql") public class ChildRepositoryGraphTest extends TestBase { private final Long childId = 1L; @Test public void testGraph_expectFieldInitialized() { Child child1 = childRepository.findOne(childId, Child.PARENT); boolean initialized = Hibernate.isInitialized(child1.getParent()); assertTrue(initialized); } @Test public void testGraph_expectFieldNotInitialized() { Child child1 = childRepository .findById(childId) .orElseThrow(NullPointerException::new); boolean initialized = Hibernate.isInitialized(child1.getParent()); assertFalse(initialized); } }
树木大的时候
而且我们很小,没有经验,我们经常不得不看下面的代码:
public List<DailyRecord> findBetweenDates(Date from, Date to) { StringBuilder query = new StringBuilder("from Record "); if (from != null) { query.append(" where date >=").append(format(from)).append(" "); } if (to != null) { if (from == null) { query.append(" where date <= " + format(to) + " "); } else { query.append(" and date <= " + format(to) + " "); } } return em.createQuery(query.toString(), DailyRecord.class).getResultList(); }
此代码逐段收集请求。 这种方法的缺点很明显:
- 与你的手有很大关系
- 有手工工作的地方-有错误
- 不突出显示语法(运行时会弹出打字错误)
- 扩展和维护代码非常困难
不久之后,出现了Criteria API,这使我们可以将上面的代码压缩一点:
public List<DailyRecord> findBetweenDates(Date from, Date to) { Criteria criteria = em .unwrap(Session.class) .createCriteria(DailyRecord.class); if (from != null) { criteria.add(Expression.ge("date", from)); } if (to != null) { criteria.add(Expression.le("date", to)); } return criteria.list(); }
使用条件有几个优点:
- 使用元模型代替“日期”等“有线”值的能力
- 构造请求时出现的一些错误是编译错误,即在编写时已经检测到它们
- 与愚蠢的粘贴字符串相比,该代码更短,更易于理解
也有缺点:
- 代码很复杂,足以理解
- 要学习如何编写此类查询,您需要付出很多努力(我记得我第一次不得不在此类查询中进行纠错时遇到的最大痛苦,有时包括100-150行,带有分支等)
- 复杂的查询相当麻烦(距离限制不超过50行)
我想轻松愉快地开发它,所以我不喜欢这两种方法。
让我们转到我们已经检查过的实体:
@Entity public class Child { @Id private Long id; @JoinColumn(name = "parent_id") @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Parent parent;
我希望能够以不同的模式(及其组合)加载实体:
- 加载(或不加载)父级
- 加载(或不加载)玩具
- 按年龄分类儿童
如果您直接解决此问题,即通过编写许多与所选加载模式相对应的查询,那么很快就会导致组合爆炸:
@Query("select c from Child c join fetch c.parent order by c.age") List<Child> findWithParentOrderByAge(); @Query("select c from Child c join fetch c.toys order by c.age") List<Child> findWithToysOrderByAge(); @Query("select c from Child c join fetch c.parent join fetch c.toys") List<Child> findWithParentAndToys();
有一个简单而优雅的方法可以解决此问题:SQL / HQL和模板引擎的组合。 我的项目中使用了“ Freemarker”,尽管可以使用其他解决方案(“ Timlif”,“ Mustash”等)。
让我们开始创建。 首先,我们需要在一个接收扩展名*.hql.ftl
或*.sql.ftl
(如果使用“纯” SQL)的文件中描述查询:
现在您需要一个处理程序:
@Component @RequiredArgsConstructor public class TemplateParser { private final Configuration configuration; @SneakyThrows public String prepareQuery(String templateName, Map<String, Object> params){ Template template = configuration.getTemplate(templateName); return FreeMarkerTemplateUtils.processTemplateIntoString(template, params); } }
没什么复杂的。 进入存储库。 显然,继承JpaRepository
的接口不适合我们。 而是利用这个机会来创建自己的存储库:
public interface ChildRepositoryCustom { List<Child> findAll(boolean fetchParent, boolean fetchToys, boolean order); } @RequiredArgsConstructor public class ChildRepositoryImpl extends BaseDao implements ChildRepositoryCustom { private final TemplateParser templateParser; @Override public List<Child> findAll(boolean fetchParent, boolean fetchToys, boolean order) { Map<String, Object> params = new HashMap<>(); params.put("fetchParent", fetchParent); params.put("fetchToys", fetchToys); params.put("orderByAge", orderByAge); String query = templateParser.prepareQuery(BASE_CHILD_TEMPLATE.name, params); return em.createQuery(query, Child.class).getResultList(); } @RequiredArgsConstructor enum RepositoryTemplates { BASE_CHILD_TEMPLATE("BaseChildTemplate.hql.ftl"); public final String name; } }
为了使findUsingTemplate
方法可从ChildRepository
访问ChildRepository
您需要执行以下操作:
public interface ChildRepository extends BaseJpaRepository<Child, Long>, ChildRepositoryCustom {
与名称相关的重要功能Spring将仅使用正确的名称将我们的类和接口绑定在一起:
- 子存储库
- ChildRepository 自定义
- ChildRepository Impl
请记住这一点,因为如果名称中有错误,将引发难以理解的异常,无法从中理解错误原因。
现在,使用这种方法可以解决更复杂的问题。 假设我们需要根据用户选择的特征进行选择。 换句话说,如果用户未指定日期“从”和“到”,那么将没有时间过滤。 如果仅指定日期“ from”或仅日期“ to”,则过滤将是单向的。 如果同时指定了两个日期,则只有指定日期之间的记录才属于选择范围:
@Getter @RequiredArgsConstructor public class RequestDto { private final LocalDate from; private final LocalDate to; public boolean hasDateFrom() { return from != null; } public boolean hasDateTo() { return to != null; } } @Override public List<Child> findAll(ChildRequest request) { Map<String, Object> params = singletonMap("request", request); String query = templateParser.prepareQuery(TEMPLATE.name, params); return em.createQuery(query, Child.class).getResultList(); }
现在是模板:
<
Oracle和NVL
考虑本质:
@Entity public class DailyRecord { @Id private Long id; @Column private String currency; @Column(name = "record_rate") private BigDecimal rate; @Column(name = "fixed_rate") private BigDecimal fxRate; @Setter(value = AccessLevel.PRIVATE) @Formula("select avg(r.record_rate) from daily_record r where r.currency = currency") private BigDecimal avgRate; }
该实体用于查询(DBMS,我们记得有Oracle):
@Query("select nvl(record.fxRate, record.avgRate) " + " from DailyRecord record " + "where record.currency = :currency") BigDecimal findRateByCurrency(@Param("currency") String currency);
这是一个有效的请求。 但是它有一个小问题,SQL专家可能会指出。 事实是Oracle中的nvl
并不懒惰。 换句话说,当我们调用findRateByCurrency
方法时, findRateByCurrency
日志将findRateByCurrency
select nvl( dr.fixed_rate, select avg(r.record_rate) from daily_record r where r.currency = dr.currency ) from daily_record dr where dr.currency = ?
即使存在dr.fixed_rate
值,DBMS仍会计算nvl
第二个表达式返回的值。
select avg(r.record_rate) from daily_record r where r.currency = dr.currency)
读者可能已经知道如何避免不必要的请求权重:当然,这是关键字coalesce
,它与nvl
懒惰性,并且可以接受两个以上的表达式,因此具有优势。 让我们更正我们的请求:
@Query("select coalesce(record.fxRate, record.avgRate) " + " from DailyRecord record " + "where record.currency = :currency") BigDecimal findRateByCurrency(@Param("currency") String currency);
然后,正如他们所说,突然之间:
select nvl(dr.fixed_rate, select avg(r.record_rate) from daily_record r where r.currency = dr.currency) from daily_record dr where dr.currency = ?
要求保持不变。 那是因为盒子里的甲骨文方言变成了nvl
链。
要避免这种情况,您需要创建自己的方言并在应用程序中使用它:
public class CustomOracleDialect extends Oracle12cDialect { public CustomOracleDialect() { super(); registerFunction("coalesce", new StandardSQLFunction("coalesce")); } }
是的,就是这么简单。 现在,将创建的方言绑定到应用程序:
spring: jpa: database-platform: com.luxoft.logeek.config.CustomOracleDialect
另一种(不建议使用的)方式: spring: jpa: properties: hibernate.dialect: com.luxoft.logeek.config.CustomOracleDialect
重复执行该请求将使梦ko以求的koalesk:
select coalesce(dr.fixed_rate, select avg(r.record_rate) from daily_record r where r.currency = dr.currency) from daily_record dr where dr.currency = ?
Oracle和页面请求
通常,方言的完成为查询操作提供了丰富的机会。 通常,在开发应用程序和网络界面时,会遇到分页上传数据的任务。 换句话说,我们在数据库中有数十万条记录,但是它们以每页10/50/100条记录的包的形式显示。 开箱即用的Spring Date为开发人员提供了类似的功能:
@Query("select new com.luxoft.logeek.data.BriefChildData(" + "c.id, " + "c.age " + ") from Child c " + " join c.parent p " + "where p.name = ''") Page<BriefChildData> browse(Pageable pageable);
这种方法有一个很大的缺点,即执行两个查询,第一个查询获取数据,第二个查询确定数据库中它们的总数(这对于显示Page
对象中的数据总量是必需的)。 在我们的例子中,对此方法的调用给出了以下请求(使用p6spy进行记录):
select * from ( select c.id, c.age from child c inner join parent p on c.parent_id = p.id where p.name = '' ) where rownum <= 3; select count(c.id) from child c inner join parent p on c.parent_id = p.id where p.name = ''
如果查询很繁琐(许多表由不可索引的列连接,只有很多连接,困难的选择条件等),那么这可能会成为问题。 但是由于我们拥有Oracle,因此使用rownum伪列可以通过一个请求获得请求。
为此,我们需要完成方言,并描述用于计数所有记录的函数:
public class CustomOracleDialect extends Oracle12cDialect { public CustomOracleDialect() { super(); registerFunction("coalesce", new StandardSQLFunction("coalesce")); registerFunction("total_count", new TotalCountFunc()); } } public class TotalCountFunc implements SQLFunction { @Override public boolean hasArguments() { return true; } @Override public boolean hasParenthesesIfNoArguments() { return true; } @Override public Type getReturnType(Type type, Mapping mapping) { return StandardBasicTypes.LONG; } @Override public String render(Type type, List arguments, SessionFactoryImplementor factory) { if (arguments.size() != 1) { throw new IllegalArgumentException("Only 1 argument acceptable"); } return " count(" + arguments.get(0) + ") over () "; } }
现在编写一个新查询(在ChildRepositoryImpl
类中):
@Override public Page<BriefChildData> browseWithTotalCount(Pageable pageable) { String query = "select " + " c.id as id," + " c.age as age, " + " total_count(c.id) as totalCount" + " from Child c " + "join c.parent p " + "where p.name = ''"; List<BriefChildData> list = em.unwrap(Session.class) .createQuery(query) .setFirstResult((int) pageable.getOffset()) .setMaxResults(pageable.getPageSize()) .setResultTransformer(Transformers.aliasToBean(BriefChildData.class)) .getResultList(); if (list.isEmpty()) { return new PageImpl(Collections.emptyList()); } long totalCount = list.get(0).getTotalCount(); return new PageImpl<>(list, pageable, totalCount); }
调用此代码时,将执行一个请求
select * from (select c.id, c.age, count(c.id) over ()
在count(c.id) over ()
使用表达式count(c.id) over ()
您可以获取数据总量,并从数据类中获取数据,以传递给PageImpl
构造函数。 有一种方法可以更优雅地完成它,而无需在数据类中添加另一个字段,而要考虑它是一项功课:)您可以使用ProjectionVsDataTest测试来测试解决方案。
定制的陷阱
我们在Oracle和Spring Date上有一个很棒的项目。 我们的任务是提高此类代码的性能:
List<Long> ids = getIds(); ids.stream() .map(repository::findById) .filter(Optional::isPresent) .map(Optional::get) .forEach(this::sendToSchool);
缺点在于表面上:数据库查询的数量等于唯一键的数量。 有一种克服此困难的已知方法:
List<Long> ids = getIds(); repository .findAllById(ids) .forEach(this::sendToSchool);
多次采样的优势是显而易见的:如果早些时候,我们有许多类似形式的查询
select p.* from Pupil p where p.id = 1 select p.* from Pupil p where p.id = 2 select p.* from Pupil p where p.id = 3
那么现在他们已经崩溃了
select p.* from Pupil p where p.id in (1, 2, 3, ... )
它似乎更容易变得更好。 该项目不断发展,发展,数据成倍增长,一旦不可避免,该项目就将出现:
晴间的Aki雷声 ERROR - ORA-01795: maximum number of expressions in a list is 1000
我们需要再次寻找出路(不要返回到旧版本)。 由于Oracle不允许他提供超过1000个键,因此您可以将整个数据集划分为不超过1000个相等的份额,并执行多个查询:
List<List<Long>> idChunks = cgccLists.partition(ids, 1000);
此方法有效,但闻起来有点(是吗?):如果您在其他地方遇到此类困难,则必须围护同一花园。 让我们尝试更优雅地解决问题,即BaseJpaRepositoryImpl
。 最简单的方法是将上述逻辑向内传递,对用户隐藏:
@Override public List<T> findAllById(Iterable<ID> ids) { Assert.notNull(ids, "The given Iterable of Id's must not be null!"); Set<ID> idsCopy = Sets.newHashSet(ids); if (idsCopy.size() <= OracleConstants.MAX_IN_COUNT) { return super.findAllById(ids); } return findAll(idsCopy); } private List<T> findAll(Collection<ID> ids) { List<List<ID>> idChunks = Lists.partition(new ArrayList<>(ids), 1000); return idChunks .stream() .map(this::findAllById) .flatMap(List::stream) .collect(Collectors.toList()); }
情况变得越来越好:首先,我们从基础结构层清除了工作代码,其次,我们将解决方案的范围扩展到了所有扩展BaseJpaRepository
项目存储库。 也有缺点。 主要的是几个请求而不是一个请求,还有(来自主要请求的词干)-需要过滤键,因为如果不这样做,那么相同的键可能会出现在不同的idChunks
。 反过来,这意味着同一实体将被两次包含在列表中,因此将被处理两次。 我们不需要这个,因此这是另一个更复杂的解决方案:
@Override public List<T> findAllById(Iterable<ID> ids) { Assert.notNull(ids, "The given Iterable of Id's must not be null!"); ArrayList<ID> idsCopy = Lists.newArrayList(ids); if (idsCopy.size() <= OracleConstants.MAX_IN_COUNT) { return super.findAllById(ids); } return findAll(idsCopy); } private List<T> findAll(ArrayList<ID> ids) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<T> query = cb.createQuery(getDomainClass()); Root<T> from = query.from(getDomainClass()); Predicate predicate = toPredicate(cb, ids, from); query = query.select(from).where(predicate); return entityManager.createQuery(query).getResultList(); } private Predicate toPredicate(CriteriaBuilder cb, ArrayList<ID> ids, Root<T> root) { List<List<ID>> chunks = Lists.partition(ids, OracleConstants.MAX_IN_COUNT); SingularAttribute<? super T, ?> id = entityInfo.getIdAttribute(); Predicate[] predicates = chunks.stream() .map(chunk -> root.get(id).in(chunk)) .toArray(Predicate[]::new); return cb.or(predicates); }
它使用Criteria API,这使得构建一个最终查询形式成为可能。
select p.* from Pupil p where p.id in (1, 2, ... , 1000) or p.id in (1001, ... , 2000) or p.id in (2001, ... , 3000)
有一个微妙之处:由于条件繁琐,类似的请求可以比平常执行更长的时间,因此第一种方法可能(有时)是更可取的。
就这样,我希望这些示例对您有用,并且对日常开发有用。 欢迎评论和添加。