Spring JPA

Az előző fejezetben megmutattuk, hogyan lehet használni a Hibernate-et a Session és a SessionFactory segítségével. Egy másik, talán kényelmesebb használati mód, ha a Hibernate-et standard Java Persistent API (JPA)-val használjuk.

JPA használatával azt az előnyt is megkapjuk, hogy a standardizált elemeket egységesen tudjuk használni akármilyen JPA provider-t is alkalmazunk a motorháztető alatt (OpenJPA, EclipseLink, Hibernate, Toplink, stb.).

A sima JPA előnyök mellett a Spring remek support-ot ad a JPA támogatáshoz (Spring Data project).

JPA bevezetés

Először pontosítsuk is, hogy mi az a JPA. A JPA egy specifikáció, melyet standardizálja az ORM technikák használatát mind JSE és JEE környezetben. Definiálja a használható koncepciók, annotációk, interfészek és egyéb szolgáltatások halmazát, melyeket a JPA provider-eknek meg kell kell valósítaniuk. Ezáltal a fejlesztők bármikor szabadon megváltoztathatják a rendszerben a JPA provider-t mindenféle szenvedés nélkül.

JPA-n belül az egyik központi szereplő az EntityManager interface, melyet az EntityManagerFactory-nak kell biztosítania. Az EntityManager feladata perzisztencia kontextus menedzselése, melyen keresztül történik az összes entitás objektum menedzselése is (tehát a DB műveleteket ezen keresztül végezhetjük majd el). Ha megfeleltetést kell tennünk, akkor az EntityManager megfelel a Session interface-nek, az EntityManagerFactory pedig a SessionFactory-nak. A fő különbség, hogy JPA-ban nem tudunk direkt módon interakcióba kerülni a kontextussal, hanem az EntityManager-re bízzuk a nehéz melót.

Az előző fejezetben saját lekérdezéseinkhez a HQL-t (Hibernate Query Language) használtuk, de a JPA ezt is szabványosította, melyet JPQL(Java Persistence Query Language)-nek hívnak.

A JPA 2 nagy újdonsága még az erősen típusos Criteria API, mely lehetővé teszi a fordítási időben történő hiba ellenőrzést.

Jelen pillanatban a JPA 2.2 szabvány a legfrissebb, mely támogatást ad a stream API használatához, a Java 8-as Date és Time típusokhoz és még néhány további hasznos elemet is biztosít.

EntityManagerFactory konfiguráció

Springben 3 lehetséges módja van az EntityManagerFactory konfigurálásának:

  • LocalEntityManagerFactoryBean: a legegyszerűbb megvalósítás, de nem támogatja a DataSource injektálását, így nem tudjuk tranzakciókezelésben sem alkalmazni.
  • JEE kompatibilis konténerekben, melyek bootstrappelik a JPA perzisztenciát biztosító komponeneseket (deployment descriptor-ban). Ilyenkor JNDI lookup-al szerezhetünk referenciát az EntityManager-re. A deployment descriptor hagyományosan a META-INF/persistence.xml volt, de ez Spring 3.1-el megváltozott, pontosabban már nem szükséges így eljárnunk.
  • LocalContainerEntityManagerFactoryBean: támogatja a DataSource injektálást is. Ez a legáltalánosabban használt módszer, melyre rögtön egy példát is mutatunk.

Mielőtt belevágunk a konfigurációba be kell állítanunk a pom.xml-ben a függőségeinket! Amennyiben szeretnénk a Spring Boot-ot JPA-val, illetve annak Hibernate megvalósításával használni, akkor a pom.xml-ben a következő függőséget kell elhelyezni (H2 drivert is megadjuk):

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

A spring-boot-starter-data-jpa automatikusan a Hibernate-et használja, mint JPA provider.

Nézzük is, hogy milyen konfigurációkat kell megtennünk ahhoz, hogy az alkalmazásunk használhassa a Spring JPA adta lehetőségeket. Ehhez tekintsük meg a JpaConfig konfigurációs osztályt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Configuration
@EnableTransactionManagement
@Slf4j
public class JpaConfig {

