Hibernate JPA Sequence (no Id)


138

¿Es posible usar una secuencia DB para alguna columna que no es el identificador / no es parte de un identificador compuesto ?

Estoy usando hibernate como proveedor de jpa, y tengo una tabla que tiene algunas columnas que son valores generados (usando una secuencia), aunque no son parte del identificador.

Lo que quiero es usar una secuencia para crear un nuevo valor para una entidad, donde la columna para la secuencia NO es (parte de) la clave principal:

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

}

Entonces cuando hago esto:

em.persist(new MyEntity());

se generará la identificación, pero la mySequenceValpropiedad también será generada por mi proveedor de JPA.

Solo para aclarar las cosas: quiero que Hibernate genere el valor de la mySequencedValuepropiedad. Sé que Hibernate puede manejar valores generados por la base de datos, pero no quiero usar un disparador ni ninguna otra cosa que no sea Hibernate para generar el valor de mi propiedad. Si Hibernate puede generar valores para las claves primarias, ¿por qué no puede generar para una propiedad simple?

Respuestas:


76

Buscando respuestas a este problema, me topé con este enlace

Parece que Hibernate / JPA no puede crear automáticamente un valor para sus propiedades que no son id. La @GeneratedValueanotación solo se usa junto con @Idpara crear números automáticos.

La @GeneratedValueanotación solo le dice a Hibernate que la base de datos está generando este valor por sí misma.

La solución (o solución alternativa) sugerida en ese foro es crear una entidad separada con un Id generado, algo como esto:

@Entidad
clase pública GeneralSequenceNumber {
  @Carné de identidad
  @GeneratedValue (...)
  número largo privado;
}

@Entidad 
clase pública MyEntity {
  @Carné de identidad ..
  Identificación larga privada;

  @Doce y cincuenta y nueve de la noche(...)
  privado GeneralSequnceNumber myVal;
}

Del documento de Java de @GeneratedValue: "La anotación GeneratedValue se puede aplicar a una propiedad o campo de clave principal de una entidad o superclase asignada junto con la anotación Id"
Kariem

11
Descubrí que @Column (columnDefinition = "serial") funciona perfecto pero solo para PostgreSQL. Para mí, esta fue la solución perfecta, porque la segunda entidad es la opción "fea"
Sergey Vedernikov

@SergeyVedernikov que fue extremadamente útil. ¿Te importaría publicar eso como una respuesta separada? Resolvió mi problema de manera muy simple y efectiva.
Matt Ball

@MattBall, publiqué esto como respuesta separada :) stackoverflow.com/a/10647933/620858
Sergey Vedernikov

1
He abierto una propuesta para permitir @GeneratedValueen campos que no son id. Vote para ser incluido en 2.2 java.net/jira/browse/JPA_SPEC-113
Petar Tahchiev el

44

Descubrí que @Column(columnDefinition="serial")funciona perfecto pero solo para PostgreSQL. Para mí, esta fue la solución perfecta, porque la segunda entidad es la opción "fea".


Hola, necesitaría una explicación al respecto. ¿Me podría decir más por favor?
Emaborsa

2
@Emaborsa El columnDefinition=bit básicamente le dice a Hiberate que no intente generar la definición de columna y en su lugar use el texto que ha proporcionado. Esencialmente, su DDL para la columna será literalmente name + columnDefinition. En este caso (PostgreSQL), mycolumn seriales una columna válida en una tabla.
Patrick

77
El equivalente para MySQL es@Column(columnDefinition = "integer auto_increment")
Richard Kennard el

2
¿Este auto genera su valor? Intenté persistir en una entidad con una definición de campo como esta, pero no generó un valor. arrojó un valor nulo en la columna <columna> viola la restricción no nula
KyelJmD

77
Solía @Column(insertable = false, updatable = false, columnDefinition="serial")evitar que hibernate intentara insertar valores nulos o actualizar el campo. Luego debe volver a consultar la base de datos para obtener la identificación generada después de una inserción si necesita usarla de inmediato.
Robert Di Paolo el

20

Sé que esta es una pregunta muy antigua, pero se mostró en primer lugar en los resultados y jpa ha cambiado mucho desde la pregunta.

La forma correcta de hacerlo ahora es con la @Generatedanotación. Puede definir la secuencia, establecer el valor predeterminado en la columna para esa secuencia y luego asignar la columna como:

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

1
Esto todavía requiere que la base de datos genere el valor, que realmente no responde la pregunta. Para las bases de datos Oracle anteriores a 12c, aún necesitaría escribir un activador de base de datos para generar el valor.
Bernie

