是否可以对某些列使用数据库序列不是标识符/不是复合标识符的一部分?
我正在使用hibernate作为jpa提供程序,我有一个表有一些生成值的列(使用序列),尽管它们不是标识符的一部分。
我想要的是使用序列为实体创建新值,其中序列的列是 NOT (部分)主键:
@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,但我的JPA提供程序也会生成mySequenceVal
属性。
只是为了说清楚:我想要 Hibernate 来生成mySequencedValue
属性的值。我知道Hibernate可以处理数据库生成的值,但是我不想使用触发器或除Hibernate之外的任何其他东西来为我的属性生成值。如果Hibernate可以为主键生成值,为什么不能为简单属性生成?
答案 0 :(得分:61)
寻找这个问题的答案,我偶然发现了this link
似乎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; }
答案 1 :(得分:33)
我发现@Column(columnDefinition="serial")
完美但只适用于PostgreSQL。对我来说这是完美的解决方案,因为第二个实体是“丑陋”的选择。
答案 2 :(得分:16)
我知道这是一个非常古老的问题,但它首先显示了结果,jpa自问题以来发生了很大的变化。
现在正确的方法是使用@Generated
注释。您可以定义序列,将列中的默认值设置为该序列,然后将列映射为:
@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)
答案 3 :(得分:14)
Hibernate绝对支持这一点。来自文档:
“生成的属性是由数据库生成其值的属性。通常,Hibernate应用程序需要刷新包含数据库生成值的任何属性的对象。但是,将属性标记为生成,可让应用程序委派此责任Hibernate。本质上,每当Hibernate为已定义生成属性的实体发出SQL INSERT或UPDATE时,它会立即发出一个select来检索生成的值。“
对于仅在insert上生成的属性,属性映射(.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确实支持它们。
答案 4 :(得分:7)
作为后续跟进,我的工作方式如下:
@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();
}
答案 5 :(得分:4)
虽然这是一个老线程,但我想分享我的解决方案并希望得到一些反馈。请注意,我只在一些JUnit测试用例中使用我的本地数据库测试了此解决方案。所以到目前为止,这不是一个富有成效的功能。
我通过引入名为Sequence且没有属性的自定义注释解决了我的问题。它只是字段的标记,应该从递增的序列中赋值。
@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实体的incrementValue定义的几个序列号。如果序列号用完,则为目标类加载SequenceNumber实体,并为下次调用保留incrementValue值。这样,每次需要序列值时,我都不需要查询数据库。 请注意正在打开的StatelessSession,用于保留下一组序列号。您不能使用目标实体当前持久化的相同会话,因为这会导致EntityPersister中出现ConcurrentModificationException。
希望这有助于某人。
答案 6 :(得分:3)
我遇到和你一样的情况,如果基本上可以用JPA生成非id属性,我也没有找到任何严肃的答案。
我的解决方案是使用本机JPA查询调用序列,以便在持久化之前手动设置属性。
这并不令人满意,但目前它可以作为一种解决方法。
马里奥
答案 7 :(得分:3)
我使用@PrePersist
注释使用Hibernate修复了UUID(或序列)的生成:
@PrePersist
public void initializeUUID() {
if (uuid == null) {
uuid = UUID.randomUUID().toString();
}
}
答案 8 :(得分:1)
我在JPA规范的会话9.1.9 GeneratedValue Annotation中找到了这个特定的注释: “[43]便携式应用程序不应在其他持久字段或属性上使用GeneratedValue注释。” 因此,我认为至少只使用JPA就不可能为非主键值自动生成值。
答案 9 :(得分:1)
如果您使用的是Postgresql
我在春季靴1.5.6中使用
@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;
答案 10 :(得分:1)
除了@Morten Berg接受的解决方案之外,我想提供一个替代方案,对我来说效果更好。
此方法允许使用实际需要的Number
类型(在我的用例中为Long
)而不是GeneralSequenceNumber
来定义字段。这可能是有用的,例如用于JSON(反序列化)。
缺点是它需要更多的数据库开销。
首先,我们需要一个ActualEntity
,其中要自动递增类型generated
的{{1}}:
Long
接下来,我们需要一个辅助实体// ...
@Entity
public class ActualEntity {
@Id
// ...
Long id;
@Column(unique = true, updatable = false, nullable = false)
Long generated;
// ...
}
。我将其package-private放在Generated
旁边,以使其保持该软件包的实现细节:
ActualEntity
最后,在保存@Entity
class Generated {
@Id
@GeneratedValue(strategy = SEQUENCE, generator = "seq")
@SequenceGenerator(name = "seq", initialValue = 1, allocationSize = 1)
Long id;
}
之前,我们需要一个可以挂入的位置。在那里,我们创建并保留一个ActualEntity
实例。然后,这提供了类型为Generated
的数据库序列生成的id
。我们通过将此值写入Long
来使用它。
在我的用例中,我使用Spring Data REST ActualEntity.generated
实现了此功能,在持久保存@RepositoryEventHandler
之前先调用该get。它应说明该原理:
ActualEntity
我没有在真实的应用程序中对其进行测试,因此请谨慎使用。
答案 11 :(得分:0)
这与使用序列不同。使用序列时,您不会插入或更新任何内容。您只是检索下一个序列值。看起来hibernate不支持它。
答案 12 :(得分:0)
“我不想使用触发器或除Hibernate之外的任何其他东西来为我的属性生成值”
在这种情况下,如何创建生成所需值的UserType实现,以及如何配置元数据以使用该UserType来持久保存mySequenceVal属性?
答案 13 :(得分:0)
如果您的列具有UNIQUEIDENTIFIER类型,并且插入时需要默认生成,但列不是PK
@Generated(GenerationTime.INSERT)
@Column(nullable = false , columnDefinition="UNIQUEIDENTIFIER")
private String uuidValue;
在db中,您将
CREATE TABLE operation.Table1
(
Id INT IDENTITY (1,1) NOT NULL,
UuidValue UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL)
在这种情况下,您将不会为所需的值定义生成器(它将自动归功于columnDefinition="UNIQUEIDENTIFIER"
)。您可以尝试其他列类型的方法
答案 14 :(得分: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");
}
}
答案 15 :(得分:0)
我今天为此苦苦挣扎,能够使用它解决
@Generated(GenerationTime.INSERT)
@Column(name = "internal_id", columnDefinition = "serial", updatable = false)
private int internalId;
答案 16 :(得分:-1)
我遇到过像你这样的情况(非@Id字段的JPA / Hibernate序列)我最终在我的数据库模式中创建了一个触发器,它在插入时添加了一个唯一的序列号。我从来没有使用它来使用JPA / Hibernate
答案 17 :(得分:-1)
花了好几个小时后,这整齐地帮助我解决了我的问题:
对于Oracle 12c:
ID NUMBER GENERATED as IDENTITY
对于H2:
ID BIGINT GENERATED as auto_increment
同时制作:
@Column(insertable = false)