web-dev-qa-db-fra.com

Séquence JPA d'Hibernate (non-Id)

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

J'utilise hibernate en tant que fournisseur jpa et ma table contient des colonnes qui sont des valeurs générées (à l'aide d'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 estPAS(une 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'identifiant sera généré, mais la propriété mySequenceVal sera également générée par mon fournisseur JPA.

Juste pour clarifier les choses: je veux Hibernate générer la valeur de la propriété mySequencedValue. Je sais que Hibernate peut gérer les valeurs générées par une base de données, mais je ne souhaite pas utiliser de déclencheur ni autre chose que Hibernate 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?

109
Miguel Ping

Cherchant des réponses à ce problème, je suis tombé sur ce lien

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

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

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

 @ Entity 
 Public class GeneralSequenceNumber {
 @Id 
 @GeneratedValue (...) 
 private Long number; 
} 

 @ Entity 
 public class MyEntity {
 @Id ..
 private Long id; 

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

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 l'option "laide".

32

Je sais que c’est une très vieille question, mais c’est d’abord sur les résultats et jpa a beaucoup changé depuis la question.

La bonne façon de procéder consiste à utiliser l'annotation @Generated. 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 suit:

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

Hibernate soutient définitivement cela. De la docs:

"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 doivent rafraîchir les objets contenant les propriétés pour lesquelles la base de données générait des valeurs. Toutefois, le marquage des propriétés ainsi généré permet à l'application de déléguer cette responsabilité à Hibernate. Essentiellement, chaque fois que Hibernate émet une instruction SQL INSERT ou UPDATE pour une entité qui a défini les propriétés générées, il émet immédiatement une sélection pour récupérer les valeurs générées. "

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

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

Pour les propriétés générées lors de l'insertion et la mise à jour de votre propriété, le mappage (.hbm.xml) se présente comme suit:

<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 suppose que ce n’est peut-être pas le cas).

Sinon, vous devriez 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 pouvez exclure la propriété de son utilisation dans les instructions insert et update:

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

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

14
alasdairg

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

Bien que ce soit un vieux sujet, je souhaite partager ma solution et espérer obtenir quelques retours à ce sujet. Soyez averti que je n'ai testé cette solution qu'avec ma base de données locale dans certains tests JUnit. Donc, ce n'est pas une fonctionnalité productive jusqu'à présent.

J'ai résolu ce problème en introduisant une annotation personnalisée appelée Séquence sans propriété. Il s'agit simplement d'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 que la base de données reste indépendante, 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 nom de classe 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 du numéro de séquence. Notez que j'ai utilisé le printemps 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 dans le code ci-dessus, l'écouteur a utilisé une instance de SequenceNumber par classe d'entité et a réservé deux 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 prochains appels. 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 requise . Notez la StatelessSession en cours d'ouverture pour la réservation du prochain jeu de numéros de séquence. Vous ne pouvez pas utiliser la même session que l'entité cible qui est actuellement conservée car cela entraînerait une exception ConcurrentModificationException dans EntityPersister.

J'espère que ça aide quelqu'un.

4
Sebastian Götz

Je cours dans la même situation que vous et je n’ai pas non plus trouvé de réponse sérieuse s’il est fondamentalement possible de générer des propriétés non-id avec JPA ou non.

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

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

Mario

3
Mario

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

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

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 doivent pas utiliser l'annotation GeneratedValue sur d'autres champs ou propriétés persistants." Il est impossible de générer automatiquement des valeurs pour des valeurs de clés non primaires, du moins en utilisant simplement JPA.

1
Gustavo Orair

Ce n'est pas la même chose que d'utiliser une séquence. Lors de l'utilisation d'une séquence, vous n'insérez ni ne mettez à jour quoi que ce soit. Vous récupérez simplement la valeur de séquence suivante. Il semble que le mode veille prolongée ne le supporte pas.

0
kammy

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

Dans ce cas, pourquoi ne pas créer une implémentation de UserType qui génère la valeur requise et configurer les métadonnées pour utiliser ce type d'utilisateur pour la persistance de la propriété mySequenceVal?

0
alasdairg

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

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

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

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

Annoter comme ça.

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

Jusqu'ici, nous avons marqué le champ dont nous avons besoin d'injecter la valeur de séquence. Nous allons donc voir comment injecter la valeur de séquence aux champs marqués, en créant le point coupé dans AspectJ.

Nous allons déclencher l'injection juste avant l'exécution de la méthode save/persist. Cela 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

Si vous utilisez postgresql 
Et je me sers de la botte de printemps 1.5.6

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