it-swarm.com.de

JPY-Sequenz im Ruhezustand (Nicht-Id)

Kann eine DB-Sequenz für eine Spalte verwendet werden, die nicht der Bezeichner ist/nicht Teil eines zusammengesetzten Bezeichners ist

Ich verwende Hibernate als JPA-Provider und habe eine Tabelle, die einige Spalten enthält, deren Werte (mithilfe einer Sequenz) generiert werden, obwohl sie nicht Teil des Bezeichners sind.

Ich möchte eine Sequenz verwenden, um einen neuen Wert für eine Entität zu erstellen, wobei die Spalte für die SequenzNICHT(Teil des) Primärschlüssels ist:

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

}

Dann, wenn ich das mache:

em.persist(new MyEntity());

die ID wird generiert, aber die mySequenceVal-Eigenschaft wird auch von meinem JPA-Provider generiert.

Um es klar zu stellen: Ich möchte Hibernate , um den Wert für die mySequencedValue-Eigenschaft zu generieren. Ich weiß, dass Hibernate mit datenbankgenerierten Werten umgehen kann, aber ich möchte keinen Auslöser oder etwas anderes als Hibernate verwenden, um den Wert für meine Eigenschaft zu generieren. Wenn der Ruhezustand Werte für Primärschlüssel generieren kann, warum kann er nicht für eine einfache Eigenschaft generiert werden?

109
Miguel Ping

Auf der Suche nach Antworten auf dieses Problem stieß ich auf diesen Link

Es scheint, dass Hibernate/JPA nicht in der Lage ist, automatisch einen Wert für Ihre Nicht-ID-Eigenschaften zu erstellen. Die Annotation @GeneratedValue wird nur in Verbindung mit @Id zum Erstellen von automatischen Nummern verwendet.

Die @GeneratedValue-Annotation teilt Hibernate lediglich mit, dass die Datenbank diesen Wert selbst generiert.

Die in diesem Forum vorgeschlagene Lösung (oder Problemumgehung) besteht darin, eine separate Entität mit einer generierten ID zu erstellen, etwa wie folgt:

 @ Entity 
 Public Klasse GeneralSequenceNumber {
 @Ich würde
 @GeneratedValue (...) 
 private Lange Nummer; 
} 

 @ Entity 
 public Klasse MyEntity {
 @Ich würde ..
 private Lange ID; 

 @Eins zu eins(...)
 private GeneralSequnceNumber myVal; 
} 
58
Morten Berg

Ich fand, dass @Column(columnDefinition="serial") perfekt funktioniert, aber nur für PostgreSQL. Für mich war das die perfekte Lösung, weil die zweite Entität "hässlich" ist.

32

Ich weiß, dass dies eine sehr alte Frage ist, aber sie zeigt sich erstens auf den Ergebnissen und jpa hat sich seit der Frage sehr verändert.

Der richtige Weg, dies jetzt zu tun, ist die Annotation @Generated. Sie können die Reihenfolge festlegen, den Standard in der Spalte auf diese Reihenfolge festlegen und die Spalte dann wie folgt zuordnen:

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

Hibernate unterstützt dies definitiv. Aus den Dokumenten:

"Generierte Eigenschaften sind Eigenschaften, deren Werte von der Datenbank generiert werden. Normalerweise mussten Hibernate-Anwendungen Objekte aktualisieren, die Eigenschaften enthalten, für die die Datenbank Werte generierte. Durch das Markieren von Eigenschaften als generiert kann die Anwendung diese Verantwortung jedoch an Hibernate delegieren. Immer wenn Hibernate für eine Entität, für die generierte Eigenschaften definiert wurden, ein SQL INSERT oder UPDATE ausgibt, gibt sie sofort eine Auswahl aus, um die generierten Werte abzurufen. "

Für Eigenschaften, die nur beim Einfügen generiert werden, würde Ihre Eigenschaftszuordnung (.hbm.xml) folgendermaßen aussehen:

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

Für Eigenschaften, die beim Einfügen und Aktualisieren generiert wurden, würde Ihre Eigenschaftszuordnung (.hbm.xml) folgendermaßen aussehen:

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

Leider kenne ich JPA nicht, daher weiß ich nicht, ob diese Funktion über JPA verfügbar gemacht wird (ich vermute, möglicherweise nicht).

Alternativ sollten Sie die Eigenschaft von Einfügungen und Aktualisierungen ausschließen können. Anschließend rufen Sie session.refresh (obj) "manuell" auf. nachdem Sie ihn eingefügt/aktualisiert haben, um den generierten Wert aus der Datenbank zu laden.

So würden Sie die Eigenschaft von der Verwendung in Einfügungs- und Aktualisierungsanweisungen ausschließen:

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

Ich weiß auch nicht, ob JPA diese Hibernate-Funktionen verfügbar macht, aber Hibernate unterstützt sie.

14
alasdairg

Als Folgemaßnahme habe ich es so geschafft:

@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

