【问题标题】:How to add custom method to Spring Data JPA如何将自定义方法添加到 Spring Data JPA
【发布时间】:2012-08-06 12:45:20
【问题描述】:

我正在研究 Spring Data JPA。考虑下面的示例,在该示例中,我将默认使用所有 crud 和 finder 功能,如果我想自定义一个 finder,那么这也可以在界面本身中轻松完成。

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

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

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

【问题讨论】:

    标签: java spring-data spring-data-jpa


    【解决方案1】:

    您需要为您的自定义方法创建一个单独的接口:

    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() { ... }
    }
    

    另请参阅:

    【讨论】:

    • 这个自定义实现可以注入实际的存储库,所以它可以使用那里定义的方法吗?具体来说,我想在更高级别的 find 实现中引用在 Repository 接口中定义的各种 find* 函数。由于那些 find*() 函数没有实现,我无法在自定义接口或 Impl 类中声明它们。
    • 我已经关注了这个答案,不幸的是,现在 Spring Data 正试图在我的“Account”对象上找到属性“customMethod”,因为它试图自动生成对 AccountRepository 上定义的所有方法的查询.有什么办法可以阻止这种情况?
    • @NickFoote 请注意,您实现存储库的类的名称应该是:AccountRepositoryImpl 而不是:AccountRepositoryCustomImpl 等 - 这是非常严格的命名约定。
    • @end-user:是的,你的 impl 对象可以注入仓库,没问题
    • 是的,如果您要扩展 QueryDslRepositorySupport,请参阅我之前关于它不起作用的评论您还必须通过字段或 setter 注入而不是构造函数注入来注入存储库,否则它将无法创建豆子。它似乎确实有效,但解决方案感觉有点“脏”,我不确定 Spring Data 团队是否有任何计划来改进它的工作方式。
    【解决方案2】:

    除了 axtavt 的 answer,如果您需要它来构建查询,请不要忘记您可以在自定义实现中注入 Entity Manager:

    public class AccountRepositoryImpl implements AccountRepositoryCustom {
    
        @PersistenceContext
        private EntityManager em;
    
        public void customMethod() { 
            ...
            em.createQuery(yourCriteria);
            ...
        }
    }
    

    【讨论】:

    • 谢谢,不过,我想知道如何在自定义实现中使用 Pageable 和 Page。有输入吗?
    • @WandMaker,只需将它们传递给您的自定义方法并在方法内部使用。
    【解决方案3】:

    如果您希望能够执行更复杂的操作,您可能需要访问 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 referralId) {
            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);
            }
        }
    
    }
    

    【讨论】:

      【解决方案4】:

      我使用以下代码从我的自定义实现中访问生成的查找方法。通过 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】:

        考虑到您的代码 sn-p,请注意您只能将 Native 对象传递给 findBy### 方法,假设您要加载属于某些客户的帐户列表,一种解决方案是这样做,

        @Query("Select a from Account a where a."#nameoffield"=?1")
        List<Account> findByCustomer(String "#nameoffield");
        

        使sue要查询的表名与Entity类相同。 如需进一步的实现,请查看this

        【讨论】:

        • 这是查询的错字,应该是nameoffield,我没有适当的权利来修正它。
        【解决方案6】:

        这里还有一个问题需要考虑。有些人希望向您的存储库添加自定义方法会自动将它们公开为“/search”链接下的 REST 服务。不幸的是,情况并非如此。 Spring 目前不支持。

        这是“设计使然”的功能,spring data rest 显式检查方法是否是自定义方法,并且不会将其公开为 REST 搜索链接:

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

        这是奥利弗·吉尔克的名言:

        这是设计使然。自定义存储库方法不是查询方法,因为 他们可以有效地实施任何行为。因此,目前 我们无法决定公开该方法的 HTTP 方法 在下面。 POST 将是最安全的选择,但这不符合 通用查询方法(接收 GET)。

        更多详情请看本期:https://jira.spring.io/browse/DATAREST-206

        【讨论】:

        • 很遗憾,我浪费了这么多时间试图找出我做错了什么,最后,我明白没有这样的功能。他们为什么还要实现这个功能?少吃豆子?把所有的道法都集中在一处?我本可以通过其他方式实现这一点。有谁知道“向单个存储库添加行为”功能的目标是什么?
        • 您可以通过 REST 公开任何存储库方法,只需在方法中添加 @RestResource(path = "myQueryMethod") 注释即可。上面的引用只是说明 Spring 不知道您希望它如何映射(即 GET 与 POST 等),因此您可以通过注释指定它。
        【解决方案7】:

        这在使用上是有限的,但是对于简单的自定义方法,您可以使用 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);
                }
            }
        }
        

        编辑:

        this spring 教程中是这样写的:

        Spring Data JPA 还允许您通过以下方式定义其他查询方法 只需声明他们的方法签名。

        所以甚至可以只声明这样的方法:

        Customer findByHobby(Hobby personHobby);
        

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

        【讨论】:

          【解决方案8】:

          接受的答案有效,但存在三个问题:

          • 在将自定义实现命名为AccountRepositoryImpl 时,它使用了未记录的 Spring Data 功能。 documentation 明确指出必须调用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
              }
          }
          

          【讨论】:

          • 这行得通。我想强调构造函数中参数名称的重要性,必须遵循这个答案中的约定(必须是accountRepositoryBasic)。否则 spring 抱怨有 2 个 bean 选择注入到我的 *Impl 构造函数中。
          • 那么AccountRepository有什么用
          • @KalpeshSoni 来自AccountRepositoryBasicAccountRepositoryCustom 的方法将通过注入的AccountRepository 提供
          • 您能否提供创建上下文的方式?我无法把它们放在一起。谢谢。
          【解决方案9】:

          我扩展了 SimpleJpaRepository:

          public class ExtendedRepositoryImpl<T extends EntityBean> extends SimpleJpaRepository<T, Long>
              implements ExtendedRepository<T> {
          
              private final JpaEntityInformation<T, ?> entityInformation;
          
              private final EntityManager em;
          
              public ExtendedRepositoryImpl(final JpaEntityInformation<T, ?> entityInformation,
                                                                final EntityManager entityManager) {
                 super(entityInformation, entityManager);
                 this.entityInformation = entityInformation;
                 this.em = entityManager;
              }
          }
          

          并将此类添加到@EnableJpaRepositoryries repositoryBaseClass。

          【讨论】:

            【解决方案10】:

            有一个稍作修改的解决方案,不需要额外的接口。

            正如documented functionality 中所指定的,Impl 后缀允许我们拥有这样干净的解决方案:

            • 在您的常规@Repository 接口中定义自定义方法,例如MyEntityRepository(除了您的Spring Data 方法)
            • 在任何地方(甚至不需要在同一个包中)创建一个类MyEntityRepositoryImplImpl 后缀是魔法)仅实现自定义方法@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 存储库之外,不需要任何接口。


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

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

            【讨论】:

            • 如何正确自动装配 MyEntityRepositoryImpl ?
            • @KonstantinZyubin 你自动接线MyEntityRepository,而不是*Impl
            • 令人惊讶的彻底、详细和有用的答案。肯定应该有更多的赞成票!
            • 非常有帮助的答案
            【解决方案11】:

            向所有存储库添加自定义行为:

            要向所有存储库添加自定义行为,您首先需要添加一个中间接口来声明共享行为。

            public interface MyRepository <T, ID extends Serializable> extends JpaRepository<T, ID>
            {
                
                void sharedCustomMethod( ID id );
            }
            

            现在您的个人存储库接口将扩展此中间接口而不是存储库接口以包含声明的功能。

            接下来,创建一个中间接口的实现,该接口扩展了特定于持久性技术的存储库基类。然后,此类将充当存储库代理的自定义基类。

            public class MyRepositoryImpl <T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID>
            {
                
                private EntityManager entityManager;
                
                   // There are two constructors to choose from, either can be used.
                public MyRepositoryImpl(Class<T> domainClass, EntityManager entityManager)
                {
                    super( domainClass, entityManager );
                    
                    // This is the recommended method for accessing inherited class dependencies.
                    this.entityManager = entityManager;
                }
                
                
                public void sharedCustomMethod( ID id )
                {
                    // implementation goes here
                }
            }
            

            Spring Data Repositories Part I. Reference

            【讨论】:

              【解决方案12】:

              我使用 SimpleJpaRepository 作为存储库实现的基类,并在接口中添加自定义方法,例如:

              public interface UserRepository  {
                  User FindOrInsert(int userId);
              }
              
              @Repository
              public class UserRepositoryImpl extends SimpleJpaRepository implements UserRepository {
              
                  private RedisClient redisClient;
              
                  public UserRepositoryImpl(RedisClient redisClient, EntityManager em) {
                      super(User.class, em);
                      this.redisClient = redisClient;
                  }
              
              
              @Override
              public User FindOrInsert(int userId) {
              
                  User u = redisClient.getOrSet("test key.. User.class, () -> {
                      Optional<User> ou = this.findById(Integer.valueOf(userId));
                      return ou.get();
                  });
                  …………
                  return u;
              }
              

              【讨论】:

                【解决方案13】:

                我喜欢 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);
                    }
                }
                

                您只需要某种方式来访问您的 db 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 崩溃了。我认为它不希望看到 Impl 甚至与 PagingAndSortingRepository 间接关联。

                【讨论】:

                  【解决方案14】:

                  我使用 mongo 和 spring 来解决这个问题。所以假设我们使用 MongoRepository 来提供基本的 crud 操作,假设我们需要使用 mongoTemplate 实现一些自定义条件查询操作。为了实现一个接口来为 crud 和 custom 注入存储库,我们需要指定:

                  自定义界面:

                  public interface UserCustomRepository {
                   List<User> findAllUsersBySomeCriteria(UserCriteriaRequest criteriaRequest);
                  }
                  

                  UserRepository 接口“必须”首先扩展 UserCustomRepository,然后是 MongoRepository

                  @Repository
                  public interface UserRepository extends UserCustomRepository, MongoRepository<User, ObjectId> {
                  }
                  

                  UserRepositoryImpl 必须与带有 *Impl 后缀的 crud 接口同名。

                  @Component
                  @NoArgsConstructor
                  @AllArgsConstructor(onConstructor = @__(@Autowired))
                  public class UserRepositoryImpl implements UserCustomRepository {
                  
                   private MongoTemplate mongoTemplate;
                  
                   @Override
                   public List<User> findAllUsersBySomeCriteria(UserCriteriaRequest criteriaRequest){
                    //some impl
                   }
                  }
                  

                  让我们实现一些服务 - 这里我们只注入 UserRepository 接口并使用来自 crud 存储库和自定义类 impl 的方法。

                  @Service
                  @NoArgsConstructor
                  @AllArgsConstructor(onConstructor = @__(@Autowired))
                  public class UserService {
                  
                   private UserRepository userReposityry;
                  
                   public List<User> getUserByCriteria(UserCriteriaRequest request) {
                     userRepository.findById(request.getUserId); // Crud repository method
                     userRepository.findAllUsersBySomeCriteria(request); // custom method.
                   }
                  }
                  

                  【讨论】:

                  • 哇 ;-) - 但不是这里的主题:我从未见过@AllArgsConstructor(onConstructor = @__(@Autowired)) 我应该怎么想。在没有 lombok 的情况下以标准方式实现它非常好或更好。有人认为这取决于您的同事(需要阅读代码的人)的设置。如果他们都熟悉,一切都很好。让人们改变很多或不想熟悉龙目岛可能会感到困惑。我猜想它看起来像是一种新的语法/插件方式,将功能插入到 java 类/对象中。 - 好的,我刚刚查了一下:lombok 声明它是实验性的!
                  猜你喜欢
                  • 2016-07-10
                  • 2015-06-16
                  • 1970-01-01
                  • 2017-06-28
                  • 2017-01-29
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多