使用Hibernate从数据库获取下一个序列值

34

我有一个实体,其中有一个非ID字段必须从序列中设置。 目前,我会获取序列的第一个值,并将其存储在客户端端,然后从该值进行计算。

但是,我正在寻找一种更好的方法来做到这一点。我已经实现了一种获取下一个序列值的方式:

public Long getNextKey()
{
    Query query = session.createSQLQuery( "select nextval('mySequence')" );
    Long key = ((BigInteger) query.uniqueResult()).longValue();
    return key;
}

然而,这种方式会显著降低性能(创建约5000个对象的速度减缓了3倍,从5740毫秒变为13648毫秒)。

我曾试图添加一个“假”的实体:

@Entity
@SequenceGenerator(name = "sequence", sequenceName = "mySequence")
public class SequenceFetcher
{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence")
    private long                      id;

    public long getId() {
        return id;
    }
}

然而,这种方法也行不通(返回的所有 Id 都为 0)。

有没有人能够指导我如何高效地使用 Hibernate 获取下一个序列值?

编辑:经过调查,我发现调用 Query query = session.createSQLQuery("select nextval('mySequence')"); 的效率远不及使用 @GeneratedValue - 这是因为 Hibernate 在访问由 @GeneratedValue 描述的序列时,某种方式成功地减少了获取次数。

例如,当我创建 70,000 个实体时(因此从同一序列获取 70,000 个主键),我得到了我需要的一切。

然而,Hibernate 只发出了 1404select nextval ('local_key_sequence') 命令。注意:在数据库端,缓存设置为 1。

如果我尝试手动获取所有数据,那么将需要 70,000 次查询,因此性能存在很大差异。有没有人知道 Hibernate 的内部功能,以及如何手动复制它?

10个回答

30

您可以按照以下方式使用Hibernate Dialect API实现数据库的独立性

class SequenceValueGetter {
    private SessionFactory sessionFactory;

    // For Hibernate 3
    public Long getId(final String sequenceName) {
        final List<Long> ids = new ArrayList<Long>(1);

        sessionFactory.getCurrentSession().doWork(new Work() {
            public void execute(Connection connection) throws SQLException {
                DialectResolver dialectResolver = new StandardDialectResolver();
                Dialect dialect =  dialectResolver.resolveDialect(connection.getMetaData());
                PreparedStatement preparedStatement = null;
                ResultSet resultSet = null;
                try {
                    preparedStatement = connection.prepareStatement( dialect.getSequenceNextValString(sequenceName));
                    resultSet = preparedStatement.executeQuery();
                    resultSet.next();
                    ids.add(resultSet.getLong(1));
                }catch (SQLException e) {
                    throw e;
                } finally {
                    if(preparedStatement != null) {
                        preparedStatement.close();
                    }
                    if(resultSet != null) {
                        resultSet.close();
                    }
                }
            }
        });
        return ids.get(0);
    }

    // For Hibernate 4
    public Long getID(final String sequenceName) {
        ReturningWork<Long> maxReturningWork = new ReturningWork<Long>() {
            @Override
            public Long execute(Connection connection) throws SQLException {
                DialectResolver dialectResolver = new StandardDialectResolver();
                Dialect dialect =  dialectResolver.resolveDialect(connection.getMetaData());
                PreparedStatement preparedStatement = null;
                ResultSet resultSet = null;
                try {
                    preparedStatement = connection.prepareStatement( dialect.getSequenceNextValString(sequenceName));
                    resultSet = preparedStatement.executeQuery();
                    resultSet.next();
                    return resultSet.getLong(1);
                }catch (SQLException e) {
                    throw e;
                } finally {
                    if(preparedStatement != null) {
                        preparedStatement.close();
                    }
                    if(resultSet != null) {
                        resultSet.close();
                    }
                }

            }
        };
        Long maxRecord = sessionFactory.getCurrentSession().doReturningWork(maxReturningWork);
        return maxRecord;
    }

}

