Hibernate JPA 序列(非 Id)

有没有可能对 不是标识符/不是复合标识符的一部分的某个列使用 DB 序列?

我使用 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 提供程序生成。

只是为了把事情弄清楚: 我希望 冬眠mySequencedValue属性生成值。我知道 Hibernate 可以处理数据库生成的值,但是我不想使用一个触发器或者除了 Hibernate 本身之外的任何东西来为我的属性生成值。如果 Hibernate 可以为主键生成值,为什么它不能为一个简单的属性生成值呢?

160657 次浏览

我曾经遇到过类似的情况(JPA/Hibernate 序列用于 non@Id 字段) ,最后我在 db 模式中创建了一个触发器,它在插入时添加一个唯一的序列号。我只是没能让它和 JPA/Hibernate 一起工作

Hibernate 绝对支持这一点:

”生成的属性是由数据库生成其值的属性。通常,Hibernate 应用程序需要刷新包含数据库为其生成值的任何属性的对象。但是,将属性标记为已生成,可以让应用程序将此责任委托给 Hibernate。基本上,每当 Hibernate 为定义了生成属性的实体发出 SQL INSERT 或 UPDATE 时,它会立即发出一个 select 以检索生成的值。”

对于仅在插入时生成的属性,属性映射(. 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 确实支持它们。

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

在这种情况下,如何创建一个生成所需值的 UserType 实现,并将元数据配置为使用该 UserType 来持久化 mySequenceVal 属性?

我也像你一样在相同的情况下运行,如果基本上可以用 JPA 生成非 id 属性,我也没有找到任何严肃的答案。

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

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

马里奥

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

看起来 Hibernate/JPA 不能自动为您的非 id 属性创建值。@GeneratedValue注释仅与 @Id一起使用,以创建自动编号。

@GeneratedValue注释只是告诉 Hibernate 数据库正在自己生成这个值。

在该论坛中提出的解决方案(或解决方案)是创建一个具有生成的 Id 的单独实体,类似于下面这样:

@Entity
public class GeneralSequenceNumber {
@Id
@GeneratedValue(...)
private Long number;
}


@Entity
public class MyEntity {
@Id ..
private Long id;


@OneToOne(...)
private GeneralSequnceNumber myVal;
}

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

接下来是我是如何做到的:

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

我在会话9.1.9中发现了这个来自 JPA 规范的 GeneratedValue 注释: “[43]便携式应用程序不应该在其他持久字段或属性上使用 GeneratedValue 注释。” 因此,我认为至少不可能使用简单的 JPA 为非主键值自动生成值。

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

在实体上调用 saveAndFlush也是必要的,而且 save不足以填充数据库中的值。

虽然这是一个老线程,我想分享我的解决方案,并希望得到一些反馈。请注意,我只是在某个 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 中出现 ConcurrentModficationException。

希望这对谁有帮助。

我知道这是一个非常古老的问题,但它首先显示在结果和 jpa 已经改变了很多,因为这个问题。

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

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

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

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

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

对于 Oracle 12c:

ID NUMBER GENERATED as IDENTITY

至于 H2:

ID BIGINT GENERATED as auto_increment

此外,请注意:

@Column(insertable = false)

如果您使用 postresgreql
我用的是春靴1.5.6

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

看起来线程已经过时了,我只想在这里添加我的解决方案(在春天使用 AspectJ-AOP)。

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

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

现在您可以在实体中注释任何字段,这样底层字段(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;
}

如果在插入时需要使用 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")。与其他列类型相同

我在一个 Spring 应用程序中使用@PostConstruction 和 JdbcTemplate 在 MySql 数据库上找到了解决这个问题的方法。其他数据库也许可以做到这一点,但是我将介绍的用例是基于我使用 MySql 的经验,因为它使用 auto _  增量。

首先,我尝试使用@Column 注释的 ColumnDefinition 属性将一个列定义为 auto _  增量,但是它不起作用,因为列需要是一個 key 才能自動增量,但是很明显,直到它被定义之后,这个列才被定义为索引,造成了死锁。

在这里,我提出了在不使用 auto _  增量定義的情況下創建列的想法,然後把它添加到 之后中,就可以創建資料库了。使用@PostConstruction 注释可以做到这一点,它会在应用程序初始化 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;
//...
}

在 PostConstruction 组件类中:

@Component
public class PostConstructComponent {
@Autowired
private JdbcTemplate jdbcTemplate;


@PostConstruct
public void makeMyEntityMySequencedValueAutoIncremental() {
jdbcTemplate.update("alter table MyTable modify mySequencedValue int unsigned auto_increment");
}
}

我想在@Morten Berg 公认的解决方案旁边提供一个替代方案,这个方案对我来说更有效。

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

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


