Sequência JPA do Hibernate (sem ID)

138

É possível usar uma sequência de banco de dados para alguma coluna que não é o identificador / não faz parte de um identificador composto ?

Estou usando o hibernate como provedor jpa e tenho uma tabela que possui algumas colunas que são geradas por valores (usando uma sequência), embora elas não façam parte do identificador.

O que eu quero é usar uma sequência para criar um novo valor para uma entidade, onde a coluna para a sequência NÃO é (parte da) a chave primária:

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

}

Então, quando eu faço isso:

em.persist(new MyEntity());

o ID será gerado, mas a mySequenceValpropriedade também será gerada pelo meu provedor de JPA.

Apenas para esclarecer: quero que o Hibernate gere o valor da mySequencedValuepropriedade. Sei que o Hibernate pode lidar com valores gerados por banco de dados, mas não quero usar um gatilho ou qualquer outra coisa além do próprio Hibernate para gerar o valor para minha propriedade. Se o Hibernate pode gerar valores para chaves primárias, por que não pode gerar para uma propriedade simples?

Miguel Ping
fonte

Respostas:

76

Procurando respostas para este problema, deparei-me com este link

Parece que o Hibernate / JPA não pode criar automaticamente um valor para suas propriedades sem identificação. A @GeneratedValueanotação é usada apenas em conjunto com @Idpara criar números automáticos.

A @GeneratedValueanotação apenas informa ao Hibernate que o banco de dados está gerando esse valor em si.

A solução (ou solução alternativa) sugerida nesse fórum é criar uma entidade separada com um ID gerado, algo como isto:

@Entidade
classe pública GeneralSequenceNumber {
  @Eu iria
  @GeneratedValue (...)
  número longo privado;
}

@Entidade 
classe pública MyEntity {
  @Eu iria ..
  ID longo privado;

  @Um a um(...)
  private GeneralSequnceNumber myVal;
}
Morten Berg
fonte
No java doc de @GeneratedValue: "A anotação GeneratedValue pode ser aplicada a uma propriedade de chave primária ou a um campo de uma entidade ou superclasse mapeada em conjunto com a anotação de ID"
Kariem 10/09/09
11
Eu descobri que @Column (columnDefinition = "serial") funciona perfeito, mas apenas para o PostgreSQL. Para mim esta foi a solução perfeita, porque segunda entidade é opção "feio"
Sergey Vedernikov
@SergeyVedernikov que foi extremamente útil. Você se importaria de postar isso como uma resposta separada? Resolveu meu problema de maneira muito simples e eficaz.
Matt Ball
@MattBall eu publiquei isso como uma resposta separada :) stackoverflow.com/a/10647933/620858
Sergey Vedernikov
1
Abri uma proposta para permitir @GeneratedValuecampos que não são id. Por favor vote para ser incluído em 2,2 java.net/jira/browse/JPA_SPEC-113
Petar Tahchiev
44

Achei que @Column(columnDefinition="serial")funciona perfeito, mas apenas para o PostgreSQL. Para mim, essa foi a solução perfeita, porque a segunda entidade é a opção "feia".

Sergey Vedernikov
fonte
Oi, eu precisaria de uma explicação sobre isso. Você poderia me dizer mais, por favor?
Emaborsa
2
@Emaborsa O columnDefinition=bit basicamente diz ao Hiberate para não tentar gerar a definição da coluna e usar o texto que você forneceu. Essencialmente, seu DDL para a coluna será literalmente apenas nome + definição de coluna. Nesse caso (PostgreSQL), mycolumn serialé uma coluna válida em uma tabela.
Patrick
7
O equivalente para o MySQL é@Column(columnDefinition = "integer auto_increment")
Richard Kennard
2
Este auto gera seu valor? Tentei persistir em uma entidade com uma definição de campo como esta, mas ela não gerou um valor. ele jogou um valor nulo na coluna <coluna> viola restrição não nula
KyelJmD
7
Eu costumava @Column(insertable = false, updatable = false, columnDefinition="serial")impedir que o hibernate tentasse inserir valores nulos ou atualizar o campo. Você precisará consultar novamente o banco de dados para obter o ID gerado após uma inserção, se precisar usá-lo imediatamente.
Robert Di Paolo
20

Eu sei que essa é uma pergunta muito antiga, mas é mostrada primeiramente nos resultados e o jpa mudou muito desde a pergunta.

A maneira correta de fazer isso agora é com a @Generatedanotação. Você pode definir a sequência, definir o padrão na coluna para essa sequência e mapear a coluna como:

@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)
Rumal
fonte
1
Isso ainda exige que o valor seja gerado pelo banco de dados, o que realmente não responde à pergunta. Para bancos de dados Oracle anteriores a 12c, você ainda precisaria gravar um gatilho de banco de dados para gerar o valor.
Bernie
9
Além disso, esta é uma anotação do Hibernate, não JPA.
precisa saber é o seguinte
14

