休眠JPA序列(非ID)


138

是否可以对不是标识符/不属于复合标识符的某些列使用数据库序列

我正在使用hibernate作为jpa提供程序,并且我有一个表,其中包含一些生成的值(使用序列)的列,尽管它们不是标识符的一部分。

我想要的是使用序列为实体创建新值,其中该序列的列不是主键(的一部分):

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

}

然后,当我这样做时:

em.persist(new MyEntity());

将会生成ID,但是 mySequenceVal属性也将由我的JPA提供程序生成。

为了清楚起见:我希望Hibernate生成该mySequencedValue属性的值。我知道Hibernate可以处理数据库生成的值,但是我不想使用触发器或Hibernate本身以外的其他任何东西来为我的属性生成值。如果Hibernate可以为主键生成值,为什么它不能为简单属性生成?

Answers:


76

寻找这个问题的答案,我偶然发现了这个链接

似乎Hibernate / JPA无法自动为您的非id属性创建一个值。该@GeneratedValue注释只有配合使用@Id,以创建自动编号。

@GeneratedValue注释只是告诉Hibernate数据库已生成该值本身。

该论坛中建议的解决方案(或解决方法)是使用生成的ID创建一个单独的实体,如下所示:

@实体
公共类GeneralSequenceNumber {
  @ID
  @GeneratedValue(...)
  私人长号;
}

@实体 
公共类MyEntity {
  @ID ..
  私人Long ID;

  @OneToOne(...)
  私人GeneralSequnceNumber myVal;
}

来自@GeneratedValue的Java文档:“ GeneratedValue批注可以与Id批注一起应用于实体或映射超类的主键属性或字段”
Kariem,2009年

11
我发现@Column(columnDefinition =“ serial”)完美,但仅适用于PostgreSQL。对我来说,这是完美的解决方案,因为第二个实体是“丑陋”的选择
谢尔盖·维德尼科夫

@SergeyVedernikov 非常有帮助。您介意将其作为单独的答案发布吗?它非常简单有效地解决了我的问题。
Matt Ball

@MattBall我已将其发布为单独的答案:) stackoverflow.com/a/10647933/620858
Sergey Vedernikov 2012年

1
我打开了一个建议,允许@GeneratedValue在非id的字段上使用。请投票将其列入2.2 java.net/jira/browse/JPA_SPEC-113
Petar Tahchiev '16

44

我发现这很@Column(columnDefinition="serial")完美,但仅适用于PostgreSQL。对我来说,这是完美的解决方案,因为第二个实体是“丑陋”的选择。


嗨,我需要一个解释。你能告诉我更多吗?
Emaborsa 2014年

2
@Emaborsa该columnDefinition=位基本上告诉Hiberate 不要尝试生成列定义,而是使用您提供的文本。本质上,您的列的DDL实际上就是名称+ columnDefinition。在这种情况下(PostgreSQL)mycolumn serial是表中的有效列。
帕特里克

7
MySQL的等效项是@Column(columnDefinition = "integer auto_increment")
Richard Kennard

2
这会自动产生价值吗?我尝试使用这样的字段定义持久化一个实体,但是它没有生成值。它在列<column>中抛出了一个空值,违反了非空约束
KyelJmD

7
我曾经@Column(insertable = false, updatable = false, columnDefinition="serial")防止冬眠尝试插入空值或更新字段。然后,如果需要立即使用数据库,则需要在插入后重新查询数据库以获取生成的ID。
罗伯特·迪保罗

20

我知道这是一个非常老的问题,但首先显示在结果上,自问题以来jpa发生了很大变化。

现在正确的方法是使用@Generated注释。您可以定义序列,将列中的默认值设置为该序列,然后将列映射为:

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

1
这仍然需要该值由数据库生成,这并不能真正回答问题。对于12c之前的Oracle数据库,您仍然需要编写数据库触发器以生成该值。
伯尼2014年

9
同样,这是一个Hibernate注释,而不是JPA。
caarlos0 2014年

14

Hibernate绝对支持这一点。从文档:

“生成的属性是具有其值由数据库生成的属性。通常,Hibernate应用程序需要刷新对象,这些对象包含数据库正在为其生成值的任何属性。但是,将属性标记为已生成,则使应用程序可以将此责任委托给Hibernate。本质上,每当Hibernate对已定义了生成属性的实体发出SQL INSERT或UPDATE时,它就会立即发出选择以检索生成的值。”

对于仅在插入时生成的属性,您的属性映射(.hbm.xml)如下所示:

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

对于在插入和更新时生成的属性,属性映射(.hbm.xml)如下所示:

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

不幸的是,我不了解JPA,所以我不知道此功能是否通过JPA公开(我怀疑可能不是)

另外,您应该能够从插入和更新中排除该属性,然后“手动”调用session.refresh(obj);。在您插入/更新它以从数据库加载生成的值之后。

这样可以避免在插入和更新语句中使用该属性:

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

同样,我不知道JPA是否公开了这些Hibernate功能,但是Hibernate确实支持它们。


1
@Generated批注对应于上述XML配置。有关更多详细信息,请参见休眠文档的本部分
埃里克

8

作为后续步骤,以下是我的工作方式:

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

使用Hibernate 4.2.19和oracle的变体: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
亚伦

6

我使用@PrePersist注释通过Hibernate修复了UUID(或序列)的生成:

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

5

尽管这是一个老话题,但我还是想分享我的解决方案,并希望能得到一些反馈。请注意,我仅在某些JUnit测试用例中使用本地数据库测试了此解决方案。因此,到目前为止,这还不是生产性功能。

