web-dev-qa-db-fra.com

Spécifiez manuellement la valeur d'une clé primaire dans la colonne JPA @GeneratedValue

J'ai une entité qui a un champ clé/id primaire comme le suivant:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

Cela fonctionne bien. J'utilise EclipseLink pour créer le schéma DDL, et la colonne est correctement créée comme suit:

`id` bigint(20) NOT NULL AUTO_INCREMENT

Cependant, j'ai plusieurs entités pour lesquelles je veux spécifier le PK moi-même (c'est une petite application qui transfère les données d'une ancienne base de données vers la nouvelle que nous construisons). Si je spécifie l'ID du POJO (en utilisant setId(Long id)) et que je le persiste, EclipseLink ne l'enregistre pas (c'est-à-dire que l'enregistrement est enregistré, mais l'identifiant est généré automatiquement par eclipseLink).

Existe-t-il un moyen de spécifier manuellement la valeur d'une colonne qui a une @GeneratedValue?

Voici quelques réflexions sur la question:

J'ai essayé de contourner le problème en n'utilisant pas du tout @GeneratedValue, mais en définissant simplement manuellement la colonne à AUTO_INCREMENTed. Cependant, cela m'oblige à fournir manuellement un ID toujours , car EclipseLink valide la clé primaire (il ne peut donc pas être nul, zéro ou un nombre négatif) . Le message d'exception indique que je dois spécifier eclipselink.id_validation, mais cela ne semble pas faire de différence (j'ai annoté @PrimaryKey(validation = IdValidation.NONE) mais j'ai toujours le même message).

Pour clarifier: J'utilise EclipseLink (2.4.0) comme fournisseur de persistance et je ne peux pas m'en éloigner (de grandes parties du projet dépendent des conseils de requête, des annotations et des classes spécifiques à eclipselink) .


EDIT (en réponse aux réponses):

Séquençage personnalisé: J'ai essayé d'implémenter mon propre séquençage. J'ai essayé de sous-classer DefaultSequence, mais EclipseLink me dira Internal Exception: org.Eclipse.persistence.platform.database.MySQLPlatform could not be found. Mais j'ai vérifié: la classe est sur le chemin de classe.

J'ai donc sous-classé une autre classe, NativeSequence:

public class MyNativeSequence extends NativeSequence {

    public MyNativeSequence() {
        super();
    }

    public MyNativeSequence(final String name) {
        super(name);
    }


    @Override
    public boolean shouldAlwaysOverrideExistingValue() {
        return false;
    }

    @Override
    public boolean shouldAlwaysOverrideExistingValue(final String seqName) {
        return false;
    }

}

Cependant, ce que j'obtiens est le suivant:

javax.persistence.RollbackException: Exception [EclipseLink-7197] (Eclipse Persistence Services - 2.4.0.v20120608-r11652): org.Eclipse.persistence.exceptions.ValidationException
Exception Description: Null or zero primary key encountered in unit of work clone [de.dfv.datenbank.domain.Mitarbeiter[ id=null ]], primary key [null]. Set descriptors IdValidation or the "eclipselink.id-validation" property.
    at org.Eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commitInternal(EntityTransactionImpl.Java:102)
    ...
Caused by: Exception [EclipseLink-7197] (Eclipse Persistence Services - 2.4.0.v20120608-r11652): org.Eclipse.persistence.exceptions.ValidationException
Exception Description: Null or zero primary key encountered in unit of work clone [de.dfv.datenbank.domain.Mitarbeiter[ id=null ]], primary key [null]. Set descriptors IdValidation or the "eclipselink.id-validation" property.
    at org.Eclipse.persistence.exceptions.ValidationException.nullPrimaryKeyInUnitOfWorkClone(ValidationException.Java:1451)
    ...