    @Bean
    public DataSource dataSource() {
        try {
            EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
            return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
        } catch (Exception e) {
            log.error("Embedded DataSource bean cannot be created!", e);
            return null;
        }
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory());
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public Properties hibernateProperties() {
        Properties hibernateProp = new Properties();
        hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        hibernateProp.put("hibernate.format_sql", true);
        hibernateProp.put("hibernate.use_sql_comments", true);
        hibernateProp.put("hibernate.show_sql", true);
        hibernateProp.put("hibernate.max_fetch_depth", 3);
        hibernateProp.put("hibernate.jdbc.batch_size", 10);
        hibernateProp.put("hibernate.jdbc.fetch_size", 50);
        return hibernateProp;
    }

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setPackagesToScan("hu.suaf.model");
        factoryBean.setDataSource(dataSource());
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        factoryBean.setJpaProperties(hibernateProperties());
        factoryBean.afterPropertiesSet();
        return factoryBean.getNativeEntityManagerFactory();
    }
}

Nézzük, hogy mi milyen célt szolgál ebben a konfigurációs állományban! A dataSource szerepét már megismertük. A transactionManager-t később nézzük meg alaposabban, viszont muszáj erre is kitérnünk, hiszen az EntityManagerFactory követel magának egy tranzakció kezelőt. Szerencsére a Spring ad is számunkra egy ilyen tranzakciókezelőt: org.springframework.orm.jpa.JpaTransactionManager. Az entityManagerFactory bean a legfontosabb a fenti bean definíciók közül. Az entityManagerFactory-nak tudnia kell, hogy melyik DataSource-ot kell használnia, hogy melyik vendor-t akarjuk használni (Hibernate-et), hogy hol kell keresni az entity-ket, hogy milyen beállításokat továbbítson a JPA provider (Hibernate) felé.

Megjegyzés

Spring Boot használatakor a fentieket mind el is hagyhatjuk, hacsak nem szeretnénk valamit egyedileg konfigurálni (illetve lehetőség van az application.properties fájlban is néhány beállítást megadni).

JPA használata ORM leképezéshez

Mivel az előző fejezetben az entitás osztályainkat a javax.persistence annotációkkal láttuk el, így nincs további teendőnk ezekkel (alapból JPA kompatibilis). Miután megvagyunk a konfigurációval használhatjuk az EntityManagerFactory-t, hogy egyszerűen injektáljuk oda, ahol szükség van rá. Például az előző fejezetben használt CustomerDaoImpl osztályban használhatjuk a következőképpen:

1
2
@PersistenceContext
private EntityManager em;

A @PersistenceContext annotáció egy standard JPA annotáció az EntityManager injektálására. Alapvetően a default persistence unit-ot fogja használni ez az EntityManager. Egy persistence unit-ot igazából meg lehet feleltetni egy-egy dataSource-nak. Ez akkor jöhet jól, ha az alkalmazásunk egyszerre több adatbázissal is kommunikál. Ilyen esetben a unitName megadásával adhatjuk meg a használni kívánt persistence unit nevét.

Ebben az esetben a findAll megvalósítása:

1
2
3
4
@Transactional(readOnly = true)
public List<Customer> findAll() {
    return em.createNamedQuery("Customer.findAll", Customer.class).getResultList();
}

Ebben az esetben a findAll az EntityManager-t használja, melynek a createNamedQuery metódusát tudjuk meghívni egy nevesített lekérdezés előkészítésére (paramétereket is beállíthatunk rajta), majd a getResultList() segítségével elkérhetjük az eredménylistát.

Fontos

A JPA előírja, hogy az összes fetch-elés EAGER módon történik alapvetően (amikor nincs ez explicit megadva), ugyanakkor a Hibernate még így is a lazy fetching technikát alkalmazza.

