J'ai une entité qui a un champ NON-ID qui doit être défini à partir d'une séquence. Actuellement, je récupère la première valeur de la séquence, la stocke du côté client et calcule à partir de cette valeur.
Cependant, je cherche une "meilleure" façon de procéder. J'ai implémenté un moyen de récupérer la prochaine valeur de séquence:
public Long getNextKey()
{
Query query = session.createSQLQuery( "select nextval('mySequence')" );
Long key = ((BigInteger) query.uniqueResult()).longValue();
return key;
}
Cependant, cette façon réduit considérablement les performances (la création de ~ 5000 objets est ralentie d'un facteur 3 - de 5740 ms à 13648 ms).
J'ai essayé d'ajouter une "fausse" entité:
@Entity
@SequenceGenerator(name = "sequence", sequenceName = "mySequence")
public class SequenceFetcher
{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence")
private long id;
public long getId() {
return id;
}
}
Cependant, cette approche n'a pas fonctionné non plus (tous les identifiants retournés étaient 0).
Quelqu'un peut-il me conseiller sur la façon de récupérer efficacement la prochaine valeur de séquence en utilisant Hibernate?
Edit: Après enquête, j'ai découvert que l'appel de Query query = session.createSQLQuery( "select nextval('mySequence')" );
est de loin plus inefficace que d'utiliser le @GeneratedValue
- à cause d'Hibernate en quelque sorte parvient à réduire le nombre de récupérations lors de l'accès à la séquence décrite par @GeneratedValue
.
Par exemple, lorsque je crée 70 000 entités (donc avec 70 000 clés primaires extraites de la même séquence), j'obtiens tout ce dont j'ai besoin.
CEPENDANT , Hibernate émet uniquement les commandes 1404select nextval ('local_key_sequence')
. REMARQUE: Côté base de données, la mise en cache est définie sur 1.
Si j'essaye de récupérer toutes les données manuellement, il me faudra 70 000 sélections, donc une énorme différence de performances. Quelqu'un connaît-il le fonctionnement interne d'Hibernate et comment le reproduire manuellement?
J'ai trouvé la solution:
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() ;
}
}
Voici ce qui a fonctionné pour moi (spécifique à Oracle, mais l'utilisation de scalar
semble être la clé)
Long getNext() {
Query query =
session.createSQLQuery("select MYSEQ.nextval as num from dual")
.addScalar("num", StandardBasicTypes.BIG_INTEGER);
return ((BigInteger) query.uniqueResult()).longValue();
}
Merci aux affiches ici: springsource_forum
Vous pouvez utiliser l'API Hibernate Dialect pour l'indépendance de la base de données comme suit
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;
}
}
Si vous utilisez Oracle, pensez à spécifier la taille du cache pour la séquence. Si vous créez régulièrement des objets par lots de 5 Ko, vous pouvez simplement le définir sur 1000 ou 5000. Nous l'avons fait pour la séquence utilisée pour la clé primaire de substitution et nous avons été surpris que les temps d'exécution d'un processus ETL écrits à la main en Java a chuté de moitié.
Je n'ai pas pu coller de code formaté dans un commentaire. Voici la séquence DDL:
create sequence seq_mytable_sid
minvalue 1
maxvalue 999999999999999999999999999
increment by 1
start with 1
cache 1000
order
nocycle;
Pour obtenir le nouvel identifiant, il vous suffit de flush
le gestionnaire d'entités. Voir la méthode getNext()
ci-dessous:
@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();
}
}
Intéressant, cela fonctionne pour vous. Lorsque j'ai essayé votre solution, une erreur est survenue, disant que "Type de non-correspondance: ne peut pas convertir de SQLQuery en Query". -> Par conséquent, ma solution ressemble à:
SQLQuery query = session.createSQLQuery("select nextval('SEQUENCE_NAME')");
Long nextValue = ((BigInteger)query.uniqueResult()).longValue();
Avec cette solution, je n'ai pas rencontré de problèmes de performances.
Et n'oubliez pas de réinitialiser votre valeur, si vous vouliez simplement le savoir à des fins d'information.
--nextValue;
query = session.createSQLQuery("select setval('SEQUENCE_NAME'," + nextValue + ")");
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();
Votre idée avec la fausse entité SequenceGenerator est bonne.
@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")
Il est important d'utiliser le paramètre avec le nom de clé "nom_séquence". Exécutez une session de débogage sur la classe hibernate SequenceStyleGenerator, la méthode configure (...) à la ligne finale QualifiedName sequenceName = determineSequenceName (params, dialect, jdbcEnvironment); pour voir plus de détails sur la façon dont le nom de séquence est calculé par Hibernate. Vous pouvez également utiliser certains paramètres par défaut.
Après la fausse entité, j'ai créé un CrudRepository:
public interface SequenceRepository extends CrudRepository<SequenceGenerator, Long> {}
Dans le Junit, j'appelle la méthode de sauvegarde du SequenceRepository.
SequenceGenerator sequenceObject = new SequenceGenerator (); Résultat SequenceGenerator = sequenceRepository.save (sequenceObject);
S'il y a une meilleure façon de faire cela (peut-être un support pour un générateur sur n'importe quel type de champ au lieu de simplement Id), je serais plus qu'heureux de l'utiliser à la place de cette "astuce".
Voici comment je le fais:
@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);
Spring 5 a des classes d'assistance intégrées pour cela: org/springframework/jdbc/support/incrementer