O Hibernate definitivamente suporta isso. Dos documentos:

"Propriedades geradas são propriedades cujos valores são gerados pelo banco de dados. Normalmente, os aplicativos Hibernate são necessários para atualizar objetos que contêm quaisquer propriedades para as quais o banco de dados estava gerando valores. No entanto, marcar propriedades como geradas permite que o aplicativo delegue essa responsabilidade ao Hibernate. Essencialmente, sempre que o Hibernate emite um SQL INSERT ou UPDATE para uma entidade que definiu propriedades geradas, ele emite imediatamente uma seleção posteriormente para recuperar os valores gerados. "

Para propriedades geradas apenas na inserção, seu mapeamento de propriedade (.hbm.xml) seria semelhante a:

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

Para propriedades geradas ao inserir e atualizar seu mapeamento de propriedades (.hbm.xml) seria semelhante a:

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

Infelizmente, eu não conheço JPA, então não sei se esse recurso é exposto via JPA (suspeito que possivelmente não)

Como alternativa, você deve poder excluir a propriedade de inserções e atualizações e depois "manualmente" chamar session.refresh (obj); depois de inseri-lo / atualizá-lo para carregar o valor gerado do banco de dados.

É assim que você excluiria a propriedade do uso nas instruções de inserção e atualização:

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

Novamente, não sei se o JPA expõe esses recursos do Hibernate, mas o Hibernate os suporta.

alasdairg
fonte
1
A anotação @Generated corresponde à configuração XML acima. Consulte esta seção dos documentos de hibernação para obter mais detalhes.
19415 Eric
8

Como acompanhamento, aqui está como eu consegui que funcionasse:

@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();
}
Paulo
fonte
Um variante com o Hibernate 4.2.19 e o oracle: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Aaron
6

Corrigi a geração de UUID (ou sequências) com o Hibernate usando a @PrePersistanotação:

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

Embora esse seja um tópico antigo, quero compartilhar minha solução e espero obter algum feedback sobre isso. Esteja avisado de que eu só testei esta solução com meu banco de dados local em alguns casos de teste JUnit. Portanto, este não é um recurso produtivo até agora.

Resolvi esse problema introduzindo uma anotação personalizada chamada Sequence sem propriedade. É apenas um marcador para os campos aos quais deve ser atribuído um valor a partir de uma sequência incrementada.

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

Usando esta anotação, marquei minhas entidades.

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

Para manter as coisas independentes do banco de dados, introduzi uma entidade chamada SequenceNumber que contém o valor atual da sequência e o tamanho do incremento. Eu escolhi o className como chave exclusiva para que cada classe de entidade obtenha sua própria sequência.

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

A última etapa e a mais difícil é um PreInsertListener que lida com a atribuição do número de sequência. Note que eu usei a primavera como recipiente de feijão.

@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 você pode ver no código acima, o ouvinte usou uma instância SequenceNumber por classe de entidade e reserva alguns números de sequência definidos pelo incrementValue da entidade SequenceNumber. Se ficar sem números de sequência, ele carregará a entidade SequenceNumber para a classe de destino e reserva os valores incrementValue para as próximas chamadas. Dessa forma, não preciso consultar o banco de dados sempre que um valor de sequência for necessário. Observe a StatelessSession que está sendo aberta para reservar o próximo conjunto de números de sequência. Você não pode usar a mesma sessão em que a entidade de destino atualmente persiste, pois isso levaria a uma ConcurrentModificationException no EntityPersister.

Espero que isso ajude alguém.

Sebastian Götz
fonte
5

Se você estiver usando o postgresql
e eu estiver usando no boot da primavera 1.5.6

@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;
Sulaymon Hursanov
fonte
1
Funcionou para mim também, estou usando a bota de mola 2.1.6.RELEASE, Hibernate 5.3.10.Final, além do que já foi apontado, tive que criar uma segurança seq_ordere fazer referência ao campo nextval('seq_order'::regclass)
OJVM
3

Eu corro na mesma situação que você e também não encontrei respostas sérias se for basicamente possível gerar propriedades sem identificação com JPA ou não.

Minha solução é chamar a sequência com uma consulta JPA nativa para definir a propriedade manualmente antes de persistir.

Isso não é satisfatório, mas funciona como uma solução alternativa no momento.

Mario


fonte
2

Encontrei esta observação específica na sessão 9.1.9 Anotação GeneratedValue da especificação JPA: "[43] Aplicativos portáteis não devem usar a anotação GeneratedValue em outros campos ou propriedades persistentes". Portanto, presumo que não seja possível gerar automaticamente valor para valores de chave não primária pelo menos usando simplesmente o JPA.

