Spring Spring JPA 연습 기록 — 설정부터 저장·NamedQuery 까지 2015 · 11 · 13
5 min read
페이퍼
목차 2015년 가을에 JPA 를 처음 붙여보면서 다섯 번에 나눠 올렸던 글을 한 페이지로 합쳤다.
따로 보면 토막글이라 순서대로 읽기가 불편했다.
그때 상황은 이랬다. 거의 다 mybatis 로 개발하고 있었는데 JPA 가 대세라길래
가벼운 프로젝트에 얹어봤다. 자료가 많아서 설정 자체는 어렵지 않았다.
mybatis 를 한번에 다 걷어낼 자신은 없었고 기존 트랜잭션에 영향을 주지 않는
선에서 얹는게 목표였다.
1. maven 설정 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. Entity 클래스 @SerializedName, @Expose 는 JPA 와 직접 관련이 없다.
객체를 그대로 JsonView 로 뿌릴 때 쓰는 Gson 쪽 어노테이션이다.
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. Repository 내용이 하나도 없어도 된다. 이게 JPA 를 쓰는 이유이기도 하다.
1
2
3
public interface NoticeRepository extends JpaRepository < Notice , Integer > {
}
4. context-jpa.xml 트랜잭션 매니저 이름이 txManager2 인 이유는 기존 mybatis 쪽에 영향을 주지 않기 위해서다.
둘을 같이 쓰려면 이렇게 분리해야 한다.
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. 여기까지의 사용 예제 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 );
}
}
설정은 여기까지가 끝이다. 문제는 실제로 쓸 때 요령이 필요하다는 것.
제대로 이해하지 않고 쓰면 성능에 영향을 준다고 한다.
연관관계 조회 테이블 관계가 이런 상황이다.
1
tb_member -< tb_member_inter >- tb_inter
원하는 조회 조건은 두 가지였다.
Member 를 가져오면 member 의 이미지들과 inter 목록을 함께 가져오도록 inter 의 상세 정보는 tb_inter 에 있으니 가져올 때 조인해서 하면서 걸렸던 것들.
MemberInter 의 PK 가 두 개라 클래스를 따로 만들어서 @IdClass 를 지정해야 한다MemberInter 에서 제일 헷갈렸다. @ManyToOne 을 쓸 때 @JoinColumn 을 추가로 써야 한다optional 을 true 로 하면 조인할 때 outer join 을 한다. false 는 inner joinFetchType 은 이렇게 갈린다.
FetchType.EAGER — 즉시 조회해서 데이터를 채운다FetchType.LAZY — 필요할 때 DB 조회필드명은 카멜케이스를 꼭 써야 한다. 안 그러면 나중에 method named query 에서 곤란해진다.
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 ;
}
테스트 코드 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.... ------------------------" );
}
}
저장 저장은 repository.save 를 쓴다. 위 Member 처럼 @OneToMany 나 @ManyToOne
필드를 함께 저장할 수 있다.
조회 때와 클래스가 조금 다르다. 필드명을 카멜케이스로 바꿨고,
@JoinColumn 에 insertable, updatable 을 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 ;
}
테스트 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 );
// 실제 저장. (member클래스를 저장. 한다)
memberRepository . save ( data );
logger . info ( "------------ jpa test ended.... ------------------------" );
}
실행하면 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 =?
insert into 가 두 번, update 가 한 번 실행된걸 확인할 수 있다.
리스트에 추가한 두 개가 insert 되고, 바꾼 nickname 이 update 된다.
@NamedQuery 와 @NamedNativeQuery repository 의 findAll, findOneBy... 시리즈로도 조회가 되지만
쿼리를 직접 써야 할 때가 있다. 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>
Entity 클래스에 직접 선언해도 된다.
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 도 있다. 이건 Repository 에 쓴다.
둘의 차이가 중요하다.
named-query 는 코드 안에 선언한 Entity 를 기준으로 쿼리를 날린다named-native-query 는 DB 에 직접 쿼리를 날린다. 그래서 result-class 를 지정해야 한다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 ;
}
NamedQuery 를 직접 호출하기 EntityManager 를 쓰면 orm.xml 에 정의한 NamedQuery 를 바로 실행할 수 있다.
repository 로 호출하면 getResultList 로만 실행되는 것 같다.
update 로 반영된 row 수를 알아야 해서 아래처럼 했다.
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 와 @Autowired 둘 다 작동하는것 같다. 차이는 아직 잘 모르겠다.
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>
정리 설정은 라이브러리 두 개 + context-jpa.xml 하나면 끝난다 mybatis 와 같이 쓸거면 트랜잭션 매니저를 분리한다 필드명은 카멜케이스로. 안 그러면 method named query 에서 막힌다 복합키는 @IdClass 로 별도 클래스를 만든다 @ManyToOne 에는 @JoinColumn 을 같이 쓴다. optional 이 조인 종류를 가른다쿼리를 직접 써야 하면 orm.xml 의 named-query / named-native-query update 의 반영 건수가 필요하면 EntityManager.createNamedQuery().executeUpdate()