Séquence JPA Hibernate (non-Id)


138

Est-il possible d'utiliser une séquence DB pour une colonne qui n'est pas l'identifiant / ne fait pas partie d'un identifiant composite ?

J'utilise hibernate comme fournisseur jpa et j'ai une table qui contient des colonnes qui sont des valeurs générées (en utilisant une séquence), bien qu'elles ne fassent pas partie de l'identifiant.

Ce que je veux, c'est utiliser une séquence pour créer une nouvelle valeur pour une entité, où la colonne de la séquence ne fait PAS (partie de) la clé primaire:

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

}

Puis quand je fais ça:

em.persist(new MyEntity());

l'id sera généré, mais la mySequenceValpropriété sera également générée par mon fournisseur JPA.

Pour clarifier les choses: je souhaite qu'Hibernate génère la valeur de la mySequencedValuepropriété. Je sais qu'Hibernate peut gérer les valeurs générées par la base de données, mais je ne veux pas utiliser de déclencheur ou autre chose que Hibernate lui-même pour générer la valeur de ma propriété. Si Hibernate peut générer des valeurs pour les clés primaires, pourquoi ne peut-il pas générer pour une propriété simple?

Réponses:


77

À la recherche de réponses à ce problème, je suis tombé sur ce lien

Il semble qu'Hibernate / JPA ne soit pas capable de créer automatiquement une valeur pour vos propriétés non id. L' @GeneratedValueannotation est uniquement utilisée avec @Idpour créer des numéros automatiques.

L' @GeneratedValueannotation indique simplement à Hibernate que la base de données génère elle-même cette valeur.

La solution (ou contournement) suggérée dans ce forum est de créer une entité distincte avec un identifiant généré, quelque chose comme ceci:

@Entité
classe publique GeneralSequenceNumber {
  @Id
  @GeneratedValue (...)
  numéro Long privé;
}

@Entité 
public class MyEntity {
  @Id ..
  identifiant long privé;

  @Un par un(...)
  private GeneralSequnceNumber myVal;
}

Extrait de la documentation java de @GeneratedValue: "L'annotation GeneratedValue peut être appliquée à une propriété de clé primaire ou à un champ d'une entité ou d'une superclasse mappée en conjonction avec l'annotation Id"
Kariem

11
J'ai trouvé que @Column (columnDefinition = "serial") fonctionne parfaitement mais uniquement pour PostgreSQL. Pour moi, c'était la solution parfaite, car la deuxième entité est une option "moche"
Sergey Vedernikov

@SergeyVedernikov qui a été extrêmement utile. Cela vous dérangerait-il de publier cela comme réponse distincte? Cela a résolu mon problème très très simplement et efficacement.
Matt Ball

@MattBall j'ai posté ceci comme réponse séparée :) stackoverflow.com/a/10647933/620858
Sergey Vedernikov

1
J'ai ouvert une proposition pour autoriser @GeneratedValueles champs qui ne sont pas id. Veuillez voter pour être inclus dans 2.2 java.net/jira/browse/JPA_SPEC-113
Petar Tahchiev

44

J'ai trouvé que cela @Column(columnDefinition="serial")fonctionne parfaitement mais uniquement pour PostgreSQL. Pour moi, c'était la solution parfaite, car la deuxième entité est une option «moche».


Salut, j'aurais besoin d'une explication à ce sujet. Pouvez-vous m'en dire plus s'il vous plaît?
Emaborsa

2
@Emaborsa Le columnDefinition=bit dit essentiellement à Hiberate de ne pas essayer de générer la définition de colonne et d'utiliser à la place le texte que vous avez donné. Essentiellement, votre DDL pour la colonne sera littéralement simplement name + columnDefinition. Dans ce cas (PostgreSQL), mycolumn serialest une colonne valide dans une table.
Patrick

7
L'équivalent pour MySQL est@Column(columnDefinition = "integer auto_increment")
Richard Kennard

2
Cette auto génère-t-elle sa valeur? J'ai essayé de persister une entité avec une définition de champ comme celle-ci, mais cela n'a pas généré de valeur. il a jeté une valeur nulle dans la colonne <colonne> viole la contrainte non nulle
KyelJmD

7
J'avais l'habitude @Column(insertable = false, updatable = false, columnDefinition="serial")d'empêcher Hibernate d'essayer d'insérer des valeurs nulles ou de mettre à jour le champ. Vous devez ensuite interroger à nouveau la base de données pour obtenir l'ID généré après une insertion si vous devez l'utiliser immédiatement.
Robert Di Paolo

20

Je sais que c'est une question très ancienne, mais elle se montre d'abord sur les résultats et jpa a beaucoup changé depuis la question.

La bonne façon de le faire maintenant est avec l' @Generatedannotation. Vous pouvez définir la séquence, définir la valeur par défaut dans la colonne sur cette séquence, puis mapper la colonne comme:

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

1
Cela nécessite toujours que la valeur soit générée par la base de données, ce qui ne répond pas vraiment à la question. Pour les bases de données Oracle antérieures à 12c, vous devrez toujours écrire un déclencheur de base de données pour générer la valeur.
Bernie

