如何向Spring Data JPA添加自定义方法

220

我正在研究Spring Data JPA。考虑下面的示例,其中默认情况下将获得所有CRUD和查找器功能,如果我想自定义查找器,则可以在接口本身轻松完成。

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {

  @Query("<JPQ statement here>")
  List<Account> findByCustomer(Customer customer);
}

我想知道如何为上述AccountRepository添加一个完整的自定义方法及其实现?由于它是一个接口,我不能在那里实现该方法。


接口自Java 8开始支持默认方法,不确定Spring Data是否支持此功能。 - Christoph Dahlen
13个回答

365
你需要为你的自定义方法创建一个单独的接口:
public interface AccountRepository 
    extends JpaRepository<Account, Long>, AccountRepositoryCustom { ... }

public interface AccountRepositoryCustom {
    public void customMethod();
}

同时为该接口提供一个实现类:

public class AccountRepositoryImpl implements AccountRepositoryCustom {

    @Autowired
    @Lazy
    AccountRepository accountRepository;  /* Optional - if you need it */

    public void customMethod() { ... }
}

另请参阅:


29
这个自定义实现能否注入实际的仓库,以便可以使用在那里定义的方法?具体地说,我想在更高级别的查找实现中引用Repository接口中定义的各种find函数。因为这些find()函数没有实现,所以我无法在Custom接口或Impl类中声明它们。 - JBCP
21
我已经按照这个答案做了,但不幸的是现在Spring Data试图在我的“Account”对象上查找属性“customMethod”,因为它正在尝试自动生成查询以覆盖AccountRepository中定义的所有方法。有什么办法可以阻止这种情况发生吗? - Nick Foote
51
请注意,你实现存储库的类名应该是:AccountRepositoryImpl而不是AccountRepositoryCustomImpl等。这是一种非常严格的命名约定。 - Xeon
5
是的,您的 impl 对象可以注入 repository,没有问题。 - JBCP
6
请看我之前的评论,如果你正在扩展 QueryDslRepositorySupport ,它不起作用。此外,必须通过字段或setter方法注入存储库,而不是构造函数注入,否则它将无法创建bean。它似乎可以工作,但解决方案感觉有点“不干净”,我不确定 Spring Data 团队是否有改进这个机制的计划。 - Robert Hunt
显示剩余19条评论

84

除了axtavt的回答之外,不要忘记如果您需要在自定义实现中构建查询,可以注入实体管理器:

public class AccountRepositoryImpl implements AccountRepositoryCustom {

    @PersistenceContext
    private EntityManager em;

    public void customMethod() { 
        ...
        em.createQuery(yourCriteria);
        ...
    }
}

11
谢谢,但我想知道如何在自定义实现中使用Pageable和Page。你有任何建议吗? - Wand Maker
1
@WandMaker,将它们传递到您的自定义方法中并在方法内使用即可。 - zygimantus

60

有一种略微修改的解决方案,不需要额外的接口。

文档中所述功能,使用Impl 后缀可以让我们拥有如此干净的解决方案:

  • 在您的常规 @Repository 接口中定义自定义方法(除Spring Data方法之外),例如 MyEntityRepository
  • 创建一个名为 MyEntityRepositoryImpl 的类(后缀 Impl 是魔法),它可以位于任何位置(甚至不需要在同一个包中)只实现自定义方法并对这样的类 注释 @Component @Repository 将不会工作)。
    • 这个类甚至可以通过 @Autowired 注入 MyEntityRepository 以供自定义方法使用。

示例:

实体类(为了完整性):

package myapp.domain.myentity;
@Entity
public class MyEntity {
    @Id     private Long id;
    @Column private String comment;
}

仓库接口:

package myapp.domain.myentity;

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {

    // EXAMPLE SPRING DATA METHOD
    List<MyEntity> findByCommentEndsWith(String x);

    List<MyEntity> doSomeHql(Long id);   // custom method, code at *Impl class below

    List<MyEntity> useTheRepo(Long id);  // custom method, code at *Impl class below

}

自定义方法实现Bean:
package myapp.infrastructure.myentity;

@Component // Must be @Component !!
public class MyEntityRepositoryImpl { // must have the exact repo name + Impl !!

    @PersistenceContext
    private EntityManager entityManager;

    @Autowired
    private MyEntityRepository myEntityRepository;

    @SuppressWarnings("unused")
    public List<MyEntity> doSomeHql(Long id) {
        String hql = "SELECT eFROM MyEntity e WHERE e.id = :id";
        TypedQuery<MyEntity> query = entityManager.createQuery(hql, MyEntity.class);
        query.setParameter("id", id);
        return query.getResultList();
    }

    @SuppressWarnings("unused")
    public List<MyEntity> useTheRepo(Long id) {
        List<MyEntity> es = doSomeHql(id);
        es.addAll(myEntityRepository.findByCommentEndsWith("DO"));
        es.add(myEntityRepository.findById(2L).get());
        return es;
    }

}