Amikor a createNamedQuery-nek megadjuk második paraméterben azt is, hogy milyen típusú eredményt várunk vissza, akkor ő egy TypedQuery<T> típusú objektummal tér vissza, mely segíti a fordítás közbeni típusellenőrzéseket. Egy ilyen TypedQuery<T>-re hívva a getResultList()-et az eredmény nyilván List<T> típusú lesz.

Előfordulhat olyan eset is, amikor szándékosan nem akarjuk lemappelni az eredményt egy entity-re, mivel nem is tudnánk. Például, amikor több táblából gereblyézzük össze egy riporthoz az információkat, akkor annak eredményét nagy valószínűséggel nem fogjuk tudni megfeleltetni egy entitásnak. Ilyenkor az ún. Untyped lekérdezéseket használjuk. Például:

1
2
3
4
5
6
7
List result = em.createQuery("select t1.col1, t2.col1, ...").getResultList();
int count = 0;

for (Iterator i = result.iterator(); i.hasNext(); ) {
    Object[] values = (Object[]) i.next();
    System.out.println(++count + ": " + values[0] + ", " + values[1] + ...);
}

Ilyen esetben a createQuery() egy sima Query objektumot ad vissza. Ezen az eredményen aztán végiglépkedhetünk egy iterátor segítségével, mely minden rekordra egy-egy Object tömböt ad vissza, mely a lekérdezett oszlopokat reprezentálja.

A fenti megoldás nem túl szép, de szerencsére van másik megoldás rá. Kérhetjük, hogy a fenti Object tömb helyett a JPA konstruáljon egy POJO-t. Ezt a POJO-t view-nak hívják, mivel több tábla adatait foglalja magába (hasonlóan, mint amikor az adatbázis view-kat használjuk). Ehhez csinálnunk kell egy POJO osztályt:

1
2
3
4
5
6
7
@Setter
@Getter
@AllArgsConstructor
public class Summary implements Serializable {
    private String name;
    private int numberOfOrders;
}

A reportáláshoz lehet csinálni egy külön service-t:

1
2
3
public interface SummaryService {
    List<Summary> findAll();
}

melyet aztán megvalósíthatunk egy tényleges implementáló osztályban:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Service("SummaryService")
@Repository
@Transactional
public class SummaryServiceImpl implements SummaryService {
    @PersistenceContext
    private EntityManager em;

    @Transactional(readOnly = true)
    @Override
    public List<Summary> findAll() {
        List<Summary> result = em.createQuery(
            "select new hu.suaf.view.Summary(c.name, o.cnt) from Customer c "
            + "left join c.orders o "
            + "where o.cnt=(select count(a2.releaseDate) "
            + "from Orders o2 where o2.customer.id = c.id))",
        Summary.class).getResultList();
        return result;
    }
}

Figyeljük meg a JPQL lekérdezésben a new kulcsszót! Ezt hívják constructor extpression-nek, aminek meg is mondjuk, hogy milyen paraméterekkel kell meghívni a konstruktort, és ez alapján elő is állnak ezek a típusú objektumok. Nyilván egy TypedQuery<Summary> típusú objektumot ad vissza a createQuery ilyen esteben.

CRUD műveletek

JPA segítségével a következőképpen végezhetjük el a mentést:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public Customer save(Customer customer) {

    if (customer.getId() == null) {
        logger.info("Inserting new customer");
        em.persist(customer);
    } else {
        em.merge(customer);
        logger.info("Updating existing customer");
    }

    logger.info("Customer saved with id: " + customer.getId());
    return customer;
}

A különbség csak annyi, hogy itt nekünk kell szétválasztani a mentés operációt új és update esetekre, melyet az id alapján végzünk el.

A törléshez is egyértelmű műveletet kínál az EntityManager, mégpedig a remove() metódust.

Megjegyzés