9
il s'agit également d'une annotation Hibernate et non de JPA.
caarlos0

14

Hibernate le soutient définitivement. À partir de la documentation:

"Les propriétés générées sont des propriétés dont les valeurs sont générées par la base de données. En règle générale, les applications Hibernate devaient actualiser les objets contenant les propriétés pour lesquelles la base de données générait des valeurs. Cependant, le marquage des propriétés comme générées permet à l'application de déléguer cette responsabilité à Hibernate. Essentiellement, chaque fois qu'Hibernate émet un SQL INSERT ou UPDATE pour une entité qui a défini des propriétés générées, il émet immédiatement une sélection par la suite pour récupérer les valeurs générées. "

Pour les propriétés générées lors de l'insertion uniquement, votre mappage de propriétés (.hbm.xml) ressemblerait à ceci:

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

Pour les propriétés générées lors de l'insertion et de la mise à jour, votre mappage de propriété (.hbm.xml) ressemblerait à ceci:

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

Malheureusement, je ne connais pas JPA, donc je ne sais pas si cette fonctionnalité est exposée via JPA (je soupçonne peut-être pas)

Vous devriez également pouvoir exclure la propriété des insertions et des mises à jour, puis appeler "manuellement" session.refresh (obj); après l'avoir inséré / mis à jour pour charger la valeur générée à partir de la base de données.

Voici comment vous excluriez l'utilisation de la propriété dans les instructions d'insertion et de mise à jour:

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

Encore une fois, je ne sais pas si JPA expose ces fonctionnalités Hibernate, mais Hibernate les prend en charge.


1
L'annotation @Generated correspond à la configuration XML ci-dessus. Consultez cette section de la documentation relative à la mise en veille prolongée pour plus de détails.
Eric

8

En guise de suivi, voici comment je l'ai fait fonctionner:

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

Une variante avec Hibernate 4.2.19 et oracle: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Aaron

6

J'ai corrigé la génération d'UUID (ou de séquences) avec Hibernate en utilisant l' @PrePersistannotation:

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

5

Bien qu'il s'agisse d'un ancien fil de discussion, je souhaite partager ma solution et j'espère obtenir des commentaires à ce sujet. Soyez averti que je n'ai testé cette solution qu'avec ma base de données locale dans certains cas de test JUnit. Ce n'est donc pas une fonctionnalité productive jusqu'à présent.

J'ai résolu ce problème pour mon en introduisant une annotation personnalisée appelée Séquence sans propriété. C'est juste un marqueur pour les champs auxquels une valeur doit être attribuée à partir d'une séquence incrémentée.

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

En utilisant cette annotation, j'ai marqué mes entités.

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

Pour garder les choses indépendantes de la base de données, j'ai introduit une entité appelée SequenceNumber qui contient la valeur actuelle de la séquence et la taille de l'incrément. J'ai choisi le className comme clé unique pour que chaque classe d'entité obtienne sa propre séquence.

@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 ....
}

La dernière étape et la plus difficile est un PreInsertListener qui gère l'attribution des numéros de séquence. Notez que j'ai utilisé le ressort comme récipient à grains.

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

Comme vous pouvez le voir à partir du code ci-dessus, l'auditeur a utilisé une instance SequenceNumber par classe d'entité et réserve un couple de numéros de séquence définis par incrementValue de l'entité SequenceNumber. S'il manque de numéros de séquence, il charge l'entité SequenceNumber pour la classe cible et réserve les valeurs incrementValue pour les appels suivants. De cette façon, je n'ai pas besoin d'interroger la base de données chaque fois qu'une valeur de séquence est nécessaire. Notez la StatelessSession qui est en cours d'ouverture pour réserver le prochain ensemble de numéros de séquence. Vous ne pouvez pas utiliser la même session que l'entité cible est actuellement persistante car cela entraînerait une ConcurrentModificationException dans EntityPersister.

J'espère que cela aide quelqu'un.


5

Si vous utilisez postgresql
et que j'utilise dans Spring Boot 1.5.6

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

1
Cela a fonctionné pour moi aussi, j'utilise spring boot 2.1.6.RELEASE, Hibernate 5.3.10.Final, En plus de ce qui a déjà été souligné, j'ai dû créer une sécurité seq_orderet faire référence du champ, nextval('seq_order'::regclass)
OJVM

3

Je cours dans la même situation que vous et je n'ai pas non plus trouvé de réponses sérieuses s'il est fondamentalement possible de générer des propriétés sans identifiant avec JPA ou non.

Ma solution est d'appeler la séquence avec une requête JPA native pour définir la propriété à la main avant de la persister.

Ce n'est pas satisfaisant mais cela fonctionne comme une solution de contournement pour le moment.

Mario


2

J'ai trouvé cette note spécifique dans la session 9.1.9 Annotation GeneratedValue de la spécification JPA: "[43] Les applications portables ne devraient pas utiliser l'annotation GeneratedValue sur d'autres champs ou propriétés persistants." Donc, je suppose qu'il n'est pas possible de générer automatiquement de la valeur pour les valeurs de clé non primaire au moins en utilisant simplement JPA.


