Spring

Spring JPA 연습 기록 — 설정부터 저장·NamedQuery 까지

목차

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 join

FetchType 은 이렇게 갈린다.

  • 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 필드를 함께 저장할 수 있다.

조회 때와 클래스가 조금 다르다. 필드명을 카멜케이스로 바꿨고, @JoinColumninsertable, 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 에 정의하는 방법이다.

/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>

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 둘 다 작동하는것 같다. 차이는 아직 잘 모르겠다.

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>

정리

  • 설정은 라이브러리 두 개 + context-jpa.xml 하나면 끝난다
  • mybatis 와 같이 쓸거면 트랜잭션 매니저를 분리한다
  • 필드명은 카멜케이스로. 안 그러면 method named query 에서 막힌다
  • 복합키는 @IdClass 로 별도 클래스를 만든다
  • @ManyToOne 에는 @JoinColumn 을 같이 쓴다. optional 이 조인 종류를 가른다
  • 쿼리를 직접 써야 하면 orm.xml 의 named-query / named-native-query
  • update 의 반영 건수가 필요하면 EntityManager.createNamedQuery().executeUpdate()