从数据库中检索到的实体与查询中的情况相同

Posted

技术标签:

【中文标题】从数据库中检索到的实体与查询中的情况相同【英文标题】:Entity retrieved from database with same case as in query 【发布时间】:2017-10-25 06:08:23 【问题描述】:

我的数据库包含下表:

表:

country 
    code varchar(255) not null
        primary key
;

类:

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

    public String getCode() 
        return mCode;
    

    public void setCode(String code) 
        mCode = code;
    

示例表行:

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

当我使用以下 CrudRepository 检索国家/地区时:

public interface CountryRepository extends CrudRepository<Country, String> 

第一个场景:

mRepository.findOne("USA")

它会在我的 rest api 中给我以下结果:


  "code": "USA"

第二种情况:

mRepository.findOne("UsA")

它会在我的 rest api 中给我以下结果:


  "code": "UsA"

第三种情况:

mRepository.findOne("Usa")

它会在我的 rest api 中给我以下结果:


  "code": "Usa"

我还使用调试器检查了相同的行为,发现我在内存中的对象实际上具有相同的行为。

我想要的:我希望返回的数据与数据库中的大小写相同。

【问题讨论】:

salaam,您如何返回/转换发送回客户端的数据? return mRepository.findOne("UsA") 我的意思是从存储库返回的对象如何转换为前端接收的 json。 Java --> JSON? 我认为 Spring 默认使用 Jackson。但是这种行为发生在转换之前。在检索数据库行之后和返回对象之前,我使用调试器进行了检查。 您使用的是 mysql,对吗?请确保,您在表格列上使用了正确的字符集。名为 *_ci 的字符集不区分大小写,这会导致此问题。见dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html 【参考方案1】:

正如@Bedla 在评论中已经暗示的那样,您可能在数据库中使用不区分大小写的 varchar 数据类型。但是,不建议对 Hibernate 中的主键(一般情况下)这样做,因为 Hibernate 在引用持久性上下文中的实体时依赖 id 属性值的唯一性(如果启用,则为二级缓存)。

例如,在通过"USA""usa" 加载实体之后(或者在合并一个分离的"usa""USA" 已经加载之后等)在相同的持久化上下文中,您最终可能会得到持久化上下文中的两个不同实例,这进一步意味着它们将被分别刷新,从而覆盖彼此的更改,等等。

在数据库中使用区分大小写的数据类型,但允许忽略大小写进行搜索:

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 getByMCodeIgnoreCase(String code) 将给出相同的结果而没有@Query + IMO,您应该重写CrudRepository.findOne 方法以引发异常,因此开发人员只能使用正确的方法来获取实体。跨度> 【参考方案2】:

尝试覆盖您的实体中的equals()hashcode() 方法,以便它们考虑mCase 的情况(并且不要在这两种方法中使用任何其他字段)。

【讨论】:

【参考方案3】:

您可以将 CrudRepository 包装在另一个层,例如服务或控制器。

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

从 mRepository 返回的实体是存储在数据库中的。我认为您应该有另一种方法来进行这种特殊处理。

【讨论】:

【参考方案4】:

通过调用repository.findOne("Usa")(默认实现是SimpleJpaRepository.findOne)Hibernate 将使用EntityManager.find 实例化实体(如果它在数据库中找到并且不存在于一级和二级缓存中)并将传递的参数值设置为主键,使用 SessionImpl.instantiate 方法,而不是使用 Select 查询结果。 我在 Hibernate Jira 中为此问题填写了 ticket。

解决方案 1:

不要使用自然 id 作为主键:

如前所述,不建议使用业务/自然键作为主键,因为将来可能会随着业务规则的变化而变化(业务规则可以在未经许可的情况下更改!!)+如果您使用的是clustered index ,主键的字符串可能比较慢,也许你会在数据库中找到两行:Country('USA')Country('USA '),改用Surrogate Key,你可以查看这两个SO问题以获取更多详细信息:

What's the best practice for primary keys in tables? Strings as Primary Keys in SQL Database

如果您选择此选项,请不要忘记使用 @UniqueConstraint 为业务键映射唯一约束:

@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "code"))
public class Country 

    // Your code ...     

    @Column(name = "code", nullable=false)
    private String mCode;

    //...     

解决方案 2:

更改 Country.mCode 大小写:

如果您有可能将所有 Country.code 存储为大写:

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

解决方案 3:

自定义 CrudRepository:

在向您的存储库添加自定义函数时,您应该始终摆脱默认的findOne(String),这样您就可以强制其他人使用“最安全”的方法。

解决方案 3.1:

将custom implementations 用于mRepository 查找方法(我将其命名为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!");
    

解决方案 3.2:

您可以为您的存储库添加Query methods:

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

或使用@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!");
    

希望对你有帮助!

注意:我使用的是 Spring Data 1.11.8.RELEASE 和 Hibernate 5.2.10.Final。

【讨论】:

投反对票的人,请解释投反对票的原因?

以上是关于从数据库中检索到的实体与查询中的情况相同的主要内容,如果未能解决你的问题,请参考以下文章

EF Core性能优化

如何让 NHibernate ISession 缓存主键未检索到的实体

本机查询 - 如何仅从数据库中检索实体的特定列

将返回从选择查询中检索到的数据的函数 - Oracle

mybatis多表查询

从 Spark Cache VS 数据库中检索数据缓慢