【问题标题】:Using TIMESTAMPDIFF with JPA criteria query and hibernate as the provider将 TIMESTAMPDIFF 与 JPA 条件查询和休眠作为提供者一起使用
【发布时间】:2014-04-20 04:06:41
【问题描述】:

我有一个数据表,其中包含 setup 和 release 列,两者都包含时间戳。我的目标是使用 CriteriaQuery 创建下面的 SQL 查询的等效项。

SQL查询:SELECT TIMESTAMPDIFF(SECOND, setup, released)) as sum_duration FROM calls

CriteriaBuilder#diff() 函数显然不起作用,因为它需要数字参数,所以我尝试使用 CriteriaBuilder#function

EntityManager entityManager = emProvider.get();

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

CriteriaQuery<Integer> query = criteriaBuilder.createQuery();

Root<Thingy> thingyRoot = query.from(Thingy.class);

Path<DateTime> setup = root.get("setup");
Path<DateTime> release = root.get("release");

Expression secondLiteral = criteriaBuilder.literal("SECOND");

Expression func = criteriaBuilder.function("TIMESTAMPDIFF", Integer.class, secondLiteral, setup, release);

entityManager.createQuery(query).getResultList();

但是,当我尝试运行此代码时,它引发了异常;看来文字没有呈现为常量,而是呈现为参数:

java.lang.IllegalStateException: No data type for node: org.hibernate.hql.internal.ast.tree.MethodNode 
 \-[METHOD_CALL] MethodNode: '('
    +-[METHOD_NAME] IdentNode: 'TIMESTAMPDIFF' {originalText=TIMESTAMPDIFF}
    \-[EXPR_LIST] SqlNode: 'exprList'
       +-[NAMED_PARAM] ParameterNode: '?' {name=param0, expectedType=null}
       +-[DOT] DotNode: 'cdr0_.setup' {propertyName=setup,dereferenceType=ALL,propertyPath=setup,path=generatedAlias0.setup,tableAlias=cdr0_,className=com.vtsl.domain.CDR,classAlias=generatedAlias0}
       |  +-[ALIAS_REF] IdentNode: 'cdr0_.id' {alias=generatedAlias0, className=com.vtsl.domain.CDR, tableAlias=cdr0_}
       |  \-[IDENT] IdentNode: 'setup' {originalText=setup}
       \-[DOT] DotNode: 'cdr0_.release' {propertyName=release,dereferenceType=ALL,propertyPath=release,path=generatedAlias0.release,tableAlias=cdr0_,className=com.vtsl.domain.CDR,classAlias=generatedAlias0}
          +-[ALIAS_REF] IdentNode: 'cdr0_.id' {alias=generatedAlias0, className=com.vtsl.domain.CDR, tableAlias=cdr0_}
          \-[IDENT] IdentNode: 'release' {originalText=release}

所以我尝试匿名覆盖LiteralExpression#render 以直接返回我提供给该方法的字符串,但是出现了这个异常。

java.lang.IllegalStateException: No data type for node: org.hibernate.hql.internal.ast.tree.MethodNode 
 \-[METHOD_CALL] MethodNode: '('
    +-[METHOD_NAME] IdentNode: 'TIMESTAMPDIFF' {originalText=TIMESTAMPDIFF}
    \-[EXPR_LIST] SqlNode: 'exprList'
       +-[IDENT] IdentNode: 'SECOND' {originalText=SECOND}
       +-[DOT] DotNode: 'cdr0_.setup' {propertyName=setup,dereferenceType=ALL,propertyPath=setup,path=generatedAlias0.setup,tableAlias=cdr0_,className=com.vtsl.domain.CDR,classAlias=generatedAlias0}
       |  +-[ALIAS_REF] IdentNode: 'cdr0_.id' {alias=generatedAlias0, className=com.vtsl.domain.CDR, tableAlias=cdr0_}
       |  \-[IDENT] IdentNode: 'setup' {originalText=setup}
       \-[DOT] DotNode: 'cdr0_.release' {propertyName=release,dereferenceType=ALL,propertyPath=release,path=generatedAlias0.release,tableAlias=cdr0_,className=com.vtsl.domain.CDR,classAlias=generatedAlias0}
          +-[ALIAS_REF] IdentNode: 'cdr0_.id' {alias=generatedAlias0, className=com.vtsl.domain.CDR, tableAlias=cdr0_}
          \-[IDENT] IdentNode: 'release' {originalText=release}

所以问题是:我怎样才能修复我正在尝试做的这个操作,或者实现最初的目标?

我正在使用 Hibernate,我的数据库是 MySQL。

【问题讨论】:

  • 嗨@WiseTree,我在使用 grails+hibernate+MySQL 时也面临同样的问题。如果您有解决方案,您可以发布它吗? -谢谢
  • 我遇到这个问题的软件被推迟了,所以不幸的是,我在弄清楚之前就停止了这个工作。
  • 对于这么简单的请求,我真的不知道该笑还是该哭。 JPA 的建设者们一定是对 SQL 恨之入骨。我遇到了同样的问题,但仍然没有办法(除了希望数据库中的任何内容都不会超过 35 天)

