Backend - Mybatis useGeneratedKeys, selectKey에 대해서 알아보자.
Mybatis를 사용하여 INSERT 쿼리를 날릴 때, JPA와는 다르게 저장된 객체를 별도로 반환해주지 않는다.
따라서, Mybatis에서는 DB에 저장된 객체에 자동으로 생성된 ID 값을 알고 싶으면 useGeneratedKeys 또는 selectKey를 사용해야한다.
useGeneratedKeys
useGeneratedKeys는 자동 증가(AUTO_INCREMENT) 컬럼의 값을 Mybatis가 직접 JDBC를 사용하여 받아오도록 설정한다.
주로 아래와 같은 상황에서 사용한다.
- Mybatis(
AUTO_INCREMENT), MSSQL(IDENTITY)와 같은 자동 증가 키를 사용할 때 INSERT수행 후 생성된 키를 바로 받아서 객체에 할당할 때
사용 방법
INSERT 태그의 속성으로 useGeneratedKeys, keyProperty라는 속성을 지정해주면 된다.
예시
1
2
3
4
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="userSeq">
INSERT INTO users (email, password)
VALUES (#{email}, #{password})
</insert>
userSeq는User객체의 있는 기본키에 매핑되는 필드명INSERT끝나면 자동으로 생성된 PK 값이userSeq에 들어감
내부 동작 방식
useGeneratedKeys를 사용하게 되면, Mybatis에서는 아래와 같은 구조로 동작하게 된다.
<insert useGeneratedKeys="true" keyProperty="id" />사용- MyBatis가
Jdbc3KeyGenerator라는 클래스에processAfter메서드를 호출 keyProperty에 설정된 필드에 PK값 할당
Jdbc3KeyGenerator에 구현되어 있는 processAfter 메서드는 아래와 같다.
Jdsb3KeyGenerator.java
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
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
this.processBatch(ms, stmt, parameter);
}
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
String[] keyProperties = ms.getKeyProperties();
if (keyProperties != null && keyProperties.length != 0) {
try {
ResultSet rs = stmt.getGeneratedKeys();
try {
ResultSetMetaData rsmd = rs.getMetaData();
Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() >= keyProperties.length) {
this.assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Throwable var9) {
if (rs != null) {
try {
rs.close();
} catch (Throwable var8) {
var9.addSuppressed(var8);
}
}
throw var9;
}
if (rs != null) {
rs.close();
}
} catch (Exception var10) {
Exception e = var10;
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}
}
위 코드와 같이 내부적으로 processAfter 메서드가 호출되면 proccessBatch 메서드가 실행되고, getGeneratedKeys 메서드에서 생성된 PK 값을 ResultSet에 담는 것을 확인할 수 있다.
selectKey
selectKey는 값을 미리 또는 나중에 조회해서 조회된 값을 객체에 할당하는 역할을 한다.
즉, DB에서 PK를 생성하는 것이 아니라 SELECT문을 직접 만들어서 INSERT 전후에 값을 넣어주는 것이다.
주로 아래와 같은 상황에서 사용한다.
SEQUENCE를 통해 PK를 생성하는 DB(Oracle, PostgreSQL)를 사용할 때
사용 방법
INSERT 태그 안에 selectKey라는 태그를 넣고 속성을 설정하여 사용이 가능하다.
사용 예시
1
2
3
4
5
6
7
8
9
10
<insert id="insertUser" parameterType="User">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
SELECT USER_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO users (id, name, email)
VALUES (#{id}, #{name}, #{email})
</insert>
keyProperty: 조회 결과를 매핑할 필드 이름resultType:SELECT로 조회한 결과의 타입order:BEFORE또는AFTER사용 가능BEFORE:INSERT전의 실행AFTER:INSERT후의 실행
BEFORE vs AFTER
BEFORE
INSERT하기 전에 키를 구함SEQUENCE기반(Oracle, PostgreSQL)에 적합한 방식
예시
1
2
3
<selectKey keyProperty="id" resultType="int" order="BEFORE">
SELECT USER_SEQ.NEXTVAL FROM DUAL
</selectKey>
AFTER
INSERT후에 키를 구함- DB가 자동으로 키 값을 생성해주는 경우(보통
AUTO_INCREMENT)- 이 경우에는
selectKey보단useGenerated사용이 일반적
- 이 경우에는
예시
1
2
3
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
내부 동작 과정
- Mybatis가
INSERT쿼리를 실행할 때MappedStatment를 통해selectKey쿼리를 먼저(또는 나중에) 실행 - 반환된 결과는 Mybatis의
KeyGenerator인터페이스를 구현한SelectKeyGenerator클래스가 받아서 처리
SelectKeyGenerator가 반환된 키를 파라미터 객체의keyProperty에 집어넣는 역할
SelectKeyGenerator 내에 구현된 코드는 아래와 같다.
SelectKeyGenerator.java
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
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (this.executeBefore) {
this.processGeneratedKeys(executor, ms, parameter);
}
}
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (!this.executeBefore) {
this.processGeneratedKeys(executor, ms, parameter);
}
}
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
if (parameter != null && this.keyStatement != null && this.keyStatement.getKeyProperties() != null) {
String[] keyProperties = this.keyStatement.getKeyProperties();
Configuration configuration = ms.getConfiguration();
MetaObject metaParam = configuration.newMetaObject(parameter);
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
List<Object> values = keyExecutor.query(this.keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
if (values.isEmpty()) {
throw new ExecutorException("SelectKey returned no data.");
}
if (values.size() > 1) {
throw new ExecutorException("SelectKey returned more than one value.");
}
MetaObject metaResult = configuration.newMetaObject(values.get(0));
if (keyProperties.length == 1) {
if (metaResult.hasGetter(keyProperties[0])) {
this.setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
} else {
this.setValue(metaParam, keyProperties[0], values.get(0));
}
} else {
this.handleMultipleProperties(keyProperties, metaParam, metaResult);
}
}
} catch (ExecutorException var10) {
ExecutorException e = var10;
throw e;
} catch (Exception var11) {
Exception e = var11;
throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
}
}
BEFORE일 경우processBefore메서드 실행AFTER일 경우processAfter메서드 실행
1
List<Object> values = keyExecutor.query(this.keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
위 코드에서 selectKey 태그에 작성한 쿼리가 실행되어 PK 값을 조회한다.