带有时间戳的 JPA @Convert 问题

Posted

技术标签:

【中文标题】带有时间戳的 JPA @Convert 问题【英文标题】:JPA @Convert issue with timestamp 【发布时间】:2018-05-04 10:49:10 【问题描述】:

我想使用带有 AES 算法的 JPA @Convert 选项加密存储在 mysql 数据库中的一些数据。一般来说,在所有领域都可以正常工作,但我遇到了其中一个时间戳的问题。我的 Hibernate 版本是 4.3.8.Final。

因为这是我第一次使用转换器,所以我关注了这个GiT example。对于这个测试,AES 加密被禁用,我稍后会启用它,这也是我想将一些字段转换为字符串的原因。因此问题必须在转换器中。

实体存储用户的几个典型信息(姓名、姓氏……)和存储为时间戳的出生日期。此外,由于我想通过birthdate 执行一些搜索,因此我删除了 setter 实体中的所有小时、秒和毫秒的生日。

public class User 
  @Column(length = 100, nullable = false)
  @Convert(converter = StringCryptoConverter.class)
  private String firstname;

  ....

  @Column(nullable = false)
  @Convert(converter = TimestampCryptoConverter.class)
  private Timestamp birthdate;

  public void setBirthdate(Timestamp birthdate) 
    // Remove time from birthdate. Is useless and can cause troubles when
    // using in SQL queries.
    if (birthdate != null) 
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(birthdate.getTime());
        calendar.set(Calendar.HOUR_OF_DAY, 0); // set hour to midnight
        calendar.set(Calendar.MINUTE, 0); // set minute in hour
        calendar.set(Calendar.SECOND, 0); // set second in minute
        calendar.set(Calendar.MILLISECOND, 0); // set millis in second
        this.birthdate = new Timestamp(calendar.getTime().getTime());
     else 
        this.birthdate = null;
    
  

....

TimestampCryptoConverter.class 有几个非常简单的方法。一般来说,范围是将时间戳转换为字符串以稍后应用 AES 算法,我将时间戳的时间与getTime() 一起取长,并将它们转换为字符串:

@Override
protected Timestamp stringToEntityAttribute(String dbData) 
    try 
        return (dbData == null || dbData.isEmpty()) ? null : new Timestamp(Long.parseLong(dbData));
     catch (NumberFormatException nfe) 
        Logger.errorMessage("Invalid long value in database.");
        return null;
    


@Override
protected String entityAttributeToString(Timestamp attribute) 
    return attribute == null ? null : attribute.getTime() + "";

这是一个非常简单的代码。而且我可以正确地将实体存储到数据库中,并正确地从数据库中检索它,例如,如果我通过 ID 获取用户。因此,转换器必须是正确的。

存储到 MySQL 中的数据是这样的:

# id, birthdate, firstname, ...
'1', '1525384800000', 'TEST', ...

如果我按任何字段搜索用户,我会检索正确转换所有数据的实体。当我想从出生日期执行搜索时出现此问题。比如在我的DAO中,我有一个方法是:

public List<User> get(String email, Timestamp birthdate) 
    // Get the criteria builder instance from entity manager
    CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
    CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(getEntityClass());
    // Tell to criteria query which tables/entities you want to fetch
    Root<User> typesRoot = criteriaQuery.from(getEntityClass());

    List<Predicate> predicates = new ArrayList<Predicate>();
    predicates.add(criteriaBuilder.equal(typesRoot.get("email"), email));

    if (birthdate != null) 
        // Remove hours and seconds.
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(birthdate.getTime());
        calendar.set(Calendar.HOUR_OF_DAY, 0); // set hour to midnight
        calendar.set(Calendar.MINUTE, 0); // set minute in hour
        calendar.set(Calendar.SECOND, 0); // set second in minute
        calendar.set(Calendar.MILLISECOND, 0); // set millis in second
        birthdate = new Timestamp(calendar.getTime().getTime());
        predicates.add(criteriaBuilder.equal(typesRoot.<Timestamp> get("birthdate"), birthdate));
    

    criteriaQuery.where(criteriaBuilder.and(predicates.toArray(new Predicate[] )));

    return getEntityManager().createQuery(criteriaQuery).getResultList();

如您所见,我还从搜索查询中删除了小时、秒和毫秒,以匹配数据库中的值。

