Java8的Optional返回值与JPA的最佳实践是什么?

18

我喜欢Java8的语义。我在我的DAO中使用了很多这样的代码:

  public Optional<User> findBy(String username) {
    try {
      return Optional.of(
        emp.get().createQuery("select u from User u where u.username = :username" , User.class)
        .setParameter("username" , username)
        .setMaxResults(1)
        .getSingleResult()
      );
    } catch (NoResultException e) {
      return Optional.empty();
    }
  }

它的工作效果很好,但是这样的代码(try catch NoResultException)散布在我的DAO上。我不得不捕获异常,这会在某种程度上降低性能。

我想知道这是否是最佳解决方案?或者有没有更好的解决方案,而不需要try-catch?

如果不可能(因为NoResultException已在JPA中定义),是否有任何“模板化”此类工作流程的快捷方式?

谢谢。


3
最好的解决方案是修复抛出异常的代码,我猜测这个代码是 getSingleResult。 "没有结果" 不是异常情况,为此抛出异常是不合适的。 - T.J. Crowder
4
这是 JPA 规范的一部分,并由 JPA 提供者实现 - 不确定这是否是一个选择。 - Boris the Spider
@BoristheSpider:天啊,那个是怎么通过审核层的?! - T.J. Crowder
@T.J.Crowder 委员会设计...总是以失败告终。Hibernate的等效只是返回null表示没有结果。然而,对于有多个实际结果的情况,仍然可能出现异常。 - Marko Topolnik
3
JPA的查询结果为什么不会返回null,是因为查询可能会选择某些列,而这些列的内容可能为空。这与“没有这样的行”是不同的。这里有一些讨论:https://dev59.com/vnI-5IYBdhLWcg3w-92b - smallufo
2个回答

28
当然,你可以使用lambda的魔力将其模板化!
首先,使用@FunctionalInterface定义lambda的契约:
@FunctionalInterface
public interface DaoRetriever<T> {
    T retrieve() throws NoResultException;
}

这是一种单方法接口(Single Method Interface,SMI),它将封装您的方法的行为。

现在创建一个实用程序方法来使用SMI:

public static <T> Optional<T> findOrEmpty(final DaoRetriever<T> retriever) {
    try {
        return Optional.of(retriever.retrieve());
    } catch (NoResultException ex) {
        //log
    }
    return Optional.empty();
}

现在,在你的调用代码中使用import static,你上面的方法变成了这样:
public Optional<User> findBy(String username) {
    return findOrEmpty(() ->
            emp.get().createQuery("select u from User u where u.username = :username", User.class)
                    .setParameter("username", username)
                    .setMaxResults(1)
                    .getSingleResult());
}

在这里,() -> emp.get()...是捕获检索行为的lambda表达式。 interface DaoRetriever允许抛出NoResultException,因此lambda表达式也可以抛出异常。
或者,我会使用TypedQuery的另一种方法-getResultList,并按以下方式更改代码:
public Optional<User> findBy(String username) {
    return emp.get().createQuery("select u from User u where u.username = :username", User.class)
            .setParameter("username", username)
            .setMaxResults(1)
            .getResultList()
            .stream()
            .findFirst();
}

这种方法的优点是更加简单易懂,但缺点是如果还有其他结果的话,它们将被直接丢弃。

哇,太棒了,无论是模板还是stream.findFirst()的解决方案。我学到了很多。谢谢。 - smallufo
1
@smallufo 非常愉快。只是稍微修改了SMI - 使其成为一个通用类。 - Boris the Spider
1
setMaxResults(1) 保证不会获取额外的结果——我会将这个习语制作成一个模板(接受查询,调用 setMaxResults(1).getResultList().stream().findFirst())。顺便说一下,我不会称呼这个方法为 findOrEmpty——仅仅使用 find 就足够了,因为返回类型是 Optional。 - Marko Topolnik
} catch (NoResultException ex) { 除非是一个异常情况并且将被重新抛出而不是记录,否则这不是一个好主意。 - cyprian

4

Boris走在正确的轨道上,但还有改进空间。我们需要更多的抽象。这种转换与daos无关。

我们需要具有不同数量参数的家族或功能接口,将抛出异常的lambda转换为不抛出异常的lambda。FunctionalJava (http://www.functionaljava.org/)可以做到:

因此,我们有了Try类的系列:Try0、Try1等。

public interface Try0<A, Z extends Exception> {
    A f() throws Z;
}

我们希望将此转换为一个不会抛出异常的函数:
static public <A, E extends Exception> Supplier<Validation<E, B>> toSupplierValidation(final Try0<A, E> t) {
    return () -> {
        try {
            return Validation.success(t.f());
        } catch (Exception e) {
            return Validation.fail((E) e);
        }
    };
}

请注意,验证操作的返回值可能是异常信息,如果操作成功则是正常的值(https://functionaljava.ci.cloudbees.com/job/master/javadoc/)。如果您不关心异常信息,可以将失败情况转换为空的可选项,将成功情况的值放在可选项中。这种方法与Boris的方法类似,但没有dao引用(这些引用是无关紧要的):
static public <A, E extends Exception> Supplier<Optional<A>> toSupplierOptional(final Try0<A, E> t) {
    return () -> {
        try {
            return Optional.of(t.f());
        } catch (Exception e) {
            return Optional.empty();
        }
    };
}

1
谢谢。你的解决方案对我来说太抽象了,可能需要一些时间来消化。感谢你提供的FunctionalJava的解决方案。(我以前没有接触过FJ) - smallufo
1
DaoRetriever接口如果表示不带参数并返回结果或抛出通用异常的任何函数,则会更好。这就是Try0接口。也许,如果我将Try1编辑为Try0以匹配先前的示例,它会有所帮助。 - Mark Perry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接