6

My database contains the following table:

table:

country {
    code varchar(255) not null
        primary key
};

class:

@Entity
public class Country {
    @Id
    @Column(name = "code")
    private String mCode;

    public String getCode() {
        return mCode;
    }

    public void setCode(String code) {
        mCode = code;
    }
}

sample table rows:

| code |
|------|
| USA  |
| UK   |

When I retrieve a country using the following CrudRepository:

public interface CountryRepository extends CrudRepository<Country, String> {
}

First scenario:

mRepository.findOne("USA")

It will give me the following result in my rest api:

{
  "code": "USA"
}

Second scenario:

mRepository.findOne("UsA")

It will give me the following result in my rest api:

{
  "code": "UsA"
}

Third scenario:

mRepository.findOne("Usa")

It will give me the following result in my rest api:

{
  "code": "Usa"
}

I have also inspected the same behavior using the debugger and found my object in memory actually have the same behavior.

What I Want: I want the returned data to be the same case as in database.

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
malhobayyeb
  • 2,725
  • 11
  • 57
  • 91
  • salaam, how are you returning/converting the data that is being sent back to the client? – Abdullah Khan Oct 25 '17 at 06:43
  • `return mRepository.findOne("UsA")` – malhobayyeb Oct 25 '17 at 07:00
  • what i mean is how the object that is being returned from the repository is being converted to the json that the front-end receives. Java --> JSON? – Abdullah Khan Oct 25 '17 at 07:03
  • I think it is Spring's default to use Jackson. But the behavior happens before the conversion. I inspected that using the debugger after retrieving the database row and before returning the object. – malhobayyeb Oct 25 '17 at 07:18
  • This is not the usual scenario you must be setting it manually somewhere. – Abdullah Khan Oct 25 '17 at 07:30
  • I have set nothing :( – malhobayyeb Oct 25 '17 at 07:35
  • 4
    You are using MySQL, right? Be sure, you are using correct character set on your table column. Charsets named ***_ci** are case insensitive and it leads to this issue. See http://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html – Bedla Nov 01 '17 at 21:32
  • I'm 100% sure that you are returning the value which you are sending, instead of the database value. – Herr Derb Nov 08 '17 at 16:05

4 Answers4

8

As already hinted by @Bedla in the comment, you may be using a case insensitive varchar data type in your database. However, this is not recommended for primary keys in Hibernate (and in general), because Hibernate relies on id property value uniqueness when referring to entities in the persistence context (and second-level cache if enabled).

For example, after loading an entity by "USA" and then by "usa" (or after merging a detached "usa" while "USA" has already been loaded, etc) in the same persistence context you may end up with two different instances in the persistence context, meaning further that they would be flushed separately thus overwriting each other changes, etc.

Rather use a case sensitive data type in the database, but allow searching by ignoring case:

public interface CountryRepository extends CrudRepository<Country, String> {
    @Query("select c from Country c where upper(c.mCode) = upper(?1)")
    Country getCountry(String code);
}

PS Country codes are not good candidates for primary keys in general, because they can change.

Dragan Bozanovic
  • 23,102
  • 5
  • 43
  • 110
  • `Country getByMCodeIgnoreCase(String code)` will give the same result whithout `@Query` + IMO, you should override `CrudRepository.findOne` method to throw an exception, so developers can use only the correct method to get the entity. – O.Badr Nov 05 '17 at 01:17
0

Try overriding the equals() and hashcode() methods in your entity such that they take the case of mCase into consideration (and don't use any other fields in these two methods).

The Student Soul
  • 2,272
  • 2
  • 14
  • 12
0

you could wrap the CrudRepository with another layer such as service or controller.

public Country findOne(String code){
    Country country = mRepository.findOne(keyWord)
    if (country !=null){
        country.setCode(code)
        return country;
    }
    return null;
}

The entity returned from mRepository is what stored in database. I think you should have another method to do this special handling.

Ben Cheng
  • 769
  • 10
  • 25
-2

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.

O.Badr
  • 2,853
  • 2
  • 27
  • 36