Spring

Spring JPA Notes — Setup, Associations, Saving and NamedQuery

Contents

This collects five short posts from autumn 2015, written while adding JPA to a project for the first time. They were hard to read in order as separate fragments.

The context: almost everything was built with mybatis at the time, but JPA was what everyone recommended, so it went onto a small project. Plenty of material was available, so the setup itself was not hard. Ripping out mybatis all at once was not an option, so the goal was to add JPA without disturbing the existing transactions.

1. Maven setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-jpa</artifactId>
   <version>1.9.0.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-entitymanager</artifactId>
   <version>4.3.8.Final</version>
</dependency>

2. The entity class

@SerializedName and @Expose have nothing to do with JPA. They are Gson annotations, used when rendering the object straight to a JSON view.

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import javax.persistence.*;

@Entity
@Table(name="tb_notice")
public class Notice {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "notice_id")
    @SerializedName(value = "notice_id")
    @Expose
    private Integer noticeId;

    @Column(name="title", nullable = false)
    @Expose
    private String title;

    @Column(name="content", nullable = false)
    @Expose
    private String content;

    @Column(name="reg_date", nullable = false)
    @SerializedName(value = "reg_date")
    @Expose
    private String regDate;

    @Column(name="del_yn", nullable = false)
    @Expose(serialize = false, deserialize = false)
    private String delYn;

    public Integer getNoticeId() {
        return noticeId;
    }

    public void setNoticeId(Integer noticeId) {
        this.noticeId = noticeId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getRegDate() {
        return regDate;
    }

    public void setRegDate(String regDate) {
        this.regDate = regDate;
    }

    public String getDelYn() {
        return delYn;
    }

    public void setDelYn(String delYn) {
        this.delYn = delYn;
    }
}

3. The repository

It can be completely empty. That is part of the point of using JPA.

1
2
3
public interface NoticeRepository extends JpaRepository<Notice, Integer> {

}

4. context-jpa.xml

The transaction manager is named txManager2 so that it does not affect the existing mybatis side. Running both requires keeping them separate like this.

 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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:p="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
    ">

    <!-- Configure the transaction manager bean -->
    <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="txManager2">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <tx:advice id="txAdvice2" transaction-manager="txManager2">
        <tx:attributes>
            <tx:method name="*" rollback-for="Exception" />
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut expression="execution(* sample..service..*.sr*(..))" id="requiredTx2" />
        <aop:advisor advice-ref="txAdvice2" pointcut-ref="requiredTx2" />
    </aop:config>

    <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" id="hibernateJpaVendorAdapter">
        <property name="showSql" value="true" />
    </bean>

    <!-- Configure the entity manager factory bean -->
    <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter" />
        <property name="packagesToScan" value="sample.app" />
    </bean>

    <jpa:repositories base-package="sample.app" transaction-manager-ref="txManager2" />
</beans>

5. Using it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Service
public class NoticeService extends ServiceBase  {
    private static final Logger logger = LoggerFactory.getLogger(NoticeService.class);

    @Autowired
    private NoticeRepository noticeRepository;

    public void srXX(RequestData req, ResponseData res) throws Exception {
        List<Notice> list = noticeRepository.findAll();
        res.put("notice_list", list);
    }
}

That is the whole setup. The harder part is using it well — without a proper understanding it reportedly costs performance.

Fetching associations

The table relationship looks like this.

1
tb_member -< tb_member_inter >- tb_inter

Two things were needed:

  • Fetching a Member should also bring back that member’s images and its inter list
  • The detail for each inter lives in tb_inter, so it has to be joined on read

Things that tripped me up:

  • MemberInter has a two-column PK, so a separate class is needed and declared with @IdClass
  • MemberInter was the most confusing part. @ManyToOne needs @JoinColumn alongside it
  • optional = true produces an outer join; false produces an inner join

FetchType splits like this:

  • FetchType.EAGER — fetch immediately and populate
  • FetchType.LAZY — hit the DB when the field is actually used

Field names must be camelCase. Otherwise method named queries become a problem later.

Member.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity @Table(name = "tb_member")
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer member_seq;

    @Expose
    @OneToMany(
            targetEntity = Image.class
            , cascade = CascadeType.ALL
            , fetch = FetchType.EAGER
            , mappedBy = "member_seq")
    private List<image> imageList;

    @Expose
    @OneToMany(
            targetEntity = MemberInter.class
            , cascade = CascadeType.ALL
            , fetch = FetchType.EAGER
            , mappedBy = "member_seq")
    private List<Memberinter> memberInterList;
}

MemberRepository.java

1
2
3
public interface MemberRepository extends JpaRepository<Member, Integer> {

}

Image.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Entity
@Table(name = "tb_image")
public class Image {
    @Id
    @GeneratedValue
    private Integer image_seq;
    @Column
    private Integer member_seq;
    @Column @Expose
    private String file_name;
}

Inter.java

1
2
3
4
5
6
7
@Entity @Table(name="tb_inter") @Embeddable
public class Inter {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer inter_seq;
    @Column @Expose
    private String inter_name_ko;
}

MemberInter.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Entity @Table(name="tb_member_inter") @IdClass(MemberInterPk.class)
public class MemberInter {
    @Id @Column
    private Integer member_seq;

    @Id @Column(insertable = false, updatable = false)
    private Integer inter_seq;

    @ManyToOne(
            targetEntity = Inter.class
            ,cascade = CascadeType.ALL
            ,fetch = FetchType.EAGER
            ,optional = false
    )
    @JoinColumn(name = "inter_seq")
    @Expose
    private Inter inter;
}

MemberInterPk.java

1
2
3
4
public class MemberInterPk implements Serializable {
    private Integer member_seq;
    private Integer inter_seq;
}

