是否可以对某些不是标识符/不是复合标识符一部分的列使用 DB 序列?
我使用休眠作为 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 可以为主键生成值,为什么它不能为一个简单的属性生成值?
在寻找这个问题的答案时,我偶然发现了 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; }
我发现 @Column(columnDefinition="serial")
非常适用,但仅适用于 PostgreSQL。对我来说,这是完美的解决方案,因为第二个实体是“丑陋”的选择。
还需要在实体上调用 saveAndFlush
,并且 save
不足以填充数据库中的值。
columnDefinition=
位基本上告诉 Hiberate 不要尝试生成列定义,而是使用您提供的文本。从本质上讲,您的列的 DDL 就是名称 + columnDefinition。在这种情况下 (PostgreSQL),mycolumn serial
是表中的有效列。
@Column(columnDefinition = "integer auto_increment")
@Column(insertable = false, updatable = false, columnDefinition="serial")
来防止休眠尝试插入空值或更新字段。然后,如果您需要立即使用它,则需要在插入后重新查询数据库以获取生成的 id。
我知道这是一个非常古老的问题,但它首先显示在结果上,并且 jpa 自从这个问题以来发生了很大变化。
现在正确的做法是使用 @Generated
注释。您可以定义序列,将列中的默认值设置为该序列,然后将列映射为:
@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)
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 确实支持它们。
我使用 @PrePersist
注释通过 Hibernate 修复了 UUID(或序列)的生成:
@PrePersist
public void initializeUUID() {
if (uuid == null) {
uuid = UUID.randomUUID().toString();
}
}
作为后续工作,这是我如何让它工作的:
@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();
}
SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
看起来线程很旧,我只是想在此处添加我的解决方案(在春季使用 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;
}
如果您使用的是 postgresql 而我在 spring boot 1.5.6 中使用
@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;
seq_order
并从字段中进行引用,{ 2}
虽然这是一个旧线程,但我想分享我的解决方案,并希望得到一些反馈。请注意,我仅在某些 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。
希望这可以帮助某人。
我在与您相同的情况下运行,如果基本上可以使用 JPA 生成非 id 属性,我也没有找到任何严肃的答案。
我的解决方案是使用本机 JPA 查询调用序列,以便在持久化之前手动设置属性。
这并不令人满意,但它目前可以作为一种解决方法。
马里奥
我在 JPA 规范的会话 9.1.9 GeneratedValue Annotation 中找到了这个特定的注释:“[43] 便携式应用程序不应在其他持久字段或属性上使用 GeneratedValue 注释。”因此,我认为至少仅使用 JPA 无法为非主键值自动生成值。
你可以完全按照你的要求去做。
我发现通过使用 Integrator 注册 Hibernate 的 IdentifierGenerator 实现是可能的。有了这个,您应该能够使用 Hibernate 提供的任何 id 序列生成器来为非 id 字段生成序列(假设非序列 id 生成器也可以工作)。
以这种方式生成 id 有很多选择。查看 IdentifierGenerator 的一些实现,特别是 SequenceStyleGenerator 和 TableGenerator。如果您使用 @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 文档中关于注册集成器的内容:
为了在 Hibernate 启动时自动使用集成器,您需要将 META-INF/services/org.hibernate.integrator.spi.Integrator 文件添加到您的 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;
}
}
我想在@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);
}
}
我没有在实际应用程序中对其进行测试,因此请谨慎使用。
“我不想使用触发器或除 Hibernate 本身之外的任何其他东西来为我的财产生成价值”
在这种情况下,如何创建生成所需值的 UserType 实现,并配置元数据以使用该 UserType 来持久化 mySequenceVal 属性?
这与使用序列不同。使用序列时,您不会插入或更新任何内容。您只是在检索下一个序列值。看起来hibernate不支持它。
如果您有一列具有 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 应用程序中使用 @PostConstruct 和 JdbcTemplate 在 MySql 数据库上找到了解决方法。它可能适用于其他数据库,但我将介绍的用例是基于我对 MySql 的经验,因为它使用 auto_increment。
首先,我尝试使用 @Column 注释的 ColumnDefinition 属性将列定义为 auto_increment,但它不起作用,因为列需要成为键才能自动增量,但显然该列不会被定义为一个索引直到它被定义之后,导致死锁。
在这里,我想到了在没有 auto_increment 定义的情况下创建列,并在创建数据库后添加它。这可以使用@PostConstruct 注释来实现,它会在应用程序初始化bean 之后立即调用一个方法,再加上JdbcTemplate 的更新方法。
代码如下:
在我的实体中:
@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");
}
}
我今天正在为此苦苦挣扎,能够使用它来解决
@Generated(GenerationTime.INSERT)
@Column(name = "internal_id", columnDefinition = "serial", updatable = false)
private int internalId;
我遇到过像你这样的情况(非 @Id 字段的 JPA/Hibernate 序列),我最终在我的数据库模式中创建了一个触发器,在插入时添加了一个唯一的序列号。我只是从来没有让它与 JPA/Hibernate 一起工作
花了几个小时后,这巧妙地帮助我解决了我的问题:
对于 Oracle 12c:
ID NUMBER GENERATED as IDENTITY
对于 H2:
ID BIGINT GENERATED as auto_increment
还制作:
@Column(insertable = false)
不定期副业成功案例分享
MyEntity.id
始终为 null 任何帮助将不胜感激。@GeneratedValue
。请投票加入 2.2 java.net/jira/browse/JPA_SPEC-113