(trace de pile raccourcie pour plus de clarté). C'est le même message que j'ai reçu auparavant. Ne devrais-je pas sous-classer NativeSequence? Si c'est le cas, je ne sais pas quoi implémenter pour les méthodes abstraites dans Sequence ou StandardSequence.

Il convient également de noter que le simple sous-classement (sans remplacer aucune méthode) de la classe fonctionne comme prévu. Cependant, returing false dans shouldAlwaysOverrideExistingValue(...) ne générera pas du tout une valeur (j'ai parcouru le programme et getGeneratedValue() n'est pas appelé une fois).

De plus, lorsque j'insère comme 8 entités d'un certain type dans une transaction, il en résulte 11 enregistrements dans la base de données (quoi diable?!).

EDIT (2012-09-01): Je n'ai toujours pas de solution au problème, l'implémentation de ma propre séquence ne l'a pas résolu. Ce dont j'ai besoin est un moyen de ne pas pouvoir définir explicitement un identifiant (il sera donc généré automatiquement) et de pouvoir définir explicitement un identifiant (il sera donc utilisé pour la création de l'enregistrement dans la base de données).

J'ai essayé de définir la colonne comme auto_increment moi-même et d'omettre @GeneratedValue, cependant la validation va démarrer et ne me permettra pas de sauvegarder une telle entité. Si je spécifie une valeur! = 0 et! = Zéro, mysql se plaindra pour une clé primaire en double.

Je suis à court d'idées et d'options pour essayer. Tout? (commencer une prime)

28
scravy

Cela fonctionne avec eclipselink. Cela créera une table séparée pour la séquence, mais cela ne devrait pas poser de problème.

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="id", insertable=true, updatable=true, unique=true, nullable=false)
private Long id;

GenerationType.AUTO choisira la stratégie de génération idéale. Étant donné que le champ est spécifié comme insérable et pouvant être mis à jour, une stratégie de génération de TABLE sera utilisée. Cela signifie qu'eclipselink va générer une autre table contenant la valeur de séquence actuelle et générer la séquence elle-même au lieu de la déléguer à la base de données. Puisque la colonne est déclarée insérable, si id est nul lorsqu'il persiste, eclipselink générera l'id. Sinon, l'identifiant existant sera utilisé.

8
poison

Si vous utilisez le séquençage TABLE, EclipseLink vous permettra de remplacer la valeur (ou SEQUENCE si votre base de données le supporte, MySQL ne le fait pas).

Pour l'IDENTITÉ, je ne suis même pas sûr que MySQL vous permettra de fournir votre propre identifiant, vous voudrez peut-être le vérifier. En général, je ne recommanderais jamais d'utiliser IDENTITY car il ne prend pas en charge la préallocation.

Il y a quelques problèmes à autoriser IDENTITY à fournir l'identifiant ou non. La première est que deux insertions SQL différentes devront être générées en fonction de la valeur de l'ID, car pour l'IDENTITÉ, l'ID ne peut pas du tout être inséré. Vous souhaiterez peut-être enregistrer un bogue pour que les identifiants fournis par le support IDENTITY soient fournis par l'utilisateur.

Vous devriez toujours pouvoir le faire fonctionner avec votre propre sous-classe Sequence, ou éventuellement la sous-classe MySQLPlatform. Vous définiriez votre sous-classe MySQLPlatform en utilisant la propriété d'unité de persistance "eclipselink.target-database".

7
James