我通过引入一个没有属性的自定义注释(称为“序列”)为我解决了这个问题。它只是应该为递增序列中的值分配字段的标记。

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

使用此注释,我标记了我的实体。

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

为了使数据库保持独立,我引入了一个名为SequenceNumber的实体,该实体保存序列的当前值和增量大小。我选择了className作为唯一键,因此每个实体类将获得自己的序列。

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

最后一步也是最困难的是处理序列号分配的PreInsertListener。请注意,我使用spring作为bean容器。

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

从上面的代码中可以看到,侦听器为每个实体类使用了一个SequenceNumber实例,并保留了几个由SequenceNumber实体的递增值定义的序列号。如果序列号用完了,它将为目标类加载SequenceNumber实体,并为下一次调用保留增量值。这样,我不需要每次需要序列值时都查询数据库。请注意,正在打开以保留下一组序列号的StatelessSession。您不能使用目标实体当前存在的同一会话,因为这会导致EntityPersister中的ConcurrentModificationException。

希望这对某人有帮助。


5

如果您使用的是Postgresql
而我在Spring Boot 1.5.6中使用

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

1
它也对我有用,我使用的是Spring Boot 2.1.6.RELEASE,Hibernate 5.3.10.Final,除了已经指出的内容外,我还必须创建一个安全性seq_order并从该字段中获得引用, nextval('seq_order'::regclass)
OJVM

3

我在与您相同的情况下运行,如果基本上可以通过JPA生成非id属性,我也没有找到任何认真的答案。

我的解决方案是使用本地JPA查询调用序列,以在持久化属性之前手动设置属性。

这并不令人满意,但目前可以作为解决方法。

马里奥


2

我在JPA规范的会话9.1.9 GeneratedValue注释中找到了此特定注释:“ [43]可移植应用程序不应在其他持久字段或属性上使用GeneratedValue注释。” 因此,我认为至少使用JPA无法为非主键值自动生成值。


1

看起来线程很旧,我只想在这里添加解决方案(在春季使用AspectJ-AOP)。

解决方案是@InjectSequenceValue按如下方法创建自定义注释。

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

现在,您可以注释实体中的任何字段,以便在运行时使用序列的nextvalue注入基础字段(Long / Integer)值。

这样注释。

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

到目前为止,我们已经标记了需要注入序列值的字段,因此我们将研究如何将序列值注入到标记的字段中,这是通过在AspectJ中创建切入点来完成的。

我们将save/persist在执行该方法之前触发注入,这在下面的类中完成。

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

现在,您可以如下注释任何实体。

@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

“除了Hibernate本身,我不想使用触发器或其他任何东西来为我的财产产生价值”

在那种情况下,如何创建生成所需值的UserType实现,并配置元数据以使用该UserType来保留mySequenceVal属性?


0

这与使用序列不同。使用序列时,您不会插入或更新任何内容。您只是在检索下一个序列值。看来冬眠不支持它。


0

如果您的列具有UNIQUEIDENTIFIER类型,并且插入时需要默认生成,但列不是PK

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

在数据库中,您将拥有

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

在这种情况下,您将不会为所需的值定义生成器(这将自动归功于 columnDefinition="UNIQUEIDENTIFIER")。您可以尝试其他列类型的方法


0

我已经在Spring应用程序中使用@PostConstruct和JdbcTemplate在MySql数据库上找到了解决方法。它可能对其他数据库可行,但是我将展示的用例基于我对MySql的经验,因为它使用了auto_increment。

首先,我尝试使用@Column批注的ColumnDefinition属性将一列定义为auto_increment,但由于该列需要作为键才能自动递增,因此无法正常工作,但显然该列未定义为直到定义索引为止的索引,从而导致死锁。

在这里,我想到了创建没有auto_increment定义的列并将其添加的想法。 在之后在创建数据库。使用@PostConstruct批注可以做到这一点,该批注导致在应用程序初始化bean之后立即调用一个方法,再加上JdbcTemplate的update方法。

代码如下:

在我的实体中:

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

在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

我想在@Morten Berg接受的解决方案旁边提供一个替代方案,它对我来说更好。

这种方法允许使用实际需要的Number类型(Long在我的用例中)定义字段GeneralSequenceNumber。这可能很有用,例如对于JSON(反序列化)。

缺点是它需要更多的数据库开销。


首先,我们需要一个ActualEntity要自动增加generatedtype的类型Long

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

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

    // ...

}

接下来,我们需要一个助手实体Generated。我将其package-private放在旁边ActualEntity,以使其保持该包的实现细节:

@Entity
class Generated {

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

}

最后,在保存之前,我们需要一个可以挂接的位置ActualEntity。在那里,我们创建并保留一个Generated实例。然后,这提供了一个id类型为的数据库序列Long。我们通过将其写入ActualEntity.generated

在我的用例中,我使用Spring Data REST实现了此功能@RepositoryEventHandler,在ActualEntity获取持久化之前立即调用该get。它应证明该原则:

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

}

我没有在实际应用中对其进行测试,因此请谨慎使用。


-1

我遇到过像您这样的情况(非@Id字段的JPA /休眠序列),最终在数据库模式中创建了一个触发器,该触发器在插入时添加了唯一的序列号。我只是从未与JPA / Hibernate一起使用


-1

花了几个小时后,这巧妙地帮助我解决了我的问题:

对于Oracle 12c:

ID NUMBER GENERATED as IDENTITY

对于H2:

ID BIGINT GENERATED as auto_increment

还可以:

@Column(insertable = false)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.