it-swarm.dev

Urutan JPA Hibernate (non-Id)

Apakah mungkin untuk menggunakan urutan DB untuk beberapa kolom yang bukan pengenal/bukan bagian dari pengidentifikasi komposit

Saya menggunakan hibernate sebagai penyedia jpa, dan saya memiliki tabel yang memiliki beberapa kolom yang menghasilkan nilai (menggunakan urutan), meskipun mereka bukan bagian dari pengidentifikasi.

Yang saya inginkan adalah menggunakan urutan untuk membuat nilai baru untuk suatu entitas, di mana kolom untuk urutan adalahBUKAN(bagian dari) kunci utama:

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

}

Lalu ketika saya melakukan ini:

em.persist(new MyEntity());

id akan dihasilkan, tetapi properti mySequenceVal juga akan dihasilkan oleh penyedia JPA saya.

Hanya untuk memperjelas: Saya ingin Hibernate untuk menghasilkan nilai untuk properti mySequencedValue. Saya tahu Hibernate dapat menangani nilai yang dihasilkan basis data, tetapi saya tidak ingin menggunakan pemicu atau hal lain selain Hibernate sendiri untuk menghasilkan nilai untuk properti saya. Jika Hibernate dapat menghasilkan nilai untuk kunci utama, mengapa tidak bisa menghasilkan untuk properti sederhana?

109
Miguel Ping

Mencari jawaban untuk masalah ini, saya menemukan tautan ini

Tampaknya Hibernate/JPA tidak dapat secara otomatis membuat nilai untuk properti non-id Anda. Anotasi @GeneratedValue hanya digunakan bersama dengan @Id untuk membuat angka-otomatis.

Anotasi @GeneratedValue hanya memberi tahu Hibernate bahwa database menghasilkan nilai ini sendiri.

Solusi (atau penyelesaian) yang disarankan dalam forum itu adalah membuat entitas terpisah dengan ID yang dihasilkan, seperti ini:

 @ Entity 
 Kelas publik GeneralSequenceNumber {
 @ Id 
 @GeneratedValue (...) 
 private Long number; 
} 

 @ Entitas .__ kelas publik MyEntity {
 @Id ..
 private Long id; 

 @OneToOne (...) 
 pribadi GeneralSequnceNumber myVal; 
} 
58
Morten Berg

Saya menemukan bahwa @Column(columnDefinition="serial") berfungsi dengan sempurna tetapi hanya untuk PostgreSQL. Bagi saya ini adalah solusi sempurna, karena entitas kedua adalah opsi "jelek".

32

Saya tahu ini adalah pertanyaan yang sangat lama, tapi itu ditunjukkan pertama pada hasilnya dan jpa telah banyak berubah sejak pertanyaan.

Cara yang tepat untuk melakukannya sekarang adalah dengan penjelasan @Generated. Anda dapat menentukan urutan, mengatur default di kolom ke urutan itu dan kemudian memetakan kolom sebagai:

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

Hibernate pasti mendukung ini. Dari dokumen:

"Properti yang dihasilkan adalah properti yang nilainya dihasilkan oleh basis data. Biasanya, aplikasi Hibernate diperlukan untuk menyegarkan objek yang berisi properti apa pun yang menghasilkan nilai dari basis data. Namun, menandai properti sebagai hasil, membuat aplikasi mendelegasikan tanggung jawab ini ke Hibernate. Pada dasarnya, setiap kali Hibernate mengeluarkan SQL INSERT atau UPDATE untuk entitas yang telah mendefinisikan properti yang dihasilkan, ia segera mengeluarkan pilihan setelahnya untuk mengambil nilai yang dihasilkan. "

Untuk properti yang dihasilkan hanya dengan menyisipkan, pemetaan properti Anda (.hbm.xml) akan terlihat seperti:

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

Untuk properti yang dihasilkan saat memasukkan dan memperbarui pemetaan properti Anda (.hbm.xml) akan terlihat seperti:

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

Sayangnya, saya tidak tahu JPA, jadi saya tidak tahu apakah fitur ini diekspos melalui JPA (saya kira mungkin tidak)

Sebagai alternatif, Anda harus dapat mengecualikan properti dari sisipan dan pembaruan, dan kemudian "secara manual" memanggil sesi.refresh (obj); setelah Anda memasukkan/memperbaruinya untuk memuat nilai yang dihasilkan dari database.

Ini adalah cara Anda mengecualikan properti agar tidak digunakan dalam menyisipkan dan memperbarui pernyataan:

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

Sekali lagi, saya tidak tahu apakah JPA memaparkan fitur-fitur Hibernate ini, tetapi Hibernate mendukungnya.

14
alasdairg

