By calling repository.findOne("Usa")
(default implementation is SimpleJpaRepository.findOne) Hibernate will use EntityManager.find which instantiates the entity(if it's found in the Database and not present in first and second level cache) and set the passed argument value as primary key, using SessionImpl.instantiate method, instead of using the Select query result.
I've filled a ticket in Hibernate Jira for this issue.
Solution 1:
Do not use natural id as a primary key:
As said, it's not recommended to use a business/natural key as primary key, as it may change in the future as business rules change (Business rules can change without permission!!) + If you're using clustered index, a string may be slower for primary key, and perhaps you will find two rows in the database: Country('USA')
, Country('USA ')
, use a Surrogate Key instead, you can check this two SO questions for more details:
If you choose this option don't forget to map a unique constraint for the business key using @UniqueConstraint:
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "code"))
public class Country {
// Your code ...
@Column(name = "code", nullable=false)
private String mCode;
//...
}
Solution 2:
Change Country.mCode
case:
If you have a possibility to store all Country.code
in UpperCase :
@Entity
public class Country {
private String mCode;
protected Country(){
}
public Country(String code){
this.mCode = code.toUpperCase()
}
@Id
@Column(name = "code")
public String getCode() {
return this.mCode;
}
// Hibernate will always use this setter to assign mCode value
private void setCode(String code) {
this.mCode = code.toUpperCase();
}
}
Solution 3:
Customize the CrudRepository:
While adding a custom function to your repository, you should always get rid of the default findOne(String)
, so you force others to use the "safest" method.
Solution 3.1:
Use custom implementations for mRepository
find method (I named it findOneWithRightStringCase
):
public interface CountryRepositoryCustom {
Country findOneWithRightStringCase(String id);
}
public class CountryRepositoryImpl implements CountryRepositoryCustom{
@PersistenceContext private EntityManager entityManager;
@Override
public Country findOneRespectCase(String id) {
try {
return entityManager.createQuery("SELECT c FROM Country c WHERE c.mCode=:code", Country.class).setParameter("code", id).getSingleResult();
} catch (NoResultException ex) {
return null;
}
}
}
public interface CountryRepository extends CrudRepository<Country, String>, CountryRepositoryCustom {
@Override
public default Country findOne(String id) {
throw new UnsupportedOperationException("[findOne(String)] may not match the case stored in database for [Country.code] value, use findOneRespectCase(String) instead!");
}
}
Solution 3.2:
You may add a Query methods for your repository :
public interface CountryRepository extends CrudRepository<Country, String> {
Country findByMCode(String mCode);
@Override
public default Country findOne(String id) {
throw new UnsupportedOperationException("[findOne(String)] may not match the case stored in database for [Country.code] value, use findByMCode(String) instead!");
}
}
Or Use @Query(JPQL):
public interface CountryRepository extends CrudRepository<Country, String> {
@Query("select c from Country c where c.mCode= ?1")
Country selectCountryByCode(String mCode);
@Override
public default Country findOne(String id) {
throw new UnsupportedOperationException("[findOne(String)] may not match the case stored in database for [Country.code] value, use selectCountryByCode(String) instead!");
}
}
Hope it helps!
Note: I'm using Spring Data 1.11.8.RELEASE and Hibernate 5.2.10.Final.