Sequenza JPA ibernazione (non ID)


138

È possibile utilizzare una sequenza DB per una colonna che non è l'identificatore / che non fa parte di un identificatore composito ?

Sto usando l'ibernazione come provider jpa e ho una tabella con alcune colonne che generano valori (usando una sequenza), sebbene non facciano parte dell'identificatore.

Quello che voglio è usare una sequenza per creare un nuovo valore per un'entità, in cui la colonna per la sequenza NON è (parte di) la chiave primaria:

@Entity
@Table(name = "MyTable")
public class MyEntity {

    //...
    @Id //... etc
    public Long getId() {
        return id;
    }

   //note NO @Id here! but this doesn't work...
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "myGen")
    @SequenceGenerator(name = "myGen", sequenceName = "MY_SEQUENCE")
    @Column(name = "SEQ_VAL", unique = false, nullable = false, insertable = true, updatable = true)
    public Long getMySequencedValue(){
      return myVal;
    }

}

Quindi quando faccio questo:

em.persist(new MyEntity());

l'id verrà generato, ma la mySequenceValproprietà verrà generata anche dal mio provider JPA.

Giusto per chiarire le cose: voglio che Hibernate generi il valore per la mySequencedValueproprietà. So che Hibernate può gestire valori generati dal database, ma non voglio usare un trigger o qualsiasi altra cosa diversa da Hibernate per generare il valore per la mia proprietà. Se Hibernate è in grado di generare valori per le chiavi primarie, perché non può generare per una proprietà semplice?

Risposte:


77

In cerca di risposte a questo problema, mi sono imbattuto in questo link

Sembra che Hibernate / JPA non sia in grado di creare automaticamente un valore per le proprietà non id. L' @GeneratedValueannotazione viene utilizzata solo in combinazione con @Idper creare numeri automatici.

L' @GeneratedValueannotazione dice a Hibernate che il database sta generando questo valore stesso.

La soluzione (o soluzione alternativa) suggerita in quel forum è quella di creare un'entità separata con un ID generato, qualcosa del genere:

@Entità
classe pubblica GeneralSequenceNumber {
  @id
  @GeneratedValue (...)
  Numero lungo privato;
}

@Entità 
public class MyEntity {
  @Id ..
  ID lungo privato;

  @Uno a uno(...)
  privato GeneralSequnceNumber myVal;
}

Dal documento java di @GeneratedValue: "L'annotazione GeneratedValue può essere applicata a una proprietà chiave chiave o a un campo di un'entità o a una superclasse mappata insieme all'annotazione Id"
Kariem,

11
Ho scoperto che @Column (columnDefinition = "serial") funziona perfettamente ma solo per PostgreSQL. Per me questa è stata la soluzione perfetta, perché la seconda entità è un'opzione "brutta"
Sergey Vedernikov,

@SergeyVedernikov che è stato estremamente utile. Ti dispiacerebbe pubblicare questo come una risposta separata? Ha risolto il mio problema in modo molto semplice ed efficace.
Matt Ball,

@MattBall l'ho pubblicato come risposta separata :) stackoverflow.com/a/10647933/620858
Sergey Vedernikov,

1
Ho aperto una proposta per consentire @GeneratedValuesu campi che non sono id. Si prega di votare per essere inclusi in 2.2 java.net/jira/browse/JPA_SPEC-113
Petar Tahchiev

44

Ho scoperto che @Column(columnDefinition="serial")funziona perfettamente, ma solo per PostgreSQL. Per me questa è stata la soluzione perfetta, perché la seconda entità è un'opzione "brutta".


Ciao, avrei bisogno di una spiegazione al riguardo. Potresti dirmi di più per favore?
Emaborsa,

2
@Emaborsa Il columnDefinition=bit in pratica dice a Hiberate di non provare a generare la definizione di colonna e usare invece il testo che hai dato. In sostanza, il tuo DDL per la colonna sarà letteralmente solo nome + colonna Definizione. In questo caso (PostgreSQL), mycolumn serialè una colonna valida in una tabella.
Patrick,

7
L'equivalente per MySQL è@Column(columnDefinition = "integer auto_increment")
Richard Kennard,

2
Questa auto genera il suo valore? Ho provato a persistere un'entità con una definizione di campo come questa ma non ha generato un valore. ha lanciato un valore nullo nella colonna <colonna> viola un vincolo non nullo
KyelJmD

7
Ho usato @Column(insertable = false, updatable = false, columnDefinition="serial")per evitare che l'ibernazione provasse a inserire valori nulli o ad aggiornare il campo. È quindi necessario eseguire nuovamente la query del db per ottenere l'id generato dopo un inserimento se è necessario utilizzarlo immediatamente.
Robert Di Paolo,