1

On dirait que le fil est vieux, je voulais juste ajouter ma solution ici (Utilisation d'AspectJ - AOP au printemps).

La solution consiste à créer une annotation personnalisée @InjectSequenceValuecomme suit.

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

Vous pouvez maintenant annoter n'importe quel champ de l'entité, de sorte que la valeur du champ sous-jacent (Long / Integer) soit injectée au moment de l'exécution à l'aide de la valeur suivante de la séquence.

Annotez comme ceci.

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

Jusqu'à présent, nous avons marqué le champ dont nous avons besoin pour injecter la valeur de séquence.Nous allons donc voir comment injecter la valeur de séquence dans les champs marqués, cela se fait en créant le point coupé dans AspectJ.

Nous déclencherons l'injection juste avant que la save/persistméthode ne soit exécutée, ce qui se fait dans la classe ci-dessous.

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

Vous pouvez maintenant annoter n'importe quelle entité comme ci-dessous.

@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

"Je ne veux pas utiliser de déclencheur ou autre chose que Hibernate lui-même pour générer la valeur de ma propriété"

Dans ce cas, que diriez-vous de créer une implémentation de UserType qui génère la valeur requise et de configurer les métadonnées pour utiliser ce UserType pour la persistance de la propriété mySequenceVal?


0

Ce n'est pas la même chose que d'utiliser une séquence. Lorsque vous utilisez une séquence, vous n'insérez ni ne mettez à jour rien. Vous récupérez simplement la valeur de séquence suivante. Il semble que Hibernate ne le prend pas en charge.


0

Si vous avez une colonne avec le type UNIQUEIDENTIFIER et la génération par défaut nécessaire à l'insertion mais que la colonne n'est pas PK

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

En db vous aurez

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

Dans ce cas vous ne définirez pas de générateur pour une valeur dont vous avez besoin (ce sera automatiquement grâce à columnDefinition="UNIQUEIDENTIFIER"). La même chose que vous pouvez essayer pour d'autres types de colonnes


0

J'ai trouvé une solution de contournement pour cela sur les bases de données MySql en utilisant @PostConstruct et JdbcTemplate dans une application Spring. Cela peut être faisable avec d'autres bases de données, mais le cas d'utilisation que je vais présenter est basé sur mon expérience avec MySql, car il utilise auto_increment.

Tout d'abord, j'avais essayé de définir une colonne comme auto_increment en utilisant la propriété ColumnDefinition de l'annotation @Column, mais cela ne fonctionnait pas car la colonne devait être une clé pour être auto-incrémentielle, mais apparemment, la colonne ne serait pas définie comme un index jusqu'à ce qu'il ait été défini, provoquant un blocage.

Voici où je suis venu avec l'idée de créer la colonne sans la définition auto_increment, et de l'ajouter après la création de la base de données. Cela est possible en utilisant l'annotation @PostConstruct, qui provoque l'appel d'une méthode juste après que l'application a initialisé les beans, associée à la méthode de mise à jour de JdbcTemplate.

Le code est comme suit:

Dans mon 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;
    //...
}

Dans une 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

Je souhaite proposer une alternative à la solution acceptée de @Morten Berg, qui a mieux fonctionné pour moi.

Cette approche permet de définir le champ avec le Numbertype réellement souhaité - Longdans mon cas d'utilisation - au lieu de GeneralSequenceNumber. Cela peut être utile, par exemple pour la (dé-) sérialisation JSON.

L'inconvénient est qu'il nécessite un peu plus de surcharge de la base de données.


Tout d'abord, nous avons besoin d'un ActualEntitydans lequel nous voulons auto-incrémenter le generatedtype Long:

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

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

    // ...

}

Ensuite, nous avons besoin d'une entité d'assistance Generated. Je l'ai placé package-private à côté de ActualEntity, pour qu'il reste un détail d'implémentation du package:

@Entity
class Generated {

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

}

Enfin, nous avons besoin d'un endroit pour accrocher juste avant d'enregistrer le fichier ActualEntity. Là, nous créons et persistons une Generatedinstance. Ceci fournit alors une séquence de base de données générée idde type Long. Nous utilisons cette valeur en l'écrivant dansActualEntity.generated .

Dans mon cas d'utilisation, j'ai implémenté cela à l'aide d'un Spring Data REST @RepositoryEventHandler, qui est appelé juste avant que le ActualEntityget ne persiste. Il doit démontrer le principe:

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

}

Je ne l'ai pas testé dans une application réelle, alors profitez-en avec précaution.


-1

J'ai été dans une situation comme vous (séquence JPA / Hibernate pour le champ non @Id) et j'ai fini par créer un déclencheur dans mon schéma db qui ajoute un numéro de séquence unique lors de l'insertion. Je ne l'ai jamais fait fonctionner avec JPA / Hibernate


-1

Après avoir passé des heures, cela m'a parfaitement aidé à résoudre mon problème:

Pour Oracle 12c:

ID NUMBER GENERATED as IDENTITY

Pour H2:

ID BIGINT GENERATED as auto_increment

Faites également:

@Column(insertable = false)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.