Obwohl dies ein alter Thread ist, möchte ich meine Lösung mitteilen und hoffentlich ein paar Rückmeldungen dazu erhalten. Seien Sie gewarnt, dass ich diese Lösung nur mit meiner lokalen Datenbank in einem JUnit-Testfall getestet habe. Dies ist also bisher kein produktives Merkmal.

Ich habe dieses Problem für mich gelöst, indem ich eine benutzerdefinierte Anmerkung namens Sequenz ohne Eigenschaft eingeführt habe. Es ist nur eine Markierung für Felder, denen ein Wert aus einer inkrementierten Sequenz zugewiesen werden soll.

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

Mit dieser Annotation habe ich meine Entitäten markiert.

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

Um die Datenbank unabhängig zu halten, habe ich eine Entität namens SequenceNumber eingeführt, die den aktuellen Wert der Sequenz und die Inkrementgröße enthält. Ich habe den Klassennamen als eindeutigen Schlüssel gewählt, damit jede Entitätsklasse ihre eigene Sequenz erhält.

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

Der letzte und schwierigste Schritt ist ein PreInsertListener, der die Zuweisung der Sequenznummer übernimmt. Beachten Sie, dass ich Spring als Bohnenbehälter verwendet habe.

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

Wie Sie dem obigen Code entnehmen können, hat der Listener eine SequenceNumber-Instanz pro Entitätsklasse verwendet und reserviert einige Sequenznummern, die durch den incrementValue der SequenceNumber-Entität definiert werden. Wenn die Sequenznummern nicht ausreichen, wird die Entity SequenceNumber für die Zielklasse geladen, und die incrementValue-Werte werden für die nächsten Aufrufe reserviert. Auf diese Weise muss die Datenbank nicht jedes Mal abgefragt werden, wenn ein Sequenzwert benötigt wird. Beachten Sie die StatelessSession, die zum Reservieren des nächsten Satzes von Sequenznummern geöffnet wird. Sie können nicht dieselbe Sitzung verwenden, in der die Zielentität derzeit persistent ist, da dies zu einer ConcurrentModificationException im EntityPersister führen würde.

Hoffe das hilft jemandem.

4
Sebastian Götz

Ich bin in der gleichen Situation wie Sie und habe auch keine ernsthaften Antworten gefunden, ob es grundsätzlich möglich ist, Nicht-ID-Eigenschaften mit JPA zu generieren oder nicht.

Meine Lösung ist, die Sequenz mit einer nativen JPA-Abfrage aufzurufen, um die Eigenschaft vor dem Weiterverfolgen manuell festzulegen.

Dies ist nicht zufriedenstellend, funktioniert aber im Moment als Workaround.

Mario

3
Mario

Ich habe die Generierung von UUID (oder Sequenzen) mit Hibernate mit @PrePersist Annotation behoben:

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

Ich habe diese spezielle Anmerkung in Sitzung 9.1.9 GeneratedValue Annotation aus der JPA-Spezifikation gefunden: "[43] Portable Anwendungen sollten die GeneratedValue-Annotation nicht für andere persistente Felder oder Eigenschaften verwenden." Es ist nicht möglich, automatisch einen Wert für Werte von Nicht-Primärschlüsseln zu generieren, zumindest unter Verwendung von JPA.

1
Gustavo Orair

Dies ist nicht dasselbe wie das Verwenden einer Sequenz. Wenn Sie eine Sequenz verwenden, fügen Sie nichts ein oder aktualisieren nichts. Sie rufen einfach den nächsten Sequenzwert ab. Es scheint, als würde der Winterschlaf es nicht unterstützen.

0
kammy

"Ich möchte keinen Auslöser oder etwas anderes als Hibernate verwenden, um den Wert für mein Eigentum zu generieren."

In diesem Fall können Sie eine Implementierung von UserType erstellen, die den erforderlichen Wert generiert, und die Metadaten so konfigurieren, dass dieser UserType für die Persistenz der mySequenceVal-Eigenschaft verwendet wird.

0
alasdairg

Anscheinend ist der Thread alt, ich wollte hier nur meine Lösung hinzufügen (mit AspectJ - AOP im Frühjahr).

Die Lösung besteht darin, eine benutzerdefinierte Anmerkung @InjectSequenceValue wie folgt zu erstellen.

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

Jetzt können Sie jedes Feld in der Entität mit Anmerkungen versehen, sodass der zugrunde liegende Feldwert (Long/Integer) zur Laufzeit mit dem nächsten Wert der Sequenz eingefügt wird.

Kommentieren Sie so.

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

Bisher haben wir das Feld markiert, das wir zum Einfügen des Sequenzwerts benötigen. Wir werden also schauen, wie der Sequenzwert in die markierten Felder eingefügt wird, indem wir den Punktschnitt in AspectJ erstellen.

Wir werden die Injection kurz vor der Ausführung der Methode save/persist auslösen. Dies wird in der folgenden Klasse durchgeführt.

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

Jetzt können Sie eine Entität wie unten beschrieben mit Anmerkungen versehen.

@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

Wenn Sie Postgresql verwenden 
Und ich benutze im Springboot 1.5.6

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