首先,我们需要一个 ActualEntity,其中我们要自动增加 Long类型的 generated:

// ...
@Entity
public class ActualEntity {


@Id
// ...
Long id;


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


// ...


}

接下来,我们需要一个助手实体 Generated。我把它的软件包-私有旁边的 ActualEntity,以保持它的软件包的实现细节:

@Entity
class Generated {


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


}

最后,我们需要一个地方挂钩之前,我们保存的 ActualEntity。在那里,我们创建并持久化一个 Generated实例。然后提供一个类型为 Long的数据库序列生成的 id。我们通过将该值写入 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);
}


}

我没有在实际应用程序中测试它,所以请小心享用。

我今天一直在纠结这个问题,用这个就能解决

@Generated(GenerationTime.INSERT)
@Column(name = "internal_id", columnDefinition = "serial", updatable = false)
private int internalId;

你可以完全按照你的要求去做。

我发现可以通过将 Hibernate 的 身份生成器实现注册为 积分器来调整它们。这样,您应该能够使用 Hibernate 提供的任何 id 序列生成器为非 id 字段生成序列(假设非顺序 id 生成器也可以工作)。

以这种方式生成 id 有很多选项。查看 IdentifierGenerator 的一些实现,特别是 SequenceStyleGeneratorTableGenerator。如果您已经使用 @ GenericGenerator注释配置了生成器,那么您可能对这些类的参数比较熟悉。这还有使用 Hibernate 生成 SQL 的优点。

我是这样让它工作的:

import org.hibernate.Session;
import org.hibernate.boot.Metadata;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.enhanced.TableGenerator;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.internal.SessionImpl;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
import org.hibernate.tuple.ValueGenerator;
import org.hibernate.type.LongType;
import java.util.Properties;


public class SequenceIntegrator implements Integrator, ValueGenerator<Long> {
public static final String TABLE_NAME = "SEQUENCE_TABLE";
public static final String VALUE_COLUMN_NAME = "NEXT_VAL";
public static final String SEGMENT_COLUMN_NAME = "SEQUENCE_NAME";
private static SessionFactoryServiceRegistry serviceRegistry;
private static Metadata metadata;
private static IdentifierGenerator defaultGenerator;


@Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactoryImplementor, SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
//assigning metadata and registry to fields for use in a below example
SequenceIntegrator.metadata = metadata;
SequenceIntegrator.serviceRegistry = sessionFactoryServiceRegistry;
SequenceIntegrator.defaultGenerator = getTableGenerator(metadata, sessionFactoryServiceRegistry, "DEFAULT");
}


private TableGenerator getTableGenerator(Metadata metadata, SessionFactoryServiceRegistry sessionFactoryServiceRegistry, String segmentValue) {
TableGenerator generator = new TableGenerator();
Properties properties = new Properties();
properties.setProperty("table_name", TABLE_NAME);
properties.setProperty("value_column_name", VALUE_COLUMN_NAME);
properties.setProperty("segment_column_name", SEGMENT_COLUMN_NAME);
properties.setProperty("segment_value", segmentValue);


//any type should work if the generator supports it
generator.configure(LongType.INSTANCE, properties, sessionFactoryServiceRegistry);


//this should create the table if ddl auto update is enabled and if this function is called inside of the integrate method
generator.registerExportables(metadata.getDatabase());
return generator;
}


@Override
public Long generateValue(Session session, Object o) {
// registering additional generators with getTableGenerator will work here. inserting new sequences can be done dynamically
// example:
// TableGenerator classSpecificGenerator = getTableGenerator(metadata, serviceRegistry, o.getClass().getName());
// return (Long) classSpecificGenerator.generate((SessionImpl)session, o);
return (Long) defaultGenerator.generate((SessionImpl)session, o);
}


@Override
public void disintegrate(SessionFactoryImplementor sessionFactoryImplementor, SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {


}
}

您需要在 META-INF/services 目录中注册这个类。以下是 Hibernate 文档对注册 Integrator 的说明:

为了在 Hibernate 启动时自动使用集成器,需要添加一个 META-INF/services/org.Hibernate.Integrator.spi。集成器文件到你的 jar。该文件应包含实现接口的类的完全限定名。

因为这个类实现了 ValueGenerator类,所以可以将它与 @ GeneratorType注释一起使用来自动生成顺序值。下面是您的类可能的配置方式:

@Entity
@Table(name = "MyTable")
public class MyEntity {


//...
@Id //... etc
public Long getId() {
return id;
}


@GeneratorType(type = SequenceIntegrator.class, when = GenerationTime.INSERT)
@Column(name = "SEQ_VAL", unique = false, nullable = false, insertable = true, updatable = true)
public Long getMySequencedValue(){
return myVal;
}


}
@Column(name = "<column name>", columnDefinition = "serial")

为 mySQL 工作