9
Además, esta es una anotación de Hibernate, no JPA.
caarlos0

14

Hibernate definitivamente apoya esto. De los documentos:

"Las propiedades generadas son propiedades que tienen sus valores generados por la base de datos. Por lo general, las aplicaciones de Hibernate necesitaban actualizar los objetos que contenían propiedades para las cuales la base de datos generaba valores. Sin embargo, marcar las propiedades como generadas permite que la aplicación delegue esta responsabilidad a Hibernate. Esencialmente, cada vez que Hibernate emite un INSERTAR o ACTUALIZAR SQL para una entidad que ha definido las propiedades generadas, inmediatamente emite una selección para recuperar los valores generados ".

Para las propiedades generadas solo en la inserción, su asignación de propiedades (.hbm.xml) se vería así:

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

Para las propiedades generadas al insertar y actualizar su mapeo de propiedades (.hbm.xml) se vería así:

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

Desafortunadamente, no conozco JPA, así que no sé si esta función está expuesta a través de JPA (sospecho que posiblemente no)

Alternativamente, debería poder excluir la propiedad de las inserciones y actualizaciones, y luego llamar "manualmente" session.refresh (obj); después de haberlo insertado / actualizado para cargar el valor generado desde la base de datos.

Así es como excluiría la propiedad de ser utilizada en declaraciones de inserción y actualización:

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

Nuevamente, no sé si JPA expone estas características de Hibernate, pero Hibernate las admite.


1
La anotación @Generated corresponde a la configuración XML anterior. Consulte esta sección de los documentos de hibernación para obtener más detalles.
Eric

8

Como seguimiento, así es como lo hice funcionar:

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

Una variante con Hibernate 4.2.19 y oráculo: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Aaron

6

Arreglé la generación de UUID (o secuencias) con Hibernate usando la @PrePersistanotación:

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

5

Aunque este es un hilo viejo, quiero compartir mi solución y espero obtener algunos comentarios al respecto. Tenga en cuenta que solo probé esta solución con mi base de datos local en algunos casos de prueba JUnit. Por lo tanto, esta no es una característica productiva hasta ahora.

Resolví ese problema para mí introduciendo una anotación personalizada llamada Secuencia sin propiedad. Es solo un marcador para los campos a los que se les debe asignar un valor de una secuencia incrementada.

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

Usando esta anotación marqué mis entidades.

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

Para mantener la base de datos independiente, introduje una entidad llamada SequenceNumber que contiene el valor actual de la secuencia y el tamaño del incremento. Elegí el className como clave única para que cada clase de entidad obtenga su propia secuencia.

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

El último paso y el más difícil es un PreInsertListener que maneja la asignación del número de secuencia. Tenga en cuenta que usé la primavera como contenedor de frijoles.

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

Como puede ver en el código anterior, el oyente utilizó una instancia de SequenceNumber por clase de entidad y se reserva un par de números de secuencia definidos por el incrementValue de la entidad SequenceNumber. Si se queda sin números de secuencia, carga la entidad SequenceNumber para la clase de destino y reserva los valores incrementValue para las próximas llamadas. De esta manera, no necesito consultar la base de datos cada vez que se necesita un valor de secuencia. Tenga en cuenta la sesión sin estado que se está abriendo para reservar el siguiente conjunto de números de secuencia. No puede usar la misma sesión en la que persiste la entidad de destino, ya que esto conduciría a una ConcurrentModificationException en EntityPersister.

Espero que esto ayude a alguien.


5

Si está utilizando postgresql
Y lo estoy usando en spring boot 1.5.6

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

1
También funcionó para mí, estoy usando Spring Boot 2.1.6.RELEASE, Hibernate 5.3.10.Final, además de lo que ya se ha señalado, tuve que crear una secuencia seq_ordery hacer referencia desde el campo, nextval('seq_order'::regclass)
OJVM

3

Corro en la misma situación que usted y tampoco encontré ninguna respuesta seria si es básicamente posible generar propiedades que no sean de identificación con JPA o no.

Mi solución es llamar a la secuencia con una consulta JPA nativa para configurar la propiedad a mano antes de persuadirla.

Esto no es satisfactorio, pero por el momento funciona como una solución alternativa.

Mario


2

Encontré esta nota específica en la sesión 9.1.9 Anotación GeneratedValue de la especificación JPA: "[43] Las aplicaciones portátiles no deben usar la anotación GeneratedValue en otros campos o propiedades persistentes". Por lo tanto, supongo que no es posible generar automáticamente un valor para valores de clave no primarios, al menos utilizando simplemente JPA.