如果我只使用电子邮件get('test@email.com', null) 调用此方法,它可以正常工作,就像检索用户之前一样,并且用户的生日是正确的。

但是如果我用生日get('test@email.com', 2018-05-04 12:09:05.862) 调用这个方法,那么得到的结果是null。在某些单一测试中,调用中使用的时间戳与用于创建用户的参数完全相同,因此必须与数据库上的值匹配。例如,我有这个单一的测试:

@Test(dependsOnMethods =  "storeUser" )
@Rollback(value = false)
@Transactional(value = TxType.NEVER)
public void searchByMailUser() 
    Assert.assertEquals(userDao.getRowCount(), 1);
    List<User> dbUsers = userDao.get(EMAIL, null);
    Assert.assertTrue(!dbUsers.isEmpty());

    User dbUser = dbUsers.iterator().next();
    Assert.assertEquals(dbUser.getFirstname(), FIRSTNAME);
    Assert.assertEquals(dbUser.getEmail(), EMAIL);
    ....        

    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(BIRTHDATE.getTime());
    calendar.set(Calendar.HOUR_OF_DAY, 0); // set hour to midnight
    calendar.set(Calendar.MINUTE, 0); // set minute in hour
    calendar.set(Calendar.SECOND, 0); // set second in minute
    calendar.set(Calendar.MILLISECOND, 0); // set millis in second
    Timestamp birthdate = new Timestamp(calendar.getTime().getTime());
    Assert.assertEquals(dbUser.getBirthdate(), birthdate);

执行得很好,最后一个断言告诉我生日已正确存储和检索。但是在这个测试中:

@Test(dependsOnMethods =  "storeUser" )
public void searchByMailAndBirthdateUser() 
    Assert.assertEquals(userDao.getRowCount(), 1);
    Assert.assertTrue(!userDao.get(EMAIL, BIRTHDATE).isEmpty());

此测试因未找到用户而失败,但如果更改为:则通过:

@Test(dependsOnMethods =  "storeUser" )
public void searchByMailAndBirthdateUser() 
    Assert.assertEquals(userDao.getRowCount(), 1);
    Assert.assertTrue(!userDao.get(EMAIL, null).isEmpty());

但是,如果我禁用转换器,则两个测试都通过了。

如果从数据库中正确检索出生日期。为什么在使用生日作为标准时我有一个null 值?

编辑

好像调用get('test@email.com', 2018-05-04 12:09:05.862)的时候没有使用protected String entityAttributeToString(Timestamp attribute);这个方法。

【问题讨论】:

【参考方案1】:

在some research 之后。在过滤带有@Convert 属性的条件时,Hibernate 可能仍然存在一些错误。我用不同的选项进行了几次测试,但都没有成功。

作为一种解决方法,我已将 birthdate 属性更改为 Long。

@Column(nullable = false)
@Convert(converter = LongCryptoConverter.class)
private Long birthdate;

更新 setter 和 getter 以使用 getTime() 将 Timestamp 转换为 Long

并使用非常简单的方法创建一个 Long CryptoConverter,将 Long 转换为 String 和相反:

@Override
protected Long stringToEntityAttribute(String dbData) 
    try 
        return (dbData == null || dbData.isEmpty()) ? null : Long.parseLong(dbData);
     catch (NumberFormatException nfe) 
        UsmoLogger.errorMessage(this.getClass().getName(), "Invalid long value in database.");
        return null;
    


@Override
protected String entityAttributeToString(Long attribute) 
    return attribute == null ? null : attribute.toString();

这按预期工作,标准过滤器现在工作正常。我仍然不确定为什么带有时间戳的版本不能正常工作。

【讨论】:

【参考方案2】:

经过一番努力,我已将 Hibernate 的版本更改为 5.2.17.Final,并且使用 @Convert 注释正确处理了时间戳。这意味着这确实是版本为4.X 的Hibernate 错误,并且此功能在Hibernate 5.X 中得到了更好的实现

【讨论】:

以上是关于带有时间戳的 JPA @Convert 问题的主要内容,如果未能解决你的问题,请参考以下文章

shell脚本每天创建带有时间戳的文件夹并推送时间戳生成的日志

带有日期和时间戳的 BBD 功能

无法保存带有时间戳的新文件

带有时间戳的 postgreSQL 排序

Sqoop 带有 Epoch 时间戳的增量负载

带时间戳的 DB2 变量