2011-06-17 12 views
22

Ho un ente che ha un campo NON-ID che deve essere impostato da una sequenza. Attualmente, recupero il primo valore della sequenza, lo memorizzo sul lato client e calcola da quel valore.ottenere valore successivo sequenza dal database utilizzando hibernate

Comunque, sto cercando un modo "migliore" di fare questo. Ho implementato un modo per andare a prendere il successivo valore di sequenza:

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

Tuttavia, in questo modo riduce le prestazioni in modo significativo (creazione di ~ 5000 oggetti viene rallentato di un fattore 3 - da 5740ms a 13648ms).

Ho provato ad aggiungere un'entità "falso":

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

    public long getId() { 
     return id; 
    } 
} 

Tuttavia questo approccio non ha funzionato neanche (tutti gli ID restituito erano 0).

Qualcuno mi può consigliare come recuperare il valore della sequenza successiva utilizzando in modo efficiente Hibernate?

Edit: Dopo alcune indagini, ho scoperto che chiamare Query query = session.createSQLQuery("select nextval('mySequence')"); è di gran lunga più inefficienti rispetto all'uso del @GeneratedValue - a causa di Hibernate qualche modo riesce a ridurre il numero di recuperi quando si accede la sequenza descritta da @GeneratedValue.

Per esempio, quando creo 70.000 entità, (quindi con 70.000 chiavi primarie recuperati dalla stessa sequenza), ottengo tutto quello che serve.

TUTTAVIA, Hibernate emette solo select nextval ('local_key_sequence') comandi. NOTA: dal lato del database, la memorizzazione nella cache è impostata su 1.

Se provo a recuperare tutti i dati manualmente, ci vorranno 70.000 selezioni, quindi un'enorme differenza di prestazioni. Qualcuno conosce il funzionamento interno di Hibernate e come riprodurlo manualmente?

risposta

5

ho trovato la soluzione:

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

} 
+2

Solo una nota: poiché la classe indica che questo funziona solo su PostgreSQL, poiché la sintassi SQL è di riferimento da Oracle. –

3

Se si utilizza Oracle, considerano specificando la dimensione della cache per la sequenza. Se si creano oggetti in lotti di 5K, è possibile impostarlo su 1000 o 5000. Lo abbiamo fatto per la sequenza utilizzata per la chiave primaria surrogata e siamo rimasti sorpresi dal fatto che i tempi di esecuzione per un processo ETL scritti a mano in Java sono stati eliminati a metà.

non ho potuto incollare formattato codice in commento. Ecco il DDL sequenza:

create sequence seq_mytable_sid 
minvalue 1 
maxvalue 999999999999999999999999999 
increment by 1 
start with 1 
cache 1000 
order 
nocycle; 
+0

sembra un approccio interessante. Puoi dirmi quali annotazioni hai usato per impostare la creazione del batch (e quindi il recupero in batch dei valori di sequenza)? – iliaden

+0

@iliaden: ho specificato la dimensione della cache nella sequenza DDL.Non ho potuto incollare il codice nel commento, quindi ho modificato la risposta. – Olaf

+0

@Olaf: 'cache 1000' sembra essere il campo necessario, ma come posso mapparlo in Hibernate? – iliaden

2

per ottenere il nuovo id, tutto quello che dovete fare è flush il gestore di entità.Vedo getNext() metodo seguito:

@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

questo è il problema - la sequenza NON è per una chiave primaria (NON il campo ID). Anche se indagherò sull'uso di 'SequenceFetcher' – iliaden

+1

SequenceFether era un approccio di classe extra, ma in ibernazione lo mappava forzatamente a una tabella [inesistente]. – iliaden

+0

"em.flush()" mi ha dato un errore. L'ho rimosso e ha funzionato bene. Grazie! – roneo

22

Ecco che cosa ha funzionato per me (specifico per Oracle, ma utilizzando scalar sembra essere la chiave)

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

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

Grazie ai poster qui: springsource_forum

+3

Come nota, puoi utilizzare anche StandardBasicTypes.LONG invece di StandardBasicTypes.BIG_INTEGER ... – rogerdpack

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

La tua teoria è valida e mi ha aiutato, ma il tuo SQL è vulnerabile all'iniezione. Non va mai bene. – Makoto

16

È possibile utilizzare Hibernate Dialetto API per l'indipendenza del database come seguire

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; 
    } 

} 
+0

Questo è bello, tranne che – Marc

+2

+1 Penso che questa sia l'unica risposta agnostica del database pubblicata, che è ciò di cui ho bisogno. (Abbiamo un'applicazione che usa Hibernate 4 che ha bisogno di recuperare un valore non-id da una sequenza db, usando HSQLDB per i test e Oracle per il vero affare.) Usando le risorse try-with di java 7 puoi accorciare il codice po. Ho dovuto usare un 'DatabaseMetaDataDialectResolutionInfoAdapter' per racchiudere il risultato di' connection.getMetaData() 'prima di passarlo a' resolveDialect() '. @punitpatel grazie! –

+0

In realtà, devo aggiungere che questo utilizza 'dialect.getSequenceNextValString()', che non funzionerà per database che non supportano sequenze ... –

0

Ecco il modo in cui lo faccio:

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

Interessante funziona per voi. Quando ho provato la tua soluzione, è emerso un errore che diceva "Tipo non corrispondente: impossibile convertire da SQLQuery a Query". -> Quindi la mia soluzione è la seguente:

SQLQuery query = session.createSQLQuery("select nextval('SEQUENCE_NAME')"); 
Long nextValue = ((BigInteger)query.uniqueResult()).longValue(); 

Con questa soluzione non ho riscontrato problemi di prestazioni.

E non dimenticare di ripristinare il valore, se si voleva solo sapere a scopo informativo.

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

La tua idea con l'entità finta SequenceGenerator è buona.

@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") 

È importante utilizzare il parametro con il nome chiave "nome_segui". Esegui una sessione di debug sulla classe di ibernazione SequenceStyleGenerator, il metodo configure (...) sulla riga finale QualifiedName sequenceName = defineSequenceName (params, dialect, jdbcEnvironment); per vedere maggiori dettagli su come il nome della sequenza viene calcolato da Hibernate. Ci sono alcune impostazioni predefinite che puoi usare anche qui.

Dopo l'entità falso, ho creato un CrudRepository:

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

Nel Junit, che io chiamo il metodo save del SequenceRepository.

SequenceGenerator sequenceObject = new SequenceGenerator(); SequenceGenerator result = sequenceRepository.save (sequenceObject);

Se c'è un modo migliore per farlo (forse il supporto per un generatore su qualsiasi tipo di campo invece di solo Id), sarei più che felice di usarlo al posto di questo "trucco".

Problemi correlati