20

So che questa è una domanda molto antica, ma è mostrata in primo luogo sui risultati e jpa è cambiata molto dalla domanda.

Il modo giusto per farlo ora è con l' @Generatedannotazione. È possibile definire la sequenza, impostare il valore predefinito nella colonna su quella sequenza e quindi mappare la colonna come:

@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)

1
Ciò richiede ancora che il valore sia generato dal database, che non risponde realmente alla domanda. Per i database Oracle precedenti alla 12c, sarebbe comunque necessario scrivere un trigger di database per generare il valore.
Bernie,

9
inoltre, si tratta di un'annotazione Hibernate, non di JPA.
caarlos0

14

Hibernate lo supporta sicuramente. Dai documenti:

"Le proprietà generate sono proprietà che hanno i loro valori generati dal database. In genere, le applicazioni di ibernazione erano necessarie per aggiornare gli oggetti che contengono proprietà per le quali il database generava valori. Contrassegnando le proprietà come generate, tuttavia, consente all'applicazione di delegare questa responsabilità a Hibernate. In sostanza, ogni volta che Hibernate emette un INSERT SQL o un UPDATE per un'entità che ha definito proprietà generate, emette immediatamente una selezione in seguito per recuperare i valori generati. "

Per le proprietà generate solo su insert, la mappatura delle proprietà (.hbm.xml) sarebbe simile a:

<property name="foo" generated="insert"/>

Per le proprietà generate all'inserimento e all'aggiornamento della mappatura delle proprietà (.hbm.xml) sarebbe simile a:

<property name="foo" generated="always"/>

Sfortunatamente, non conosco l'APP, quindi non so se questa funzione è esposta tramite l'APP (sospetto che probabilmente non lo sia)

In alternativa, dovresti essere in grado di escludere la proprietà da inserimenti e aggiornamenti e quindi chiamare "manualmente" session.refresh (obj); dopo averlo inserito / aggiornato per caricare il valore generato dal database.

Ecco come escluderesti che la proprietà venisse utilizzata nelle istruzioni insert e update:

<property name="foo" update="false" insert="false"/>

Ancora una volta, non so se JPA esponga queste funzionalità di Hibernate, ma Hibernate le supporta.


1
L'annotazione @Generated corrisponde alla precedente configurazione XML. Vedi questa sezione dei documenti di ibernazione per maggiori dettagli.
Eric,

8

Come follow-up ecco come l'ho fatto funzionare:

@Override public Long getNextExternalId() {
    BigDecimal seq =
        (BigDecimal)((List)em.createNativeQuery("select col_msd_external_id_seq.nextval from dual").getResultList()).get(0);
    return seq.longValue();
}

Un varietà con Hibernate 4.2.19 e oracolo: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Aaron,

6

Ho corretto la generazione di UUID (o sequenze) con Hibernate usando l' @PrePersistannotazione:

@PrePersist
public void initializeUUID() {
    if (uuid == null) {
        uuid = UUID.randomUUID().toString();
    }
}

5

Anche se questo è un vecchio thread, voglio condividere la mia soluzione e spero di ricevere un feedback su questo. Tieni presente che ho provato questa soluzione solo con il mio database locale in alcuni testcase di JUnit. Quindi questa non è una caratteristica produttiva finora.

Ho risolto questo problema per me introducendo un'annotazione personalizzata chiamata Sequence senza proprietà. È solo un indicatore per i campi a cui dovrebbe essere assegnato un valore da una sequenza incrementata.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sequence
{
}

Usando questa annotazione ho segnato le mie entità.

public class Area extends BaseEntity implements ClientAware, IssuerAware
{
    @Column(name = "areaNumber", updatable = false)
    @Sequence
    private Integer areaNumber;
....
}

Per mantenere le cose indipendenti dal database, ho introdotto un'entità chiamata SequenceNumber che contiene il valore corrente della sequenza e la dimensione dell'incremento. Ho scelto className come chiave univoca in modo che ogni classe di entità ottenga la propria sequenza.

@Entity
@Table(name = "SequenceNumber", uniqueConstraints = { @UniqueConstraint(columnNames = { "className" }) })
public class SequenceNumber
{
    @Id
    @Column(name = "className", updatable = false)
    private String className;

    @Column(name = "nextValue")
    private Integer nextValue = 1;

    @Column(name = "incrementValue")
    private Integer incrementValue = 10;

    ... some getters and setters ....
}

L'ultimo passo e il più difficile è un PreInsertListener che gestisce l'assegnazione dei numeri di sequenza. Nota che ho usato la primavera come contenitore di fagioli.