Solution centrée sur la base de données à votre problème:

  1. Créez une colonne auxiliaire, nullable dans votre table. Il contiendra vos identifiants attribués manuellement:

    CREATE TABLE `test_table`
    (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `manual_id` bigint(20) NULL,
      `some_other_field` varchar(200) NOT NULL,
      PRIMARY KEY(id)
    );
    
  2. Mappez cette colonne à un champ normal dans votre entité:

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name="manual_id")
    private Integer manualId;
    
  3. Créez un déclencheur qui définit l'ID de table sur l'ID attribué manuellement s'il n'est pas nul:

    DELIMITER //
    CREATE TRIGGER `test_table_bi` BEFORE INSERT ON `test_table`
      FOR EACH ROW 
      BEGIN
        IF NEW.`manual_id` IS NOT NULL THEN 
          SET NEW.`id` = NEW.`manual_id`;
        END IF;
    END;//
    DELIMITER;    
    
  4. Utilisez toujours le manualId lorsque vous devez attribuer un identifiant personnalisé. Le déclencheur fera la magie pour vous:

    testEntiy.setManualId(300);
    entityManager.persist(testEntity);
    
  5. Après la phase d'importation de la base de données, supprimez simplement le déclencheur, la colonne auxiliaire et son mappage.

    DROP TRIGGER `test_table_bi`;
    ALTER TABLE `test_table` DROP COLUMN `manual_id`;
    

Avertissement

Si vous spécifiez manuellement un identifiant supérieur à la valeur actuelle de AUTO_INCREMENT, Le id généré suivant passera à la valeur de l'identifiant attribué manuellement plus 1, par exemple:

INSERT INTO `test_table` (manual_id, some_other_field) VALUES (50, 'Something');
INSERT INTO `test_table` (manual_id, some_other_field) VALUES (NULL, 'Something else');
INSERT INTO `test_table` (manual_id, some_other_field) VALUES (90, 'Something 2');
INSERT INTO `test_table` (manual_id, some_other_field) VALUES (NULL, 'Something else 2');
INSERT INTO `test_table` (manual_id, some_other_field) VALUES (40, 'Something 3');
INSERT INTO `test_table` (manual_id, some_other_field) VALUES (NULL, 'Something else 3');

Manipulera les résultats:

+----+-----------+------------------+
| id | manual_id | some_other_field |
+----+-----------+------------------+
| 50 |        50 | Something        |
| 51 |      NULL | Something else   |
| 90 |        90 | Something 2      |
| 91 |      NULL | Something else 2 |
| 40 |        40 | Something 3      |
| 92 |      NULL | Something else 3 |
+----+-----------+------------------+

Pour éviter les problèmes, il est fortement recommandé de définir la colonne AUTO_INCREMENT Pour commencer avec un nombre supérieur à tous les identifiants existants dans votre base de données précédente, par exemple:

ALTER TABLE `test_table` AUTO_INCREMENT = 100000;
6
Anthony Accioly

Il se peut que je manque quelque chose d'évident, mais pourquoi ne pas simplement définir une autre entité avec la même annotation @Table(name="....."), avec les mêmes champs, mais ne pas générer l'identifiant? Ensuite, vous pouvez utiliser cette entité pour le code qui copie les données de l'ancienne base de données vers la nouvelle, et celle avec l'ID généré peut être utilisée pour les créations normales qui nécessitent la génération d'ID.

Je ne peux pas vous dire si cela fonctionne avec EclipseLink, mais nous utilisons Hibernate ici et cela ne semble pas le déranger.

4
DuncanKinnear

L'utilisation de GenerationType.SEQUENCE avec PostgreSQL et EclipseLink a fonctionné pour moi.

1) Changer

@GeneratedValue(strategy = GenerationType.IDENTITY) 

par

@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="step_id_seq")
@SequenceGenerator(name="step_id_seq", sequenceName="step_id_seq")

Maintenant, vous pouvez appeler une séquence à l'aide de NativeQuery:

return ((Vector<Integer>) em.createNativeQuery("select nextval('step_id_seq')::int").getSingleResult()).get(0);

et définissez l'ID retourné sur votre entité avant d'appeler la méthode EntityManager.persist ().

J'espère qu'il n'est pas trop tard!

1
szarza

Recherchez le générateur d'ID personnalisé

http://blog.anorakgirl.co.uk/2009/01/custom-hibernate-sequence-generator-for-id-field/

peut-être que cela pourrait aider.

0
Dangling Piyush