标签: java mysql sql hibernate jpa


【解决方案1】:

这里有一个比 Kalid Shah 的更通用的解决方案。

基本上,我们需要的是一个自定义的“不透明文字”Expression 类,它能够将任意标记表达式(如MONTHYEARINTERVAL 1 DAY 等)作为函数参数传递给 MySQL:

import javax.persistence.criteria.CriteriaBuilder;

import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
import org.hibernate.query.criteria.internal.compile.RenderingContext;
import org.hibernate.query.criteria.internal.expression.LiteralExpression;

/**
 * Represents an opaque literal that gets pass through JPA unaltered into SQL.
 */
@SuppressWarnings("serial")
public class OpaqueLiteralExpression extends LiteralExpression<Void> {

    private final String value;

    public OpaqueLiteralExpression(CriteriaBuilderImpl builder, String value) {
        super(builder, Void.class, null);
        if (value == null)
            throw new IllegalArgumentException("null value");
        this.value = value;
    }

// ExpressionImpl

    @Override
    public String render(RenderingContext renderingContext) {
        return this.value;
    }
}

那么你可以这样使用它:

final Expression<LocalDate> birthDate = student.get(Student_.birthDate);
final Expression<Integer> age = builder.function("TIMESTAMPDIFF", Integer.class,
  new OpaqueLiteralExpression(builder, "YEAR"), birthDate, LocalDate.now());

【讨论】:

    【解决方案2】:
    CASE WHEN 1=1 THEN TIMESTAMPDIFF(SECOND,startDatetime,endDatetime) ELSE 0 END
    

    用 case 语句模拟它。

    【讨论】:

      【解决方案3】:

      这里是对等价的JPA Criteria Query的解释

      SELECT * from calls where TIMESTAMPDIFF(SECOND, setup, released)

      首先,您必须创建单位表达式并从BasicFunctionExpression 扩展它,其中以“SECOND”参数为单位并仅覆盖其rendor(RenderingContext renderingContext) 方法。

      import java.io.Serializable;
      import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
      import org.hibernate.query.criteria.internal.compile.RenderingContext;
      import org.hibernate.query.criteria.internal.expression.function.BasicFunctionExpression;
      
      public class UnitExpression extends BasicFunctionExpression<String> implements Serializable {
      
        public UnitExpression(CriteriaBuilderImpl criteriaBuilder, Class<String> javaType,
            String functionName) {
          super(criteriaBuilder, javaType, functionName);
        }
      
        @Override
        public String render(RenderingContext renderingContext) {
          return getFunctionName();
        }
      }
      

      然后你在你的 JPA 条件查询中使用这个单位表达式。

      EntityManager entityManager = emProvider.get();
      CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
      Root<Calls> thingyRoot = query.from(Calls.class);
      
          Expression<String> second = new UnitExpression(null, String.class, "SECOND");
          Expression<Integer> timeInSec = cb.function(
              "TIMESTAMPDIFF",
              Integer.class,
              second ,
              root.<Timestamp>get("setup"),
              root.<Timestamp>get("release"));
          List<Predicate> conditions = new ArrayList<>();
          conditions.add(cb.lessThan(timeInSec, 3600));
          cq.where(conditions.toArray(new Predicate[]{}));
          return session.createQuery(cq);
      

      它正在工作。

      【讨论】:

      • 不需要将一个函数包装到另一个函数中。
      【解决方案4】:

      我遇到了同样的问题:SECOND 将被撇号包围,查询将引发异常。

      我通过以下代码解决了这个问题:

      CriteriaBuilder builder = em.getCriteriaBuilder();
      CriteriaQuery<MyEntity> cq = builder.createQuery( MyEntity.class );
      Root<MyEntity> root = cq.from( MyEntity.class );
      
      javax.persistence.criteria.Expression<java.sql.Time> timeDiff = builder.function(
                  "TIMEDIFF",
                  java.sql.Time.class,
                  root.<Date>get( "endDate" ),
                  root.<Date>get( "startDate" ) );
      javax.persistence.criteria.Expression<Integer> timeToSec = builder.function(
                  "TIME_TO_SEC",
                  Integer.class,
                  timeDiff );
      
      //lessThanOrEqualTo 60 minutes
      cq.where( builder.lessThanOrEqualTo( timeToSec, 3600 ) );
      
      return em.createQuery( cq ).getResultList();
      

      这给了我同样的结果。

      【讨论】:

      • 我遇到了同样的错误,这可能是迄今为止最好的解决方案,谢谢! OP 应将此答案标记为已接受。
      • 谢谢,我将在这里强调关键思想:建议将 TIMESTAMPDIFF(SECOND, START_TIME, END_TIME) 替换为 TIME_TO_SEC(TIMEDIFF(START_TIME, END_TIME))。这将有助于避免 SECOND 文字或关键字,不知道如何调用它。
      • 需要注意的是,与TIMESTAMPDIFF不同,TIMEDIFF返回的是一个TIME对象,并且限制为35天。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-03-20
      • 2017-10-15
      • 2012-10-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-08
      相关资源
      最近更新 更多