【问题标题】:JPA: How to get entity based on field value other than ID?JPA:如何根据 ID 以外的字段值获取实体?
【发布时间】:2013-02-20 10:01:56
【问题描述】:

在JPA(Hibernate)中,当我们自动生成ID字段时,假设用户不知道这个key。因此,在获取实体时,用户会根据 ID 以外的某些字段进行查询。在这种情况下我们如何获取实体(因为不能使用em.find())。

我知道我们可以稍后使用查询并过滤结果。但是,有没有更直接的方法(因为据我了解,这是一个非常普遍的问题)。

【问题讨论】:

  • 那么哪个更好用?你会接受哪个答案?

标签: java hibernate jpa


【解决方案1】:

这不是你所说的“问题”。

Hibernate 具有内置的find(),但您必须构建自己的查询才能获取特定对象。我推荐使用HibernateCriteria

Criteria criteria = session.createCriteria(YourClass.class);
YourObject yourObject = criteria.add(Restrictions.eq("yourField", yourFieldValue))
                             .uniqueResult();

这将在您当前的类上创建一个criteria,并添加“yourField”列等于值yourFieldValue 的限制。 uniqueResult() 告诉它带来独特的结果。如果有更多对象匹配,您应该检索一个列表。

List<YourObject> list = criteria.add(Restrictions.eq("yourField", yourFieldValue)).list();

如果您还有其他问题,请随时提出。希望这会有所帮助。

【讨论】:

  • 这是 Hibernate 特有的吗?我也可以将它与 JPA 一起使用吗?
  • Criteria 是 Hibernate 特有的,因为它来自 org.hibernate 包。
  • 请原谅我的新手问题:会话实例是什么?
  • 在您的示例中,您在名为 YourClass 的类上调用 session.createCriteria,但在以下示例中,您引用了名为 YourObject 的类。这是一个错字,对吗?
【解决方案2】:

如果您有实体Foo 的存储库,并且需要选择具有精确字符串值boo 的所有条目(也适用于其他原始类型或实体类型)。将其放入您的存储库界面:

List<Foo> findByBoo(String boo);

如果您需要订购结果:

List<Foo> findByBooOrderById(String boo);

reference 上查看更多信息。

【讨论】:

  • 请注意,这仅对 Spring 有效,一般对 Hibernate 或 JPA 无效。
【解决方案3】:

基本上,您应该添加一个特定的唯一字段。我通常使用xxxUri 字段。

class User {

    @Id
    // automatically generated
    private Long id;

    // globally unique id
    @Column(name = "SCN", nullable = false, unique = true)
    private String scn;
}

你的业务方法会这样做。

public User findUserByScn(@NotNull final String scn) {
    CriteriaBuilder builder = manager.getCriteriaBuilder();
    CriteriaQuery<User> criteria = builder.createQuery(User.class);
    Root<User> from = criteria.from(User.class);
    criteria.select(from);
    criteria.where(builder.equal(from.get(User_.scn), scn));
    TypedQuery<User> typed = manager.createQuery(criteria);
    try {
        return typed.getSingleResult();
    } catch (final NoResultException nre) {
        return null;
    }
}

【讨论】:

【解决方案4】:

最佳实践是使用@NaturalId 注释。它可以用作某些情况下的业务密钥它太复杂了,因此某些字段在现实世界中用作标识符。 例如,我有以用户 ID 作为主键的用户类,电子邮件也是唯一字段。所以我们可以使用 email 作为我们的自然 id

@Entity
@Table(name="user")
public class User {
  @Id
  @Column(name="id")
  private int id;

  @NaturalId
  @Column(name="email")
  private String email;

  @Column(name="name")
  private String name;
}

要获得我们的记录,只需简单地使用 'session.byNaturalId()'

Session session = sessionFactory.getCurrentSession();
User user = session.byNaturalId(User.class)
                   .using("email","huchenhai@qq.com")
                   .load()

【讨论】:

  • 我认为,在你的情况下,电子邮件应该是一个 ID。
  • 是的,它是 10 年前创建的遗留数据库,有时无法修复
【解决方案5】:

此解决方案来自《Beginning Hibernate》一书:

     Query<User> query = session.createQuery("from User u where u.scn=:scn", User.class);
     query.setParameter("scn", scn);
     User user = query.uniqueResult();    