Törlés előtt érdemes lehet egy merge() hívást megejteni, hogy az asszociációkat updateljük még mielőtt kitöröljük az entitást. Nyilván, ha csak kaszkádolt törlések vannak megadva mindenhol, akkor ez a része nem feltétlenül lehet szükséges.

Natív lekérdezések

Amikor valamilyen vendor-specifikus (nem támogatott csak egy adott DB által) lekérdezést szeretnénk használni, akkor nincs más lehetőségünk, mint egy natív lekérdezést használni, melyet a JPA szimplán továbbpasszol az adatbázis szervernek úgy ahogy van (mapping és transzformáció nélkül) és a többit majd intézi maga az adatbázis. Ettől függetlenül magát az eredményt (ResultSet) vissza tudjuk mappelni az entity-khez. Példa:

1
2
3
4
5
6
7
8
final static String ALL_CUSTOMER_NATIVE_QUERY =
"select id, name, birth_date, version from customer";

@Transactional(readOnly=true)
@Override
public List<Customer> findAllByNativeQuery() {
    return em.createNativeQuery(ALL_CUSTOMER_NATIVE_QUERY, Customer.class).getResultList();
}

A fentiek mellett lehetőség van arra is, hogy az SQL ResultSet mapping-et megadjuk, melyet az entity osztályon kell jeleznünk. Egy egyszerű példa:

1
2
3
4
@SqlResultSetMapping(
    name="customerResult",
    entities=@EntityResult(entityClass=Customer.class)
)

A JPA támogatja több entity használatát és az oszlop-szintű mapping-et. Ezután ezt az SqlResultSetMapping-et a következőképpen használhatjuk fel:

1
2
3
4
5
6
7
8
9
...

@Transactional(readOnly=true)
@Override
public List<Customer> findAllByNativeQuery() {
    return em.createNativeQuery(ALL_CUSTOMER_NATIVE_QUERY, "customerResult").getResultList();
}

...

Criteria API

A Criteria API akkor jön nagyon jól, amikor kereséseket szeretnénk végezni valamilyen field-ek alapján. Ahelyett, hogy rengeteg kombinációt adunk meg (keresés csak név alapján, csak id alapján, csak létrehozás dátuma alapján, stb.), használhatjuk a Criteria API-t, mellyel ez sokkal szebben megoldható.

A JPA a 2.0-ás verziótól támogatja az erősen típusos lekérdezéseket a Criteria API használatával, melyet a Metamodel API segítségével tudunk megvalósítani. A lekérdezéseket az entity osztály meta-modelljén fogalmazhatjuk meg, melyet a class neve mögé fűzött _ (underscore)-ral kaphatjuk meg.

Példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Customer.class)
public abstract class Customer_ {

    public static volatile SingularAttribute<Customer, String> name;
    public static volatile SetAttribute<Customer, Order> orders;
    public static volatile SetAttribute<Customer, Group> groups;
    public static volatile SingularAttribute<Customer, Long> id;
    public static volatile SingularAttribute<Customer, Integer> version;
    public static volatile SingularAttribute<Customer, Date> birthDate;
}

Az osztály a @StaticMetamodel annotációval kell ellátnunk, melynek attribútumában megadjuk a mappelt entitást. Az osztályon belül az összes kereshető attribútumhoz megadjuk a megfelelő keresés lehetőséget, melyeken a típust is előírjuk (mindegyik használt attribútumnak Attribute típusúnak kell lennie). Ennek az egész Metamodel API-nak a lényege az, hogy a keresési feltételeinket ne stringekkel adjuk meg, hanem azokat típusosan tudjuk kezelni. Ez a fenti metamodell viszont nem igazán tűnik jónak, mert ezt is karban kell tartani, ugyanakkor nagyon egyszerű szerkezetű, ami adja, hogy léteznek hozzá generátorok, mint például a hibernate jpamodelgen. Ehhez a következőt kell a pom.xml-be írnunk:

1
2
3
4
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
</dependency>

A generátor a target/generated-sources alá helyezi el a legenerált metamodelleket.