1
这很好,除了... - Marc
4
我认为这是唯一与数据库无关的答案,这正是我所需要的。(我们有一个应用程序使用Hibernate 4需要从DB序列中获取非ID值,在测试中使用HSQLDB,在实际操作中使用Oracle。)使用Java 7的try-with-resources可以缩短代码。在传递给resolveDialect()之前,我必须使用DatabaseMetaDataDialectResolutionInfoAdapter来包装connection.getMetaData()的结果。感谢@punitpatel! - Ricardo van den Broek
实际上,我必须补充说明这里使用了 dialect.getSequenceNextValString(),对于不支持序列的数据库将无法正常工作... - Ricardo van den Broek
你可能想要使用doReturningWork(可能比3更新)。此外,getSequenceNextValString(...)不会引用或验证序列名称,并且在本评论发布时存在注入漏洞。因此,不要从用户输入中获取序列名称,尽管这种情况相当不太可能发生。 - xenoterracide

25

以下是对我有效的解决方案(特定于Oracle,但使用 scalar 似乎是关键)

Long getNext() {
    Query query = 
        session.createSQLQuery("select MYSEQ.nextval as num from dual")
            .addScalar("num", StandardBasicTypes.BIG_INTEGER);

    return ((BigInteger) query.uniqueResult()).longValue();
}

感谢这里的帖子作者:springsource_forum


3
请注意,您也可以使用StandardBasicTypes.LONG替代StandardBasicTypes.BIG_INTEGER... (翻译:作为注释,您可以使用StandardBasicTypes.LONG而不是StandardBasicTypes.BIG_INTEGER。) - rogerdpack
显然,你不需要 addScalar 部分(Hibernate 会根据你的数据库序列底层类型返回 Integer 或 BigInteger),但由于它可能在不同的数据库和序列之间有所不同,为什么不保证它始终返回 long,就像这样... - rogerdpack

6
我找到了解决方案:
public class DefaultPostgresKeyServer
{
    private Session session;
    private Iterator<BigInteger> iter;
    private long batchSize;

    public DefaultPostgresKeyServer (Session sess, long batchFetchSize)
    {
        this.session=sess;
        batchSize = batchFetchSize;
        iter = Collections.<BigInteger>emptyList().iterator();
    }

        @SuppressWarnings("unchecked")
        public Long getNextKey()
        {
            if ( ! iter.hasNext() )
            {
                Query query = session.createSQLQuery( "SELECT nextval( 'mySchema.mySequence' ) FROM generate_series( 1, " + batchSize + " )" );

                iter = (Iterator<BigInteger>) query.list().iterator();
            }
            return iter.next().longValue() ;
        }

}

3
注意:正如课程所示,这只适用于PostgreSQL,因为SQL语法是从Oracle引用的。 - Paulo Fidalgo

3
如果您正在使用Oracle,请考虑为序列指定缓存大小。如果您经常批量创建5K个对象,可以将其设置为1000或5000。我们为用于代理主键的序列做了这样的设定,并惊讶地发现,用Java手写的ETL过程的执行时间减少了一半。
我无法将格式化代码粘贴到评论中。这是序列DDL:
create sequence seq_mytable_sid 
minvalue 1 
maxvalue 999999999999999999999999999 
increment by 1 
start with 1 
cache 1000 
order  
nocycle;

听起来是一个有趣的方法。你能告诉我你使用了哪些注释来设置批量创建(以及序列值的批量获取)吗? - iliaden
@iliaden:我在序列DDL中指定了缓存大小。我无法将代码粘贴到评论中,因此我编辑了答案。 - Olaf
@Olaf:cache 1000似乎是所需的字段,但我该如何在Hibernate中进行映射呢? - iliaden
@iliaden:如果你之前的策略是有效的,只是速度慢了,那么更改序列定义应该会起到魔法般的效果。在我们的情况下,我们没有必要改变应用程序中的任何内容,也不需要进行新的构建。 - Olaf
@ Olaf:我进行了更深入的调查,发现了一些黑魔法。请参见编辑后的问题。 - iliaden
@iliaden: 我明白了。我的经验是用于生成替代主键的序列。 - Olaf

2

这似乎没有任何缓存,所以每次增量操作都需要与数据库进行一次往返交互... - Martin Mucha