@Component
public class SequenceListener implements PreInsertEventListener
{
    private static final long serialVersionUID = 7946581162328559098L;
    private final static Logger log = Logger.getLogger(SequenceListener.class);

    @Autowired
    private SessionFactoryImplementor sessionFactoryImpl;

    private final Map<String, CacheEntry> cache = new HashMap<>();

    @PostConstruct
    public void selfRegister()
    {
        // As you might expect, an EventListenerRegistry is the place with which event listeners are registered
        // It is a service so we look it up using the service registry
        final EventListenerRegistry eventListenerRegistry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class);

        // add the listener to the end of the listener chain
        eventListenerRegistry.appendListeners(EventType.PRE_INSERT, this);
    }

    @Override
    public boolean onPreInsert(PreInsertEvent p_event)
    {
        updateSequenceValue(p_event.getEntity(), p_event.getState(), p_event.getPersister().getPropertyNames());

        return false;
    }

    private void updateSequenceValue(Object p_entity, Object[] p_state, String[] p_propertyNames)
    {
        try
        {
            List<Field> fields = ReflectUtil.getFields(p_entity.getClass(), null, Sequence.class);

            if (!fields.isEmpty())
            {
                if (log.isDebugEnabled())
                {
                    log.debug("Intercepted custom sequence entity.");
                }

                for (Field field : fields)
                {
                    Integer value = getSequenceNumber(p_entity.getClass().getName());

                    field.setAccessible(true);
                    field.set(p_entity, value);
                    setPropertyState(p_state, p_propertyNames, field.getName(), value);

                    if (log.isDebugEnabled())
                    {
                        LogMF.debug(log, "Set {0} property to {1}.", new Object[] { field, value });
                    }
                }
            }
        }
        catch (Exception e)
        {
            log.error("Failed to set sequence property.", e);
        }
    }

    private Integer getSequenceNumber(String p_className)
    {
        synchronized (cache)
        {
            CacheEntry current = cache.get(p_className);

            // not in cache yet => load from database
            if ((current == null) || current.isEmpty())
            {
                boolean insert = false;
                StatelessSession session = sessionFactoryImpl.openStatelessSession();
                session.beginTransaction();

                SequenceNumber sequenceNumber = (SequenceNumber) session.get(SequenceNumber.class, p_className);

                // not in database yet => create new sequence
                if (sequenceNumber == null)
                {
                    sequenceNumber = new SequenceNumber();
                    sequenceNumber.setClassName(p_className);
                    insert = true;
                }

                current = new CacheEntry(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue(), sequenceNumber.getNextValue());
                cache.put(p_className, current);
                sequenceNumber.setNextValue(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue());

                if (insert)
                {
                    session.insert(sequenceNumber);
                }
                else
                {
                    session.update(sequenceNumber);
                }
                session.getTransaction().commit();
                session.close();
            }

            return current.next();
        }
    }

    private void setPropertyState(Object[] propertyStates, String[] propertyNames, String propertyName, Object propertyState)
    {
        for (int i = 0; i < propertyNames.length; i++)
        {
            if (propertyName.equals(propertyNames[i]))
            {
                propertyStates[i] = propertyState;
                return;
            }
        }
    }

    private static class CacheEntry
    {
        private int current;
        private final int limit;

        public CacheEntry(final int p_limit, final int p_current)
        {
            current = p_current;
            limit = p_limit;
        }

        public Integer next()
        {
            return current++;
        }

        public boolean isEmpty()
        {
            return current >= limit;
        }
    }
}

Come puoi vedere dal codice sopra, il listener ha usato un'istanza SequenceNumber per classe di entità e riserva un paio di numeri di sequenza definiti dal valore incrementale dell'entità SequenceNumber. Se esaurisce i numeri di sequenza, carica l'entità SequenceNumber per la classe di destinazione e riserva i valori incrementValue per le chiamate successive. In questo modo non ho bisogno di interrogare il database ogni volta che è necessario un valore di sequenza. Si noti la StatelessSession che si sta aprendo per riservare la serie successiva di numeri di sequenza. Non è possibile utilizzare la stessa sessione in cui l'entità target è attualmente persistente poiché ciò comporterebbe una ConcurrentModificationException in EntityPersister.

Spero che questo aiuti qualcuno.


5

Se stai usando postgresql
E sto usando in avvio a molla 1.5.6

@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;

1
Ha funzionato anche per me, sto usando lo stivale a molla 2.1.6.RELEASE, Hibernate 5.3.10.Final, Oltre a quanto già sottolineato, ho dovuto creare una sicurezza seq_ordere fare riferimento al campo, nextval('seq_order'::regclass)
OJVM