【讨论】:

    【解决方案6】:

    像这样写一个自定义方法:

    public Object findByYourField(Class entityClass, String yourFieldValue)
    {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery(entityClass);
        Root<Object> root = criteriaQuery.from(entityClass);
        criteriaQuery.select(root);
    
        ParameterExpression<String> params = criteriaBuilder.parameter(String.class);
        criteriaQuery.where(criteriaBuilder.equal(root.get("yourField"), params));
    
        TypedQuery<Object> query = entityManager.createQuery(criteriaQuery);
        query.setParameter(params, yourFieldValue);
    
        List<Object> queryResult = query.getResultList();
    
        Object returnObject = null;
    
        if (CollectionUtils.isNotEmpty(queryResult)) {
            returnObject = queryResult.get(0);
        }
    
        return returnObject;
    }
    

    【讨论】:

    • 我无法相信这么简单的事情需要多少代码。这是一个 API 失败 IMO。
    • @stephen.hanson 让我想知道是否值得使用 ORM?但这意味着要回到 JDBC 并自己映射 ResultSet 等等,这也不好; (
    【解决方案7】:

    我解决了一个类似的问题,我想通过 isbnCode 而不是你的 id(主键)来查找一本书。

    @Entity
    public class Book implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @Id
        @GeneratedValue(strategy=GenerationType.IDENTITY)
        private Integer id;
        private String isbnCode;
    
    ...
    

    在存储库中,该方法的创建方式与提到的@kamalveer singh 类似。注意方法名是findBy+fieldName(在我的例子中:findByisbnCode):

    @Repository
    public interface BookRepository extends JpaRepository<Book, Integer> {
    
        Book findByisbnCode(String isbnCode);
    }
    

    然后,在服务中实现该方法:

    @Service
    public class BookService {
    
        @Autowired
        private BookRepository repo;
        
        
        public Book findByIsbnCode(String isbnCode) {
            Book obj = repo.findByisbnCode(isbnCode);
            return obj; 
        }
    }
    

    【讨论】:

    • 感觉是最好的答案。
    【解决方案8】:

    编辑:刚刚意识到@Chinmoy 基本上是在做同样的事情,但我认为我可能在 ELI5 上做得更好 :)

    如果您使用 Spring Data 的风格来帮助从您定义的任何类型的 Repository 中持久化/获取内容,您可以让您的 JPA 提供者通过一些巧妙的技巧为您执行此操作你的Repository 接口类。请允许我解释一下。

    (作为免责声明,我刚才确实/仍在为自己解决这个问题。)

    例如,如果我将 Tokens 存储在我的数据库中,我可能有一个如下所示的实体类:

    @Data // << Project Lombok convenience annotation
    @Entity
    public class Token {
        @Id
        @Column(name = "TOKEN_ID")
        private String tokenId;
    
        @Column(name = "TOKEN")
        private String token;
    
        @Column(name = "EXPIRATION")
        private String expiration;
    
        @Column(name = "SCOPE")
        private String scope;
    }
    

    我可能有一个像这样定义的CrudRepository&lt;K,V&gt; 接口,以便免费为我提供对该存储库的简单 CRUD 操作。

    @Repository
    // CrudRepository<{Entity Type}, {Entity Primary Key Type}>
    public interface TokenRepository extends CrudRepository<Token, String> { }
    

    例如,当我查找其中一个令牌时,我的目的可能是检查到期时间或范围。在这两种情况下,我可能没有方便的tokenId,而只是我想要查找的token 字段本身的值。

    为此,您可以巧妙地向您的TokenRepository 接口添加一个额外的方法,告诉您的JPA 提供者您传递给该方法的值不是tokenId,而是Entity 类中的另一个字段,它应该在生成将针对您的数据库运行的实际 SQL 时考虑到这一点。

    @Repository
    // CrudRepository<{Entity Type}, {Entity Primary Key Type}>
    public interface TokenRepository extends CrudRepository<Token, String> { 
        List<Token> findByToken(String token);
    }
    

    我在 Spring Data R2DBC docs page 上读到了这个,到目前为止,它似乎在存储在嵌入式 H2 数据库中的 SpringBoot 2.x 应用程序中工作。

    【讨论】:

      【解决方案9】:

      不,您不需要进行标准查询,如果您在 Spring-boot 中工作,您只需做简单的事情就可以了样板代码: 在您的 repo 中使用 findBy[exact field name] 声明一个方法名称。 例子- 如果您的模型或文档包含一个字符串字段 myField 并且您想通过它查找,那么您的方法名称将是:

      findBymyField(String myField);

      【讨论】:

        【解决方案10】:

        所有答案都要求您编写某种 SQL/HQL/任何东西。为什么?您不必 - 只需使用 CriteriaBuilder:

        Person.java

        @Entity
        class Person  {
          @Id @GeneratedValue
          private int id;
        
          @Column(name = "name")
          private String name;
          @Column(name = "age")
          private int age;
          ...
        }
        

        Dao.java

        public class Dao  {
          public static Person getPersonByName(String name)  {
                SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
                Session session = sessionFactory.openSession();
                session.beginTransaction();
                
                CriteriaBuilder cb = session.getCriteriaBuilder();
                
                CriteriaQuery<Person> cr = cb.createQuery(Person.class);
                Root<Person> root = cr.from(Person.class);
                cr.select(root).where(cb.equal(root.get("name"), name));  //here you pass a class field, not a table column (in this example they are called the same)
        
                Query query = session.createQuery(cr);
                query.setMaxResults(1);
                List<Person> resultList = query.getResultList();
        
                Person result = resultList.get(0);
                
                return result;
          }
        }
        

        使用示例:

        public static void main(String[] args)  {
          Person person = Dao.getPersonByName("John");
          System.out.println(person.getAge());  //John's age
        }
        

        【讨论】:

        • 我认为这是最好的答案。考虑我的修改
        • @amphibient 谢谢! Queryquery 是它应该的样子,还是一个错字?
        • 没有输入
        【解决方案11】:

        看看:

        【讨论】:

          【解决方案12】:

          我已经编写了一个库来帮助做到这一点。它允许通过仅初始化您要过滤的字段来按对象搜索:https://github.com/kg6zvp/GenericEntityEJB

          【讨论】:

            【解决方案13】:

            参考 - Spring docs for query methods

            我们可以在 Spring Jpa 中通过传递 diff 参数来添加方法,例如:
            List&lt;Person&gt; findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

            // Enabling static ORDER BY for a query 
            
            List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
            

            【讨论】:

            • 问题不在于 Spring,而在于 JPA 和 Hibernate。它们可以在 Spring 之外使用。
            【解决方案14】:

            在我的 Spring Boot 应用程序中,我解决了类似的问题,如下所示:

            @Autowired
            private EntityManager entityManager;
            
            public User findByEmail(String email) {
                User user = null;
                Query query = entityManager.createQuery("SELECT u FROM User u WHERE u.email=:email");
                query.setParameter("email", email);
                try {
                    user = (User) query.getSingleResult();
                } catch (Exception e) {
                    // Handle exception
                }
                return user;
            }
            

            【讨论】:

            • 在springbot中你可以使用repositories,只要在它的接口中定义像findByEmail(String email)这样的方法就可以了
            • 我迷路了——如果我仍然必须手动将查询写成字符串,我到底能从 SQL 迁移到 JPA 中得到什么?
            • 我不是专家,但我非常担心在此解决方案中似乎是对 SQL 注入的欢迎邀请。除非你能解释为什么不是这样,否则我不会建议任何人这样做。 @Serenade,请不要把这当成个人 :)
            • @Ubunfu,据我所知,这是一个参数绑定示例(在EntityManager Query 上使用.setParameter()。用户数据不在查询字符串本身内。从我找到的文档中,使用这种方法意味着 JDBC 驱动程序将在执行查询之前正确转义数据。例如,检查 hereherehere
            • @Ubunfu,另外:如果我说服了你,你能删除反对票吗?谢谢!
            【解决方案15】:

            这是非常基本的查询:

            实体:学生

            @Entity
            @Data
            @NoArgsConstructor
            public class Student{
            @Id
            @GeneratedValue(generator = "uuid2", strategy = GenerationType.IDENTITY)
            @GenericGenerator(name = "uuid2", strategy = "uuid2")
            private String id;
            
            @Column(nullable = false)
            @Version
            @JsonIgnore
            private Integer version;
            
            private String studentId;
            
            private String studentName;
            
            private OffsetDateTime enrollDate;
            
            }
            

            存储库接口:StudentRepository

            @Repository
            public interface StudentRepository extends JpaRepository<Student, String> {
            
            List<Student> findByStudentName(String studentName);
            
            List<Student> findByStudentNameOrderByEnrollDateDesc(String studentName);
            
            @Transactional
            @Modifying
            void deleteByStudentName(String studentName);
            } 
            

            注意: findByColumnName : 按条件给出结果

            列出 findByStudentName(String studentName) 内部转换为查询:select * from Student where name='studentName'

            @Transactional @修改 当您想从数据库中删除持久数据时很有用。

            【讨论】:

              【解决方案16】:

              使用 CrudRepository 和 JPA 查询对我有用:

              import org.springframework.data.jpa.repository.Query;
              import org.springframework.data.repository.CrudRepository;
              import org.springframework.data.repository.query.Param;
              
              public interface TokenCrudRepository extends CrudRepository<Token, Integer> {
              
               /**
               * Finds a token by using the user as a search criteria.
               * @param user
               * @return  A token element matching with the given user.
               */
                  @Query("SELECT t FROM Token t WHERE LOWER(t.user) = LOWER(:user)")
                  public Token find(@Param("user") String user);
              
              }
              

              然后你像这样调用 find 自定义方法:

              public void destroyCurrentToken(String user){
                  AbstractApplicationContext context = getContext();
              
                  repository = context.getBean(TokenCrudRepository.class);
              
                  Token token = ((TokenCrudRepository) repository).find(user);
              
                  int idToken = token.getId();
              
                  repository.delete(idToken);
              
                  context.close();
              }
              

              【讨论】:

                猜你喜欢
                • 2011-05-09
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2017-05-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多