2
为了获取新的id,你只需要执行flush方法。参见下面的getNext()方法:
@Entity
@SequenceGenerator(name = "sequence", sequenceName = "mySequence")
public class SequenceFetcher
{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence")
    private long id;

    public long getId() {
        return id;
    }

    public static long getNext(EntityManager em) {
        SequenceFetcher sf = new SequenceFetcher();
        em.persist(sf);
        em.flush();
        return sf.getId();
    }
}

1
这就是问题所在 - 序列不是主键(不是ID字段)。尽管我会调查使用SequenceFetcher的方法。 - iliaden
1
SequenceFether是一种额外的类方法,但是Hibernate强制将其映射到一个[不存在]的表。 - iliaden
"em.flush()" 给了我一个错误。我把它移除后,一切正常了。谢谢! - roneo

1

这是我做的方法:

@Entity
public class ServerInstanceSeq
{
    @Id //mysql bigint(20)
    @SequenceGenerator(name="ServerInstanceIdSeqName", sequenceName="ServerInstanceIdSeq", allocationSize=20)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="ServerInstanceIdSeqName")
    public Long id;

}

ServerInstanceSeq sis = new ServerInstanceSeq();
session.beginTransaction();
session.save(sis);
session.getTransaction().commit();
System.out.println("sis.id after save: "+sis.id);

1
有趣,你的方法对你起作用。当我尝试你的解决方案时,出现了一个错误,说“类型不匹配:无法将SQLQuery转换为Query”。--> 因此,我的解决方案看起来像这样:
SQLQuery query = session.createSQLQuery("select nextval('SEQUENCE_NAME')");
Long nextValue = ((BigInteger)query.uniqueResult()).longValue();

使用该解决方案,我没有遇到性能问题。

如果您只是想了解信息,请不要忘记重置您的值。

    --nextValue;
    query = session.createSQLQuery("select setval('SEQUENCE_NAME'," + nextValue + ")");

1
POSTGRESQL
String psqlAutoincrementQuery = "SELECT NEXTVAL(CONCAT(:psqlTableName, '_id_seq')) as id";

Long psqlAutoincrement = (Long) YOUR_SESSION_OBJ.createSQLQuery(psqlAutoincrementQuery)
                                                      .addScalar("id", Hibernate.LONG)
                                                      .setParameter("psqlTableName", psqlTableName)
                                                      .uniqueResult();

MYSQL
String mysqlAutoincrementQuery = "SELECT AUTO_INCREMENT as id FROM information_schema.tables WHERE table_name = :mysqlTableName AND table_schema = DATABASE()";

Long mysqlAutoincrement = (Long) YOUR_SESSION_OBJ.createSQLQuery(mysqlAutoincrementQuery)
                                                          .addScalar("id", Hibernate.LONG)
                                                          .setParameter("mysqlTableName", mysqlTableName)                                                              
                                                          .uniqueResult();

1
你的理论很有道理,确实帮了我不少忙,但是你的SQL容易受到注入攻击。这可不太好。 - Makoto

0

你使用SequenceGenerator虚拟实体的想法很好。

@Id
@GenericGenerator(name = "my_seq", strategy = "sequence", parameters = {
        @org.hibernate.annotations.Parameter(name = "sequence_name", value = "MY_CUSTOM_NAMED_SQN"),
})
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my_seq")

使用关键字名称为“sequence_name”的参数非常重要。在Hibernate类SequenceStyleGenerator上运行调试会话,查看configure(...)方法的最终QualifiedName序列名称= determineSequenceName( params, dialect, jdbcEnvironment ); 所在行,以查看Hibernate如何计算序列名称的更多详细信息。你也可以使用其中的一些默认值。

在虚构实体之后,我创建了一个CrudRepository:

public interface SequenceRepository extends CrudRepository<SequenceGenerator, Long> {}

在Junit中,我调用了SequenceRepository的save方法。
SequenceGenerator sequenceObject = new SequenceGenerator(); SequenceGenerator result = sequenceRepository.save(sequenceObject);
如果有更好的方法来完成这个任务(也许支持任何类型字段的生成器而不仅仅是Id),我会非常乐意使用它,而不是这个“技巧”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接