Fontos

Mivel a generált állományok a target/generated-sources alá kerülnek be, így ezt hozzá kell adnunk a classpath-hoz, máskülönben nem fogja fejlesztés közben feloldani az IDE a hivatkozásokat.

Miután ezzel megvagyunk, definiáljunk egy List<Customer> findByCriteriaQuery(String name, Date before) alakú lekérdezést, ahol a vásárló nevének pontosan egyeznie kell a megadott name-mel, illetve a születésnapja a megadott dátum előtt van!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Transactional(readOnly=true)
public List<Customer> findByCriteriaQuery(String name, Date before){
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Customer> criteriaQuery = cb.createQuery(Customer.class);
    Root<Customer> customerRoot = criteriaQuery.from(Customer.class);
    customerRoot.fetch(Customer_.orders, JoinType.LEFT);
    customerRoot.fetch(Customer_.groups, JoinType.LEFT);

    criteriaQuery.select(customerRoot).distinct(true);

    Predicate criteria = cb.conjunction();
    if(name != null){
        Predicate p = cb.equal(customerRoot.get(Customer_.name), name);
        criteria = cb.and(criteria, p);
    }

    if(before != null){
        Predicate p = cb.lessThanOrEqualTo(customerRoot.get(Customer_.birthDate), before);
        criteria = cb.and(criteria, p);
    }

    criteriaQuery.where(criteria);
    return em.createQuery(criteriaQuery).getResultList();
}

Vegyük sorra, hogy mit tettünk a fenti lekérdezésben:

  • Először is referenciát kell szereznünk a CriteriaBuilder-re, melyet az EntityManager-től kérhetünk el.
  • A createQuery() visszaad egy CriteriaQuery<Customer> objektumot, azaz erősen típusos lekérdezést
  • A lekérdezés gyökér objektumát a Customer entitásra adjuk meg, azaz az olyan feltételek, melyekben útvonal kifejezések szerepelnek, innen fognak indulni. Ez mindig valamilyen entitás kell legyen
  • a Root<T>.fetch hívások az asszociációk menti EAGER lekérdezéseket adják meg.
  • A CriteriaQuery.select() lényegében megmondja, hogy melyik táblából kell majd a select-et véghezvinnünk, illetve az eredmény DISTINCT legyen (duplikált elemek szűrése).
  • Egy Predicate példányt kapunk, ha a CriteriaBuilder.conjunction() metódust meghívjuk, ami egy vagy több feltétel együttes teljesülését várja el. A feltételeket mind egy-egy Predicate hivatott megadni.
  • Ezután rendre megnézzük, hogy kell-e további feltételeket (Predicate) hozzáadni a keresési kritériumokhoz
  • A keresési feltételeket a criteriaQuery where metódusában adhatjuk át.
  • Végül lefuttatjuk a lekérdezést és visszaadjuk annak eredményét

Amennyiben a customer nevének nem kell teljes egyezést adnia, akkor használhatjuk a cb.like(customerRoot.get(Customer_.name), name); alternatívát is.

Repository

Az eddigiek pusztán JPA specifikus megoldások voltak. A Spring Data segítségével használhatjuk az igen erős Repository absztrakciót, mely tovább egyszerűsíti az adatbázis műveleteket. Ez az absztrakció magában foglalja a JPA EntityManager-ét is. A központi interfész, melyet használhatunk a org.springframework.data.repository.Repository<T,ID extends Serializable>. Ehhez az alap interface-hez számos bővítés tartozik, melyek közül az egy a CrudRepository interface.

A CrudRepository a következő műveleteket adja (forráskódból kiemelt):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);
    Optional<T> findById(ID id);
    boolean existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> ids);
    long count();
    void deleteById(ID id);
    void delete(T entity);
    void deleteAll(Iterable<? extends T> entities);
    void deleteAll();
}

Ezeket a metódusokat alapból használhatjuk, viszont amennyiben egyéb lekérdezésre van szükségünk úgy további metódusokat adhatunk az interfészhez:

1
2
3
4
public interface CustomerRepository extends CrudRepository<Customer, Long> {
    List<Customer> findByName(String name);
    List<Customer> findByNameIsLikeAndAndBirthDateBefore(String name, Date before);
}

A csodás dolog az inteface-hez hozzáadott metódusokban az, hogy ha néhány névkonvenciót követünk, akkor semmilyen lekérdezést nem kell megadnunk. A Spring Data JPA implementációja ki tudja találni magát a lekérdezést a metódus neve alapján, illetve ismeri azt is, hogy egy CrudRepository<Customer, Long>-val van dolga, azaz tudja, hogy a Customer táblára vonatkoznak a megadott lekérdezések. A findByName metódusból például a következő lekérdezést rakja össze a rendszer: select c from Customer c where c.name = :name, továbbá be is állítja a paramétert a megadott paraméterre, melynek neve megegyezik a nevesített paraméter nevével.

A CrudRepository-nál létezik egy fejletteb interface, melyet JpaRepository-nak hívnak és az alapműveletek mellett támogatja a batch, paginálás és rendezés műveleteket is.

Láttuk, hogy a megadott metódusokból a rendszer kitalálja a lekérdezést magát. Vannak helyzetek, amikor viszont ennyi nem elég és mégis valami egyedi lekérdezést szeretnénk írni. Ilyen esetben magát a lekérdezést a @Query annotáció használatával adhatjuk meg. Például a vásárló név alapú keresését így is megadhatom:

1
2
@Query("select c from Customer c where c.name like %:name%")
List<Customer> findByNameLike(@Param("name") String n);

Abban az esetben, ha a nevesített paraméter neve megegyezik a metódus paraméterének nevével akkor nincs szükség a @Param használatára.

Változások követése az Entity osztályon

Egy alkalmazásban általában követnünk kell bizonyos tevékenységeket, melyek az entitásokat érintik, mint például létrehozás dátuma, utolsó módosítás dátuma, ki módosította utoljára, stb. Ezek támogatására a Spring Data szolgáltatja számunkra JPA entity listener-eket, melyek segítségével automatizálhatjuk ezen tevékenységeket is. Spring 4 és azelőtt elég sokat kellett ehhez dolgozni, mivel implementálni kellett az Auditable<U, ID extends Serializable, T extends TemporalAccessor> extends Persistable<ID> interfészt, melynek 8 metódusát is ki kellett fejteni. Spring 5-ben szerencsére megváltozott ez és mindent tudunk annotációval szabályozni:

  • @CreatedBy
  • @CreatedDate
  • @LastModifiedBy
  • @LastModifiedDate

Az Entity-re rá kell aggatnunk az @EntityListeners(AuditingEntityListener.class) annotációt, hogy támogassuk erre az entitásra a fenti műveleteket. Olyan esetben, ha több osztályban is szeretnénk alkalmazni a fenti auditálásokat, akkor érdemes egy külön osztályba ezt kiszervezni, melyből aztán a többi entity származik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity<U> implements Serializable {

    @CreatedDate
    @Column(name = "CREATED_DATE")
    @Temporal(TemporalType.TIMESTAMP)
    protected Date createdDate;

    @CreatedBy
    @Column(name = "CREATED_BY")
    protected String createdBy;

    @LastModifiedBy
    @Column(name = "LAST_MODIFIED_BY")
    protected String lastModifiedBy;

    @LastModifiedDate
    @Column(name = "LAST_MODIFIED_DATE")
    @Temporal(TemporalType.TIMESTAMP)
    protected Date lastModifiedDate;
}

A @MappedSuperclass-ra azért van szükség, hogy az itt megadott fieldek is szerves részét képezzék a leszármazott osztály adattagjainak és a mapping során ezek a field-ek is leképződjenek a tábla megfelelő oszlopaira.


Utolsó frissítés: 2020-09-30 08:40:19