Meskipun ini adalah utas lama saya ingin membagikan solusi saya dan mudah-mudahan mendapatkan umpan balik tentang ini. Berhati-hatilah karena saya hanya menguji solusi ini dengan database lokal saya di beberapa testcase JUnit. Jadi ini bukan fitur yang produktif sejauh ini.

Saya memecahkan masalah itu untuk saya dengan memperkenalkan anotasi khusus yang disebut Sequence tanpa properti. Itu hanya penanda untuk bidang yang harus diberi nilai dari urutan yang meningkat.

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

Dengan menggunakan anotasi ini saya menandai entitas saya.

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

Untuk menjaga hal-hal basis data tetap independen saya memperkenalkan entitas yang disebut SequenceNumber yang menyimpan urutan nilai saat ini dan ukuran kenaikan. Saya memilih className sebagai kunci unik sehingga setiap kelas entitas akan mendapatkan urutannya sendiri.

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

Langkah terakhir dan yang paling sulit adalah PreInsertListener yang menangani tugas nomor urut. Perhatikan bahwa saya menggunakan pegas sebagai wadah kacang.

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

Seperti yang Anda lihat dari kode di atas, pendengar menggunakan satu instance SequenceNumber per kelas entitas dan menyimpan beberapa nomor urut yang ditentukan oleh kenaikan nilai entitas SequenceNumber. Jika kehabisan nomor urut itu memuat entitas SequenceNumber untuk kelas target dan cadangan nilai kenaikan nilai untuk panggilan berikutnya. Dengan cara ini saya tidak perlu query database setiap kali nilai urutan diperlukan . Perhatikan StatelessSession yang sedang dibuka untuk pemesanan set nomor urutan berikutnya. Anda tidak dapat menggunakan sesi yang sama dengan entitas target saat ini bertahan karena ini akan mengarah ke ConcurrentModificationException di EntityPersister.

Semoga ini bisa membantu seseorang.

4
Sebastian Götz

Saya menjalankan dalam situasi yang sama seperti Anda dan saya juga tidak menemukan jawaban serius jika pada dasarnya memungkinkan untuk menghasilkan propertys non-id dengan JPA atau tidak.

Solusi saya adalah memanggil urutan dengan permintaan JPA asli untuk mengatur properti dengan tangan sebelum mengatasinya.

Ini tidak memuaskan tetapi berfungsi sebagai solusi untuk saat ini.

Mario

3
Mario

Saya memperbaiki pembuatan UUID (atau urutan) dengan Hibernate menggunakan @PrePersist penjelasan:

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

Saya telah menemukan catatan spesifik ini di sesi 9.1.9 GeneratedValue Annotation dari spesifikasi JPA: "[43] Aplikasi portabel tidak boleh menggunakan anotasi GeneratedValue pada bidang atau properti persisten lainnya." ... Jadi, saya berasumsi bahwa itu tidak dimungkinkan untuk secara otomatis menghasilkan nilai untuk nilai-nilai non primary key setidaknya menggunakan JPA.

1
Gustavo Orair

Ini tidak sama dengan menggunakan urutan. Saat menggunakan urutan, Anda tidak memasukkan atau memperbarui apa pun. Anda hanya mengambil nilai urutan berikutnya. Sepertinya hibernasi tidak mendukungnya.

0
kammy

"Saya tidak ingin menggunakan pemicu atau hal lain selain dari Hibernate itu sendiri untuk menghasilkan nilai untuk properti saya"

Dalam hal itu, bagaimana dengan membuat implementasi UserType yang menghasilkan nilai yang diperlukan, dan mengkonfigurasi metadata untuk menggunakan UserType itu untuk kegigihan properti mySequenceVal?

0
alasdairg

Sepertinya utas sudah tua, saya hanya ingin menambahkan solusi saya di sini (Menggunakan AspectJ - AOP di musim semi).

Solusi adalah membuat @InjectSequenceValue anotasi khusus sebagai berikut.

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

Sekarang Anda bisa membuat anotasi bidang apa pun di entitas, sehingga nilai bidang yang mendasarinya (Panjang/Integer) akan disuntikkan pada saat runtime menggunakan nilai berikutnya dari urutan.

Beranotasi seperti ini.

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

Sejauh ini kita telah menandai bidang yang kita butuhkan untuk menyuntikkan nilai urutan. Jadi kita akan melihat bagaimana menyuntikkan nilai urutan ke bidang yang ditandai, ini dilakukan dengan membuat titik yang dipotong di AspectJ.

Kami akan memicu injeksi tepat sebelum metode save/persist dijalankan. Ini dilakukan di kelas di bawah ini.

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

Sekarang Anda dapat membubuhi keterangan Entitas seperti di bawah ini.

@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

Jika Anda menggunakan postgresql 
Dan saya menggunakan di spring boot 1.5.6

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