1

Parece que el hilo es viejo, solo quería agregar mi solución aquí (Uso de AspectJ - AOP en primavera).

La solución es crear una anotación personalizada de la @InjectSequenceValuesiguiente manera.

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

Ahora puede anotar cualquier campo en la entidad, de modo que el valor del campo subyacente (Largo / Entero) se inyectará en tiempo de ejecución utilizando el siguiente valor de la secuencia.

Anotar así.

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

Hasta ahora hemos marcado el campo que necesitamos para inyectar el valor de la secuencia, así que veremos cómo inyectar el valor de la secuencia en los campos marcados, esto se hace creando el corte de punto en AspectJ.

Activaremos la inyección justo antes de save/persistque se ejecute el método. Esto se hace en la siguiente clase.

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

Ahora puede anotar cualquier entidad de la siguiente manera.

@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

"No quiero usar un activador ni ninguna otra cosa que no sea Hibernate para generar el valor de mi propiedad"

En ese caso, ¿qué tal crear una implementación de UserType que genere el valor requerido y configurar los metadatos para usar ese UserType para la persistencia de la propiedad mySequenceVal?


0

Esto no es lo mismo que usar una secuencia. Al usar una secuencia, no está insertando ni actualizando nada. Simplemente está recuperando el siguiente valor de secuencia. Parece que hibernate no lo admite.


0

Si tiene una columna con el tipo UNIQUEIDENTIFIER y la generación predeterminada es necesaria en la inserción pero la columna no es PK

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

En db tendrás

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

En este caso no definirá generador para un valor que necesita (será automáticamente gracias a columnDefinition="UNIQUEIDENTIFIER"). Lo mismo que puedes probar para otros tipos de columna


0

He encontrado una solución para esto en las bases de datos MySql usando @PostConstruct y JdbcTemplate en una aplicación Spring. Puede ser factible con otras bases de datos, pero el caso de uso que presentaré se basa en mi experiencia con MySql, ya que utiliza auto_increment.

Primero, intenté definir una columna como auto_increment usando la propiedad ColumnDefinition de la anotación @Column, pero no funcionaba ya que la columna necesitaba ser una clave para ser incremental automático, pero aparentemente la columna no se definiría como un índice hasta después de que se definió, causando un punto muerto.

Aquí es donde llegué con la idea de crear la columna sin la definición de auto_increment y agregarla después de crear la base de datos. Esto es posible utilizando la anotación @PostConstruct, que hace que se invoque un método justo después de que la aplicación haya inicializado los beans, junto con el método de actualización de JdbcTemplate.

El código es el siguiente:

En mi entidad:

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

En una clase 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

Quiero proporcionar una alternativa junto a la solución aceptada de @Morten Berg, que funcionó mejor para mí.

Este enfoque permite definir el campo con el Numbertipo realmente deseado , Longen mi caso de uso, en lugar de GeneralSequenceNumber. Esto puede ser útil, por ejemplo, para la (des) serialización JSON.

La desventaja es que requiere un poco más de sobrecarga de la base de datos.


Primero, necesitamos un ActualEntityen el que queremos auto-incremento generatedde tipo Long:

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

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

    // ...

}

A continuación, necesitamos una entidad auxiliar Generated. Lo coloqué junto al paquete privado ActualEntitypara mantenerlo como un detalle de implementación del paquete:

@Entity
class Generated {

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

}

Finalmente, necesitamos un lugar para conectar justo antes de guardar el ActualEntity. Allí, creamos y persistimos una Generatedinstancia. Esto proporciona una secuencia de base de datos generada idde tipo Long. Hacemos uso de este valor escribiéndolo en ActualEntity.generated.

En mi caso de uso, implementé esto usando un Spring Data REST @RepositoryEventHandler, que se llama justo antes ActualEntityde que persista el get. Debe demostrar el 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);
    }

}

No lo probé en una aplicación de la vida real, así que disfruta con cuidado.


-1

He estado en una situación como usted (secuencia JPA / Hibernate para el campo que no es @Id) y terminé creando un disparador en mi esquema db que agrega un número de secuencia único en la inserción. Nunca lo hice funcionar con JPA / Hibernate


-1

Después de pasar horas, esto me ayudó a resolver mi problema:

Para Oracle 12c:

ID NUMBER GENERATED as IDENTITY

Para H2:

ID BIGINT GENERATED as auto_increment

También haz:

@Column(insertable = false)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.