SpringSpring JPA Notes — Setup, Associations, Saving and NamedQuery
2015 · 11 · 13
4 min read
Paper
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 @IdClassMemberInter was the most confusing part. @ManyToOne needs @JoinColumn alongside itoptional = true produces an outer join; false produces an inner join
FetchType splits like this:
FetchType.EAGER — fetch immediately and populateFetchType.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.
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 codenamed-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.
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()