3

Corro nella stessa situazione come te e non ho trovato alcuna risposta seria se è fondamentalmente possibile generare proprietà non id con JPA o meno.

La mia soluzione è quella di chiamare la sequenza con una query JPA nativa per impostare la proprietà a mano prima di perseguitarla.

Questo non è soddisfacente, ma per il momento funziona come una soluzione alternativa.

Mario


2

Ho trovato questa nota specifica nella sessione 9.1.9 GeneratedValue Annotation dalla specifica JPA: "[43] Le applicazioni portatili non dovrebbero usare l'annotazione GeneratedValue su altri campi o proprietà persistenti." Quindi, presumo che non sia possibile generare automaticamente valore per valori di chiave non primaria almeno usando semplicemente JPA.


1

Sembra che il thread sia vecchio, volevo solo aggiungere la mia soluzione qui (utilizzando AspectJ - AOP in primavera).

La soluzione è creare un'annotazione personalizzata @InjectSequenceValuecome segue.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectSequenceValue {
    String sequencename();
}

Ora puoi annotare qualsiasi campo nell'entità, in modo che il valore del campo sottostante (Long / Integer) venga iniettato in fase di esecuzione usando il valore successivo della sequenza.

Annota in questo modo.

//serialNumber will be injected dynamically, with the next value of the serialnum_sequence.
 @InjectSequenceValue(sequencename = "serialnum_sequence") 
  Long serialNumber;

Finora abbiamo contrassegnato il campo di cui abbiamo bisogno per iniettare il valore della sequenza, quindi vedremo come iniettare il valore della sequenza nei campi contrassegnati, questo viene fatto creando il taglio del punto in AspectJ.

Attiveremo l'iniezione poco prima save/persistdell'esecuzione del metodo, fatto nella classe seguente.

@Aspect
@Configuration
public class AspectDefinition {

    @Autowired
    JdbcTemplate jdbcTemplate;