使用方法:

// You just autowire the the MyEntityRepository as usual
// (the Impl class is just impl detail, the clients don't even know about it)
@Service
public class SomeService {
    @Autowired
    private MyEntityRepository myEntityRepository;

    public void someMethod(String x, long y) {
        // call any method as usual
        myEntityRepository.findByCommentEndsWith(x);
        myEntityRepository.doSomeHql(y);
    }
}

就这些了,除了你已经拥有的Spring Data repo接口之外,不需要任何其他接口。


我发现的唯一可能的缺点是:

  • Impl类中的自定义方法被编译器标记为未使用,因此建议使用@SuppressWarnings("unused")
  • 您只能有一个Impl类。(而在常规片段接口实现文档建议中,您可以有很多。)
  • 如果您将Impl类放在不同的包中,并且您的测试仅使用@DataJpaTest,则必须在测试中添加@ComponentScan("package.of.the.impl.clazz"),以便Spring加载它。

1
如何正确地自动装配MyEntityRepositoryImpl? - Konstantin Zyubin
@KonstantinZyubin 您应该自动装配 MyEntityRepository,而不是 *Impl - acdcjunior
3
非常全面、详细和有帮助的答案。绝对应该有更多的赞! - arnaldop
非常有帮助的答案。 - Samuel Moshie
1
但是你会失去接口<->实现方法签名检查。 - Julien
显示剩余5条评论

29

被接受的答案能够工作,但存在三个问题:

  • 当将自定义实现命名为AccountRepositoryImpl时,它使用了一个未记录在Spring Data文档中的功能。而文档明确指出,它必须被称为AccountRepositoryCustomImpl,即自定义接口名称加上Impl
  • 您不能使用构造函数注入,只能使用@Autowired,这被认为是不好的做法
  • 您在自定义实现中存在循环依赖(这就是为什么您不能使用构造函数注入)。

然而,我找到了一种使其完美的方法,尽管使用了另一个未记录在Spring Data文档中的功能:

public interface AccountRepository extends AccountRepositoryBasic,
                                           AccountRepositoryCustom 
{ 
}

public interface AccountRepositoryBasic extends JpaRepository<Account, Long>
{
    // standard Spring Data methods, like findByLogin
}

public interface AccountRepositoryCustom 
{
    public void customMethod();
}

public class AccountRepositoryCustomImpl implements AccountRepositoryCustom 
{
    private final AccountRepositoryBasic accountRepositoryBasic;

    // constructor-based injection
    public AccountRepositoryCustomImpl(
        AccountRepositoryBasic accountRepositoryBasic)
    {
        this.accountRepositoryBasic = accountRepositoryBasic;
    }

    public void customMethod() 
    {
        // we can call all basic Spring Data methods using
        // accountRepositoryBasic
    }
}

1
这个可行。我想强调构造函数中参数的名称必须遵循此答案中的约定(必须为accountRepositoryBasic)。否则,Spring 将抱怨在我的 *Impl 构造函数中存在两个可选的 bean 供注入。 - goat
那么AccountRepository有什么用途? - Kalpesh Soni
@KalpeshSoni 通过注入的 AccountRepositoryAccountRepositoryBasicAccountRepositoryCustom 的方法都将可用。 - geg
2
请问您能提供创建上下文的方法吗?我无法将它们整合在一起。谢谢。 - franta kocourek
这也会导致循环依赖错误。 - Walking Corpse
这个应该是被接受的解决方案。我已经尝试过它并且有效,不需要@Autowired,也不会导致循环依赖错误。 - undefined

17

这在使用上有限制,但是对于简单的自定义方法,您可以使用default接口方法,例如:

import demo.database.Customer;
import org.springframework.data.repository.CrudRepository;

public interface CustomerService extends CrudRepository<Customer, Long> {


    default void addSomeCustomers() {
        Customer[] customers = {
            new Customer("Józef", "Nowak", "nowakJ@o2.pl", 679856885, "Rzeszów", "Podkarpackie", "35-061", "Zamknięta 12"),
            new Customer("Adrian", "Mularczyk", "adii333@wp.pl", 867569344, "Krosno", "Podkarpackie", "32-442", "Hynka 3/16"),
            new Customer("Kazimierz", "Dejna", "sobieski22@weebly.com", 996435876, "Jarosław", "Podkarpackie", "25-122", "Korotyńskiego 11"),
            new Customer("Celina", "Dykiel", "celina.dykiel39@yahoo.org", 947845734, "Żywiec", "Śląskie", "54-333", "Polna 29")
        };

        for (Customer customer : customers) {
            save(customer);
        }
    }
}

编辑:

此Spring教程中写道:

Spring Data JPA还允许您通过声明方法签名来定义其他查询方法。

因此,甚至可以只声明类似于以下方法:

Customer findByHobby(Hobby personHobby);