Gustavo Orair
fonte
1

Parece que o thread é antigo, eu só queria adicionar minha solução aqui (usando o AspectJ-AOP na primavera).

A solução é criar uma anotação personalizada da @InjectSequenceValueseguinte maneira.

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

Agora você pode anotar qualquer campo na entidade, para que o valor do campo subjacente (Longo / Inteiro) seja injetado no tempo de execução usando o próximo valor da sequência.

Anote assim.

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

Até agora, marcamos o campo que precisamos para injetar o valor da sequência. Então, veremos como injetar o valor da sequência nos campos marcados, isso é feito criando o corte de ponto no AspectJ.

Iremos disparar a injeção imediatamente antes da save/persistexecução do método. Isso é feito na classe abaixo.

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

Agora você pode anotar qualquer Entidade como abaixo.

@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;
}
Subin Chalil
fonte
0

"Não quero usar um gatilho ou qualquer outra coisa além do próprio Hibernate para gerar o valor para minha propriedade"

Nesse caso, que tal criar uma implementação de UserType que gere o valor necessário e configurar os metadados para usar esse UserType para persistência da propriedade mySequenceVal?

alasdairg
fonte
0

Isso não é o mesmo que usar uma sequência. Ao usar uma sequência, você não está inserindo ou atualizando nada. Você está simplesmente recuperando o próximo valor da sequência. Parece que o hibernate não suporta.

kammy
fonte
0

Se você tiver uma coluna com o tipo UNIQUEIDENTIFIER e a geração padrão necessária na inserção, mas a coluna não for PK

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

Em db você terá

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

Nesse caso, você não definirá o gerador para um valor que você precisa (será automaticamente graças a columnDefinition="UNIQUEIDENTIFIER"). O mesmo que você pode tentar para outros tipos de coluna

Artyom Novitskii
fonte
0

Eu encontrei uma solução alternativa para isso nos bancos de dados MySql usando @PostConstruct e JdbcTemplate em um aplicativo Spring. Pode ser possível com outros bancos de dados, mas o caso de uso que apresentarei é baseado na minha experiência com o MySql, pois ele usa auto_increment.

Primeiro, tentei definir uma coluna como auto_increment usando a propriedade ColumnDefinition da anotação @Column, mas ela não estava funcionando, pois a coluna precisava ser uma chave para ser incrementada automaticamente, mas aparentemente a coluna não seria definida como um índice até depois de ser definido, causando um impasse.

Aqui é onde eu vim com a idéia de criar a coluna sem a definição de auto_increment e adicioná-la depois a criação do banco de dados. Isso é possível usando a anotação @PostConstruct, que faz com que um método seja chamado logo após o aplicativo inicializar os beans, juntamente com o método de atualização do JdbcTemplate.

O código é o seguinte:

Em Minha Entidade:

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

Em uma 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");
    }
}
Ignacio Velásquez Lagos
fonte
0

Quero fornecer uma alternativa ao lado da solução aceita do @Morten Berg, que funcionou melhor para mim.

Essa abordagem permite definir o campo com o Numbertipo realmente desejado - Longno meu caso de uso - em vez de GeneralSequenceNumber. Isso pode ser útil, por exemplo, para serialização (des) JSON.

A desvantagem é que requer um pouco mais de sobrecarga do banco de dados.


Primeiro, precisamos de um ActualEntityno qual queremos incrementar automaticamente o generatedtipo Long:

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

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

    // ...

}

Em seguida, precisamos de uma entidade auxiliar Generated. Coloquei o pacote private ao lado de ActualEntity, para manter um detalhe de implementação do pacote:

@Entity
class Generated {

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

}

Finalmente, precisamos de um lugar para conectar antes de salvar o arquivo ActualEntity. Lá, criamos e persistimos uma Generatedinstância. Isso fornece uma sequência de banco de dados gerada iddo tipo Long. Utilizamos esse valor escrevendo-o paraActualEntity.generated .

No meu caso de uso, implementei isso usando um Spring Data REST @RepositoryEventHandler, que é chamado logo antes ActualEntityda persistência. Deve demonstrar o princípio:

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

}

Não testei em um aplicativo da vida real; portanto, aproveite com cuidado.

aboger
fonte
-1

Eu estive em uma situação como você (sequência JPA / Hibernate para campo não @Id) e acabei criando um gatilho no meu esquema db que adiciona um número de sequência exclusivo na inserção. Eu nunca consegui trabalhar com o JPA / Hibernate

Frederic Morin
fonte
-1

Depois de passar horas, isso me ajudou a resolver meu problema:

Para Oracle 12c:

ID NUMBER GENERATED as IDENTITY

Para H2:

ID BIGINT GENERATED as auto_increment

Faça também:

@Column(insertable = false)
Primavera
fonte