89

In JPA (Hibernate), when we automatically generate the ID field, it is assumed that the user has no knowledge about this key. So, when obtaining the entity, user would query based on some field other than ID. How do we obtain the entity in that case (since em.find() cannot be used).

I understand we can use a query and filter the results later. But, is there a more direct way (because this is a very common problem as I understand).

MD. Mohiuddin Ahmed
  • 2,092
  • 2
  • 26
  • 35
Neo
  • 3,465
  • 5
  • 26
  • 37

17 Answers17

46

It is not a "problem" as you stated it.

Hibernate has the built-in find(), but you have to build your own query in order to get a particular object. I recommend using Hibernate's Criteria :

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

This will create a criteria on your current class, adding the restriction that the column "yourField" is equal to the value yourFieldValue. uniqueResult() tells it to bring a unique result. If more objects match, you should retrive a list.

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

If you have any further questions, please feel free to ask. Hope this helps.

Raul Rene
  • 10,014
  • 9
  • 53
  • 75
  • Is this Hibernate specific? Can I use this with JPA as well? – Neo Feb 20 '13 at 10:19
  • `Criteria` is Hibernate specific because it comes from `org.hibernate` package. – Raul Rene Feb 20 '13 at 12:14
  • 15
    may you forgive my newbie question : what is the session instance ? – Alex Nov 27 '15 at 14:53
  • 1
    in your example, you're calling `session.createCriteria` on a class named `YourClass` but in the following examples you reference a class named `YourObject`. This is a typo, correct? – skeryl Jan 04 '16 at 16:58
  • createCriteria is deprecated since 5.2 in favor of using JPA criteria. https://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/SharedSessionContract.html#createCriteria-java.lang.Class- – marcioggs Aug 10 '22 at 12:12
34

if you have repository for entity Foo and need to select all entries with exact string value boo (also works for other primitive types or entity types). Put this into your repository interface:

List<Foo> findByBoo(String boo);

if you need to order results:

List<Foo> findByBooOrderById(String boo);

See more at reference.

cici
  • 389
  • 3
  • 8
11

Basically, you should add a specific unique field. I usually use xxxUri fields.

class User {

    @Id
    // automatically generated
    private Long id;

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

And you business method will do like this.

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;
    }
}
tonio
  • 10,355
  • 2
  • 46
  • 60
Jin Kwon
  • 20,295
  • 14
  • 115
  • 184
7

Best practice is using @NaturalId annotation. It can be used as the business key for some cases it is too complicated, so some fields are using as the identifier in the real world. For example, I have user class with user id as primary key, and email is also unique field. So we can use email as our natural 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;
}

To get our record, just simply use 'session.byNaturalId()'

Session session = sessionFactory.getCurrentSession();
User user = session.byNaturalId(User.class)
                   .using("email","huchenhai@qq.com")
                   .load()
Chenhai-胡晨海
  • 1,315
  • 9
  • 7
  • I think ,in your caase the email should be an Id . – Menai Ala Eddine - Aladdin Mar 29 '18 at 12:18
  • 1
    ya it was the legacy database created 10 years ago, sometime there is no way to fix around – Chenhai-胡晨海 Dec 13 '18 at 14:54
  • No, your email field should definitely NOT be an id. Email can change and id field is something that should be immutable as it is used as foreign key from other tables. Imagine using the email as id and referring it from 10 tables and then the user wants to change their email. Ouch. This is a rookie mistake that you should not be making. – Jukka Hämäläinen Jun 21 '23 at 14:16
  • Chenhai-胡晨海 I want to thank you for solving another problem not related to this question for me. Been using Hibernate 13 years now and did not know about the @NaturalId annotation. – Jukka Hämäläinen Jun 21 '23 at 14:20
7

I solved a similar problem, where I wanted to find a book by its isbnCode not by your id(primary key).

@Entity
public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
    private String isbnCode;

...

In the repository the method was created like @kamalveer singh mentioned. Note that the method name is findBy+fieldName (in my case: findByisbnCode):

@Repository
public interface BookRepository extends JpaRepository<Book, Integer> {

    Book findByisbnCode(String isbnCode);
}

Then, implemented the method in the service:

@Service
public class BookService {

    @Autowired
    private BookRepository repo;
    
    
    public Book findByIsbnCode(String isbnCode) {
        Book obj = repo.findByisbnCode(isbnCode);
        return obj; 
    }
}
romeucr
  • 169
  • 1
  • 13
6

This solution is from Beginning Hibernate book:

     Query<User> query = session.createQuery("from User u where u.scn=:scn", User.class);
     query.setParameter("scn", scn);
     User user = query.uniqueResult();    
J Asgarov
  • 2,526
  • 1
  • 8
  • 18
4

Write a custom method like this:

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;
}
Johannes Stadler
  • 737
  • 12
  • 24
tinker_fairy
  • 1,323
  • 1
  • 15
  • 18
  • 36
    I can't believe how much code that is for something so simple. That's an API fail IMO. – stephen.hanson May 13 '14 at 20:52
  • @stephen.hanson Makes me wonder if it's worth using ORM at all? But that would mean going back to JDBC and mapping ResultSets on your own etc etc which ain't nice either ; ( – parsecer Dec 16 '19 at 17:55
1

Edit: Just realized that @Chinmoy was getting at basically the same thing, but I think I may have done a better job ELI5 :)

If you're using a flavor of Spring Data to help persist / fetch things from whatever kind of Repository you've defined, you can probably have your JPA provider do this for you via some clever tricks with method names in your Repository interface class. Allow me to explain.