如果Hobby对象是客户的属性,那么Spring会自动为您定义方法。


6

我使用以下代码来访问自定义实现生成的查找方法。通过bean工厂获取实现可以防止循环bean创建问题。

public class MyRepositoryImpl implements MyRepositoryExtensions, BeanFactoryAware {

    private BrandRepository myRepository;

    public MyBean findOne(int first, int second) {
        return myRepository.findOne(new Id(first, second));
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        myRepository = beanFactory.getBean(MyRepository.class);
    }
}

5
考虑到您的代码片段,请注意您只能将本地对象传递给 findBy### 方法,假设您想加载属于某些客户的账户列表,一种解决方法是这样做:
@Query("Select a from Account a where a."nameoffield"=?1")
List<Account> findByCustomer(String "nameoffield");

确保要查询的表名与实体类的名称相同。 有关进一步的实现,请查看this

1
查询中有一个错别字,应该是nameoffield,但我没有适当的权限来修复它。 - Bruno Medeiros

5

我喜欢Danila的解决方案并开始使用,但团队中没有其他人喜欢每个仓库创建4个类的方式。 Danila的解决方案是这里唯一可以在Impl类中使用Spring Data方法的解决方案。不过,我发现了一种只需要一个类就能实现的方法:

public interface UserRepository extends MongoAccess, PagingAndSortingRepository<User> {

    List<User> getByUsername(String username);


    default List<User> getByUsernameCustom(String username) {
        // Can call Spring Data methods!
        findAll();

        // Can write your own!
        MongoOperations operations = getMongoOperations();
        return operations.find(new Query(Criteria.where("username").is(username)), User.class);
    }
}

你只需要获取访问数据库的bean(在这个例子中是MongoOperations)的方法。MongoAccess通过直接检索bean来为你的所有存储库提供访问权限:

public interface MongoAccess {
    default MongoOperations getMongoOperations() {
        return BeanAccessor.getSingleton(MongoOperations.class);
    }
}

BeanAccessor是什么:

@Component
public class BeanAccessor implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    public static <T> T getSingleton(Class<T> clazz){
        return applicationContext.getBean(clazz);
    }

    public static <T> T getSingleton(String beanName, Class<T> clazz){
        return applicationContext.getBean(beanName, clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        BeanAccessor.applicationContext = applicationContext;
    }

}

不幸的是,在接口中无法使用 @Autowire。您可以将 bean 自动装配到 MongoAccessImpl 中,并在接口中提供一个访问它的方法,但是 Spring Data 会出错。我认为它不希望看到关联着 PagingAndSortingRepository 的实现,即使间接关联也不行。


4

如果您想要执行更为复杂的操作,您可能需要访问Spring Data的内部。在这种情况下,以下内容可以起到作用(作为我对DATAJPA-422的临时解决方案):

public class AccountRepositoryImpl implements AccountRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    private JpaEntityInformation<Account, ?> entityInformation;

    @PostConstruct
    public void postConstruct() {
        this.entityInformation = JpaEntityInformationSupport.getMetadata(Account.class, entityManager);
    }
    
    @Override
    @Transactional
    public Account saveWithReferenceToOrganisation(Account entity, long organisationId) {
        entity.setOrganisation(entityManager.getReference(Organisation.class, organisationId));
        return save(entity);
    }

    private Account save(Account entity) {
        // save in same way as SimpleJpaRepository
        if (entityInformation.isNew(entity)) {
            entityManager.persist(entity);
            return entity;
        } else {
            return entityManager.merge(entity);
        }
    }

}

3

这里需要考虑另一个问题。有些人希望将自定义方法添加到仓库中后,它们会自动以“/search”链接的形式公开为REST服务。但不幸的是,Spring目前不支持这种情况。

Spring Data REST明确检查自定义方法是否作为特定设计功能,并不将其公开为REST搜索链接:

private boolean isQueryMethodCandidate(Method method) {    
  return isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method);
}

这是Oliver Gierke的一句名言:
“这是有意而为之的。自定义存储库方法不是查询方法,因为它们可以实现任何行为。因此,我们目前无法决定将该方法暴露在哪种HTTP方法下。POST可能是最安全的选项,但这与通用查询方法(接收GET)不符合。”
更多细节请参见此问题:https://jira.spring.io/browse/DATAREST-206

很不幸,我浪费了很多时间试图找出我的错误所在,最终我明白了这个功能根本不存在。他们为什么要实现那个功能呢?为了少些代码?为了将所有dao方法放在一个地方?我本可以用其他方式实现这个目标。有人知道“向单个存储库添加行为”的功能的目的是什么吗? - Skeeve
您可以通过在方法上添加@RestResource(path = "myQueryMethod")注释来通过REST公开任何存储库方法。上面的引用只是说明Spring不知道您想要将其映射为什么(即GET vs POST等),因此由您通过注释指定。 - GreenGiant

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