Test code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
        "classpath:servlet-context.xml",
        "classpath:config/context-datasource.xml"
})
public class TestServiceTest {
    private static final Logger logger = LoggerFactory.getLogger(TestServiceTest.class);

    @Autowired
    private Gson gson;

    @Autowired
    private MemberRepository memberRepository;

    @Test
    public void testGetMemberList() throws Exception {
        logger.info("------------ jpa test starting.... ------------------------");

        List<Member> list = memberRepository.findAll();
        logger.info("memberList={}", gson.toJson(list));

        logger.info("------------ jpa test ended....    ------------------------");
    }
}

Saving

Saving goes through repository.save. As with Member above, @OneToMany and @ManyToOne fields can be saved together with the parent.

The classes differ slightly from the read example: field names moved to camelCase, and @JoinColumn got insertable and updatable set to false.

Member.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Entity 
@Table(name = "tb_member")
public class Member {
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "member_seq")
    public Integer memberSeq;

    @Column
    public String nickname;
    
    @Expose
    @OneToMany(
            targetEntity = MemberInter.class
            , cascade = CascadeType.ALL
            , fetch = FetchType.EAGER
            , mappedBy = "memberSeq")
    public List<MemberInter> memberInterList;
}

MemberInter.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity 
@Table(name="tb_member_inter") 
@IdClass(value = MemberInterPk.class)
public class MemberInter {
    @Id 
    @Column(name = "member_seq")
    public Integer memberSeq;
    
    @Id 
    @Column(name = "inter_seq")
    public Integer interSeq;
    
    @ManyToOne(
            targetEntity = Inter.class
            ,cascade = CascadeType.ALL
            ,fetch = FetchType.LAZY
            ,optional = false
    )
    @JoinColumn(name = "inter_seq", referencedColumnName = "inter_seq"
            , insertable = false, updatable = false)
    public Inter inter;
}

The test

 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
@Test
public void testSave() throws Exception{
    logger.info("------------ jpa test starting.... ------------------------");
    
    Member data = memberRepository.findByNickname("111");
    logger.info("data={}", gson.toJson(data));
    
    data.nickname = "222";
    
    MemberInter memberInter = new MemberInter();
    memberInter.interSeq = 28;
    memberInter.memberSeq = data.memberSeq;

    MemberInter memberInter2 = new MemberInter();
    memberInter2.interSeq = 29;
    memberInter2.memberSeq = data.memberSeq;

    data.memberInterList.add(memberInter);
    data.memberInterList.add(memberInter2);
    
    // the actual save (saves the member class)
    memberRepository.save(data);
    
    logger.info("------------ jpa test ended....    ------------------------");
}

Running it produces this SQL:

1
2
3
4
5
...
...
Hibernate: insert into tb_member_inter (inter_seq, member_seq) values (?, ?)
Hibernate: insert into tb_member_inter (inter_seq, member_seq) values (?, ?)
Hibernate: update tb_member set nickname=? where member_seq=?

Two insert into statements and one update. The two entries added to the list get inserted, and the changed nickname gets updated.

@NamedQuery and @NamedNativeQuery

The repository’s findAll and findOneBy... family cover a lot, but sometimes a query has to be written by hand. One way is to declare it in orm.xml.

/classes/META-INF/orm.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 version="2.0">

    <named-query name="Inter.findByAlal2">
        <query>select i from Inter i where i.internameko = ?1</query>
    </named-query>

    <named-native-query name="Inter.findByAlal" result-class="sample.jpa.Inter">
        <query>select a.inter_seq, a.inter_name_ko, a.inter_name_en from tb_inter a where a.inter_name_ko = ?</query>
    </named-native-query>

</entity-mappings>

It can also be declared on the entity class directly.

1
2
3
4
5
6
@Entity @Table(name="tb_inter")
@NamedQuery(name = "User.findByAlal2",
  query = "select i from Inter i where i.internameko = ?1")
public class Inter {
....
}

@Query is a similar option, used on the repository instead.

The difference between the two matters.

  • named-query runs against the entity declared in code
  • named-native-query goes straight to the DB, which is why result-class is required

Inter.java

1
2
3
4
5
6
7
8
9
@Entity @Table(name="tb_inter")
public class Inter {
    @Id @Column(name = "inter_seq") @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer interseq;
    @Column(name = "inter_name_ko") @Expose
    private String internameko;
    @Column(name = "inter_name_en") @Expose
    private String internameen;
}

Calling a NamedQuery directly

EntityManager can run a named query from orm.xml directly.

Going through the repository seems to only ever run getResultList. Getting the number of rows an update touched required this instead:

1
2
3
4
5
6
@PersistenceContext private EntityManager em;

public void test() {
    int cnt = em.createNamedQuery("Order.clearOrder").executeUpdate();
    logger.info("Order.clearOrder updated={}", cnt);
}

@PersistenceContext and @Autowired both seem to work here. I am still not sure what the difference is.

META-INF/orm.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 version="2.0">
    <named-native-query name="Order.clearOrder">
        <query>
update tb_order
   set order_name=null 
     , order_date=null
     , order_no=null
     , order_state='S00'
        </query>
    </named-native-query>
</entity-mappings>

Summary

  • Setup is two libraries plus a single context-jpa.xml
  • Running alongside mybatis means keeping the transaction managers separate
  • Use camelCase field names, or method named queries will block you later
  • Composite keys need a separate class declared with @IdClass
  • @ManyToOne goes together with @JoinColumn; optional decides the join type
  • For hand-written queries, use named-query / named-native-query in orm.xml
  • To get the affected row count of an update, use EntityManager.createNamedQuery().executeUpdate()