(As a disclaimer, I just a few moments ago did/still am figuring this out for myself.)

For example, if I am storing Tokens in my database, I might have an entity class that looks like this:

@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;
}

And I probably have a CrudRepository<K,V> interface defined like this, to give me simple CRUD operations on that Repository for free.

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

And when I'm looking up one of these tokens, my purpose might be checking the expiration or scope, for example. In either of those cases, I probably don't have the tokenId handy, but rather just the value of a token field itself that I want to look up.

To do that, you can add an additional method to your TokenRepository interface in a clever way to tell your JPA provider that the value you're passing in to the method is not the tokenId, but the value of another field within the Entity class, and it should take that into account when it is generating the actual SQL that it will run against your database.

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

I read about this on the Spring Data R2DBC docs page, and it seems to be working so far within a SpringBoot 2.x app storing in an embedded H2 database.

Ubunfu
  • 1,083
  • 2
  • 10
  • 21
1

All the answers require you to write some sort of SQL/HQL/whatever. Why? You don't have to - just use 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;
  }
}

example of use:

public static void main(String[] args)  {
  Person person = Dao.getPersonByName("John");
  System.out.println(person.getAge());  //John's age
}
amphibient
  • 29,770
  • 54
  • 146
  • 240
parsecer
  • 4,758
  • 13
  • 71
  • 140
1

No, you don't need to make criteria query it would be boilerplate code you just do simple thing if you working in Spring-boot: in your repo declare a method name with findBy[exact field name]. Example- if your model or document consist a string field myField and you want to find by it then your method name will be:

findBymyField(String myField);

0

Have a look at:

Wilhelm Kleu
  • 10,821
  • 4
  • 36
  • 48
0

I've written a library that helps do precisely this. It allows search by object simply by initializing only the fields you want to filter by: https://github.com/kg6zvp/GenericEntityEJB

KG6ZVP
  • 3,610
  • 4
  • 26
  • 45
0

In my Spring Boot app I resolved a similar type of issue like this:

@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;
}
Andrew Regan
  • 5,087
  • 6
  • 37
  • 73
Serenade
  • 309
  • 1
  • 4
  • 12
  • 1
    In springbot you can use repositories, and just defining methods like findByEmail(String email) in its interface will do the magic – kappa May 08 '19 at 06:40
  • 2
    I'm lost - what on earth did I get out of moving from SQL to JPA if I have to still manually write out queries as strings? – ArtOfWarfare Jun 19 '19 at 20:26
  • I'm not an expert, but I'm very concerned about what appears to be a welcoming invitation for SQL injection in this solution. Unless you can explain how this is not the case I would not recommend anyone ever do this. @Serenade, please don't take this personally :) – Ubunfu Sep 06 '19 at 19:34
  • 1
    @Ubunfu, as far as I am aware, this is an example of parameter binding (using `.setParameter()` on the `EntityManager` `Query`. The user data is not within the query string itself. From the docs I have found, using this approach means the JDBC driver will properly escape the data before the query is executed. For an example, check [here](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-persistence-api-jpa), [here](https://vladmihalcea.com/a-beginners-guide-to-sql-injection-and-how-you-should-prevent-it/), and [here](https://www.baeldung.com/sql-injection). – Serenade Sep 21 '19 at 11:56
  • @Ubunfu, also: If I've convinced you, could you remove the downvote? Thanks! – Serenade Sep 21 '19 at 12:04
  • Oh yea looks like you’re right. JPA makes best effort to protect you from SQL injection as long as you’re not using literal String concatenation to manually build a query. Unfortunately SO isn’t letting me remove the downvote unless you edit the answer?... I’ll try again later but to anyone who reads this just ignore my comment lol – Ubunfu Sep 21 '19 at 13:49
0

Refer - Spring docs for query methods

We can add methods in Spring Jpa by passing diff params in methods like:
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

// Enabling static ORDER BY for a query 

List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
Chinmoy
  • 1,391
  • 13
  • 14
0

This is very basic query :

Entity : Student

@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;

}

Repository Interface : 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);
} 

Note: findByColumnName : give results by criteria

List findByStudentName(String studentName) Internally convert into query : select * from Student where name='studentName'

@Transactional @Modifying Is useful when you want to remove persisted data from database.

Dheeraj Upadhyay
  • 336
  • 2
  • 12
0

I think all these answers are missing the easiest way of getting the id, which is returning the persisted entity right after persisting it. So if this is your entity:

public class Entity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
}

The ID is to be generated at flush when using entity manager. You can call flush the entity manager explicitly:

em.persist(entity);
em.flush();
return entity.getId();

(In fact above I would return the whole entity to the client instead of just the id, but this is only an example.)

Or you can just return the entity and when the transaction is ended, it will also be flushed and the id should be filled automatically to your entity:

@Transactional(readOnly = false)
@Override
public Entity store(Entity entity) {
    dao.store(entity);
    // the id should be filled after this gets returned as the transaction boundary is set for this method
    return entity;
}

As the question is very old, I will also add modern way with Spring Data:

@Transactional(readOnly = false)
@Override
public Entity store(Entity entity) {
    return getRepository().save(entity);
}

There is no need for extra fetches.

Jukka Hämäläinen
  • 647
  • 1
  • 6
  • 17
-2

Using CrudRepository and JPA query works for me:

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);

}

and you invoke the find custom method like this:

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();
}
kristianva
  • 65
  • 2