    //@Before("execution(* org.hibernate.session.save(..))") Use this for Hibernate.(also include session.save())
    @Before("execution(* org.springframework.data.repository.CrudRepository.save(..))") //This is for JPA.
    public void generateSequence(JoinPoint joinPoint){

        Object [] aragumentList=joinPoint.getArgs(); //Getting all arguments of the save
        for (Object arg :aragumentList ) {
            if (arg.getClass().isAnnotationPresent(Entity.class)){ // getting the Entity class

                Field[] fields = arg.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (field.isAnnotationPresent(InjectSequenceValue.class)) { //getting annotated fields

                        field.setAccessible(true); 
                        try {
                            if (field.get(arg) == null){ // Setting the next value
                                String sequenceName=field.getAnnotation(InjectSequenceValue.class).sequencename();
                                long nextval=getNextValue(sequenceName);
                                System.out.println("Next value :"+nextval); //TODO remove sout.
                                field.set(arg, nextval);
                            }

                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

        }
    }

    /**
     * This method fetches the next value from sequence
     * @param sequence
     * @return
     */

    public long getNextValue(String sequence){
        long sequenceNextVal=0L;

        SqlRowSet sqlRowSet= jdbcTemplate.queryForRowSet("SELECT "+sequence+".NEXTVAL as value FROM DUAL");
        while (sqlRowSet.next()){
            sequenceNextVal=sqlRowSet.getLong("value");

        }
        return  sequenceNextVal;
    }
}

Ora puoi annotare qualsiasi Entità come di seguito.

@Entity
@Table(name = "T_USER")
public class UserEntity {

    @Id
    @SequenceGenerator(sequenceName = "userid_sequence",name = "this_seq")
    @GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "this_seq")
    Long id;
    String userName;
    String password;

    @InjectSequenceValue(sequencename = "serialnum_sequence") // this will be injected at the time of saving.
    Long serialNumber;

    String name;
}

0

"Non voglio usare un trigger o qualsiasi altra cosa diversa da Hibernate per generare il valore per la mia proprietà"

In tal caso, che ne dici di creare un'implementazione di UserType che generi il valore richiesto e di configurare i metadati per usare tale UserType per la persistenza della proprietà mySequenceVal?


0

Questo non è lo stesso che usare una sequenza. Quando si utilizza una sequenza, non si sta inserendo o aggiornando nulla. Stai semplicemente recuperando il valore della sequenza successiva. Sembra che l'ibernazione non lo supporti.


0

Se si dispone di una colonna con tipo UNIQUEIDENTIFIER e generazione predefinita necessaria per l'inserimento, ma la colonna non è PK

@Generated(GenerationTime.INSERT)
@Column(nullable = false , columnDefinition="UNIQUEIDENTIFIER")
private String uuidValue;

In db avrai

CREATE TABLE operation.Table1
(
    Id         INT IDENTITY (1,1)               NOT NULL,
    UuidValue  UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL)

In questo caso non definirai il generatore per un valore di cui hai bisogno (sarà automaticamente grazie a columnDefinition="UNIQUEIDENTIFIER"). Lo stesso puoi provare per altri tipi di colonne


0

Ho trovato una soluzione per questo sui database MySql usando @PostConstruct e JdbcTemplate in un'applicazione Spring. Potrebbe essere fattibile con altri database, ma il caso d'uso che presenterò si basa sulla mia esperienza con MySql, in quanto utilizza auto_increment.

Innanzitutto, avevo provato a definire una colonna come auto_increment usando la proprietà ColumnDefinition dell'annotazione @Column, ma non funzionava poiché la colonna doveva essere una chiave per essere auto incrementale, ma apparentemente la colonna non sarebbe stata definita come un indice fino a dopo che è stato definito, causando un deadlock.

Qui è dove mi è venuta l'idea di creare la colonna senza la definizione auto_increment e di aggiungerla dopo la creazione del database. Questo è possibile usando l'annotazione @PostConstruct, che fa invocare un metodo subito dopo che l'applicazione ha inizializzato i bean, insieme al metodo di aggiornamento di JdbcTemplate.

Il codice è il seguente:

Nella mia entità:

@Entity
@Table(name = "MyTable", indexes = { @Index(name = "my_index", columnList = "mySequencedValue") })
public class MyEntity {
    //...
    @Column(columnDefinition = "integer unsigned", nullable = false, updatable = false, insertable = false)
    private Long mySequencedValue;
    //...
}

In una classe PostConstructComponent:

@Component
public class PostConstructComponent {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void makeMyEntityMySequencedValueAutoIncremental() {
        jdbcTemplate.update("alter table MyTable modify mySequencedValue int unsigned auto_increment");
    }
}

0

Voglio fornire un'alternativa accanto alla soluzione accettata di @Morten Berg, che ha funzionato meglio per me.

Questo approccio consente di definire il campo con il Numbertipo effettivamente desiderato - Longnel mio caso d'uso - anziché GeneralSequenceNumber. Questo può essere utile, ad es. Per la serializzazione JSON (de).

Il rovescio della medaglia è che richiede un po 'più di sovraccarico del database.


In primo luogo, abbiamo bisogno di un ActualEntityin cui vogliamo auto-incrementare generateddi tipo Long:

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

    @Column(unique = true, updatable = false, nullable = false)
    Long generated;

    // ...

}

Successivamente, abbiamo bisogno di un'entità helper Generated. L'ho messo accanto a pacchetto privato ActualEntity, per mantenerlo un dettaglio di implementazione del pacchetto:

@Entity
class Generated {

    @Id
    @GeneratedValue(strategy = SEQUENCE, generator = "seq")
    @SequenceGenerator(name = "seq", initialValue = 1, allocationSize = 1)
    Long id;

}

Infine, abbiamo bisogno di un posto dove agganciarci prima di salvare il file ActualEntity. Lì, creiamo e persistiamo Generatedun'istanza. Ciò fornisce quindi una sequenza di database generata iddi tipo Long. Usiamo questo valore scrivendolo suActualEntity.generated .

Nel mio caso d'uso, l'ho implementato usando un REST di dati di primavera @RepositoryEventHandler, che viene chiamato subito prima che ActualEntitypersista. Dovrebbe dimostrare il principio:

@Component
@RepositoryEventHandler
public class ActualEntityHandler {

    @Autowired
    EntityManager entityManager;

    @Transactional
    @HandleBeforeCreate
    public void generate(ActualEntity entity) {
        Generated generated = new Generated();

        entityManager.persist(generated);
        entity.setGlobalId(generated.getId());
        entityManager.remove(generated);
    }

}

Non l'ho provato in un'applicazione reale, quindi per favore divertiti con cura.


-1

Sono stato in una situazione come te (sequenza JPA / Hibernate per campo non @Id) e ho finito per creare un trigger nel mio schema db che aggiunge un numero di sequenza univoco all'insert. Non l'ho mai fatto funzionare con JPA / Hibernate


-1

Dopo aver trascorso ore, questo mi ha aiutato ordinatamente a risolvere il mio problema:

Per Oracle 12c:

ID NUMBER GENERATED as IDENTITY

Per H2:

ID BIGINT GENERATED as auto_increment

Fai anche:

@Column(insertable = false)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.