We can use subtraction to negate one or more character classes. For example, we can match a set of odd decimal numbers:
@Test
public void givenSetWithSubtraction_whenMatchesAccurately_thenCorrect() {
int matches = runTest("[0-9&&[^2468]]", "123456789");
assertEquals(matches, 5);
}Copy
Only1, 3, 5, 7, 9will be matched.
Let's dive to Pattern and Matcher
1.
if String qr has contains "substring value" like otpauth%3A%2F%2Ftotp%2F4009acd5fe30716b6201a93f5ed98999%253A35mwlee%2540naver.com%3Fsecret%3DMIHON7TZY7SEIH27RAOROQ7DI7LHCAJEDBLI4R6LSITWZSITS7PQN75S65E2TLEB%26issuer%3D%25EB%258D%2594%25EB%2593%25AC%25EC%259D%25B4%25EC%2599%2595%25ED%2594%25BC%25EC%25BD%259C%25EB%25A1%259C%26id%3Do87a66240433494aa362ad88e69af9c5
We can parse ID string like this
try{
String decodeStr = URLDecoder.decode(qr, "UTF-8"); // URL Decode
Pattern pattern = Pattern.compile("id=([^&]+)"); // Standby ID substring
Matcher matcher = pattern.matcher(decodeStr); // Matcher Object will find the target data
if(matcher.find()) {
jsonUuid = matcher.group(1);
log.debug("Extracted ID : {}", jsonUuid);
} else {
log.debug("ID not found");
}
} catch (UnsupportedEncodingException e){
e.printStackTrace();;
}
최초 입수된 레코드가 남아 있음 최초 입수된 레코드의 AUTO_INCREMENT 값은 변하지 않음
REPLACE INTO ...
최초 입수된 레코드가 삭제되고, 신규 레코드가 INSERT됨 AUTO_INCREMENT의 값이 변경됨
INSERT INTO ... ON DUPLICATE UPDATE
INSERT IGNORE의 장점 포함함 중복 키 오류 발생 시, 사용자가 UPDATE될 값을 지정할 수 있음
2. 사전 조건
중복 레코드 관리를 위해선 테이블에PRIMARY KEY혹은UNIQUE INDEX가 필요하다.
본 예제에서는 아래와 같은person테이블을 이용하여 설명한다.
CREATE TABLE person
(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(20),
address VARCHAR(255),
PRIMARY KEY (id),
UNIQUE INDEX (name) -- 중복 검사용 필드
);
person테이블에서 PRIMARY KEY로 지정된id필드는 AUTO_INCREMENT를 위해 만든 필드이며, 본 문서에는name필드를 이용하여 중복을 검사한다.
만약 기존에 만들어진 테이블에 PRIMARY KEY 혹은 UNIQUE INDEX를 추가하려면 아래의 SQL을 이용하면 된다.
-- PRIMARY 추가하는 방법
ALTER TABLE person ADD PRIMARY KEY (name)
-- UNIQUE INDEX를 추가하는 방법
ALTER TABLE person ADD UNIQUE INDEX (name)
3. 중복 처리 방법
3-1) INSERT IGNORE
INSERT IGNORE는 중복 키 에러가 발생했을 때 신규로 입력되는 레코드를 무시하는 단순한 방법이다.
다음의 예를 보면 중복 키 에러가 발생했을 때 INSERT 구문 자체는 오류가 발생하지 않고, 대신’0 row affected’가 출력된 것을 볼 수 있다.
mysql> INSERT IGNORE INTO person VALUES (NULL, 'James', 'Seoul');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT IGNORE INTO person VALUES (NULL, 'Cynthia', 'Yongin');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT IGNORE INTO person VALUES (NULL, 'James', 'Seongnam');
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT IGNORE INTO person VALUES (NULL, 'Cynthia', 'Seoul');
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT IGNORE INTO person VALUES (NULL, 'James', 'Incheon');
Query OK, 0 rows affected (0.00 sec)
SELECT의 결과는 2건만 존재한다.
mysql> SELECT * FROM person;
+----+---------+---------+
| id | name | address |
+----+---------+---------+
| 1 | James | Seoul |
| 2 | Cynthia | Yongin |
+----+---------+---------+
2 rows in set (0.00 sec)
James의 주소가 최초 입력한Seoul이다. 즉, 최초에 입력된 레코드가 남아 있는 걸을 볼 수 있다.
또한 AUTO_INCREMENT 컬럼의 값이 1, 2인 것에 주목하라.
MySQL에서 AUTO_INCREMENT는 식별키 용도로 많이 사용하는데, 중복 발생 여부에 따라 식별 키가 변경되는 경우 여러 가지 불편한 점이 생긴다.
INSERT IGNORE에서는 AUTO_INCREMENT의 값이 변경되지 않는다는 장점이 있다.
3-2) REPLACE INTO
REPLACE INTO는 중복이 발생되었을 때 기존 레코드를 삭제하고 신규 레코드를 INSERT하는 방식이다.
person테이블을 drop 후 다시 생성한 뒤에 아래의 레코드를 입수해보자.
mysql> REPLACE INTO person VALUES (NULL, 'James', 'Seoul');
Query OK, 1 row affected (0.00 sec)
mysql> REPLACE INTO person VALUES (NULL, 'Cynthia', 'Yongin');
Query OK, 1 row affected (0.00 sec)
mysql> REPLACE INTO person VALUES (NULL, 'James', 'Seongnam');
Query OK, 2 rows affected (0.00 sec)
mysql> REPLACE INTO person VALUES (NULL, 'Cynthia', 'Seoul');
Query OK, 2 rows affected (0.00 sec)
mysql> REPLACE INTO person VALUES (NULL, 'James', 'Incheon');
Query OK, 2 rows affected (0.00 sec)
mysql> SELECT * FROM person;
+----+---------+---------+
| id | name | address |
+----+---------+---------+
| 4 | Cynthia | Seoul |
| 5 | James | Incheon |
+----+---------+---------+
2 rows in set (0.00 sec)
id가 4, 5로 변하였다. 또한 James의 주소가 “Incheon”으로 변경되었다.
이를 ‘2 rows affected’와 함께 종합적으로 판단한다면 “REPLACE INTO”는 다음과 같이 작동하는 것을 알 수 있다.
중복 키 오류 발생 시 기존 레코드를 삭제 -> 첫 번째 레코드가 affected되었음
이후 새로운 레코드를 입력 -> 두 번째 레코드가 affected되었음
그래서 ‘2 rows affected’가 출력되었다.
새로운 레코드가 입력되면서 AUTO_INCREMENT 컬럼의 값이 매번 새롭게 발급되었다.
“REPLACE INTO”는 그다지 좋은 방법이 아닌데 앞서 이야기한 것처럼 AUTO_INCREMENT는 레코드를 식별할 수 있는 id로 사용되는데 중복 발생 시 id가 변경되기 때문이다.
3-3) ON DUPLICATE UPDATE
ON DUPLICATE UPDATE의 장점은 중복 키 오류 발생 시 사용자가 원하는 값을 직접 설정할 수 있다는 점이다.
우선 기본적인 사용 방법을 보자.
마찬가지로 테이블을 drop후 새로 생성했다.
mysql> INSERT INTO person VALUES (NULL, 'James', 'Seoul')
ON DUPLICATE KEY UPDATE address = VALUES(address);
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'Cynthia', 'Yongin')
ON DUPLICATE KEY UPDATE address = VALUES(address);
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'James', 'Seongnam')
ON DUPLICATE KEY UPDATE address = VALUES(address);
Query OK, 2 rows affected, 1 warning (0.01 sec)
mysql> INSERT INTO person VALUES (NULL, 'Cynthia', 'Seoul')
ON DUPLICATE KEY UPDATE address = VALUES(address);
Query OK, 2 rows affected, 1 warning (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'James', 'Incheon')
ON DUPLICATE KEY UPDATE address = VALUES(address);
Query OK, 2 rows affected, 1 warning (0.01 sec)
(‘2 rows affected’의 의미는 아래 내용을 읽고 각자 생각해보자)
SELECT 결과를 보자.
mysql> SELECT * FROM person;
+----+---------+---------+
| id | name | address |
+----+---------+---------+
| 1 | James | Incheon |
| 2 | Cynthia | Seoul |
+----+---------+---------+
2 rows in set (0.00 sec)
이번에는id값이 최초 입수된 레코드의 값 그대로이다. 하지만address의 값이 마지막에 입수한 레코드로 변경되어 있다.
INSERT INTO ... ON DUPLICATE KEY UPDATE의 장점은 중복 발생 시 필드의 값을 내 맘대로 UPDATE할 수 있다는 점이다.
id필드만 놓고 보면INSERT IGNORE와 동일하지만address의 값이 변경된 것이INSERT IGNORE와INSERT INTO ... ON DUPLICATE KEY UPDATE의 차이점이다.
ON DUPLICATE KEY UPDATE를 이용하면 재미있는 것들을 할 수 있다. 중복 레코드가 총 몇 번이나 입수되었는지를 기록해보자.
이를 위해inserted_cnt필드를 추가하였다.
CREATE TABLE person
(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(20),
address VARCHAR(255),
inserted_cnt INT, -- 레코드가 몇 번 입수되었는지 확인용 필드
PRIMARY KEY (id),
UNIQUE INDEX (name)
);
mysql> INSERT INTO person VALUES (NULL, 'James', 'Seoul', 1)
ON DUPLICATE KEY UPDATE inserted_cnt = inserted_cnt + 1;
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'Cynthia', 'Yongin', 1)
ON DUPLICATE KEY UPDATE inserted_cnt = inserted_cnt + 1;
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'James', 'Seongnam', 1)
ON DUPLICATE KEY UPDATE inserted_cnt = inserted_cnt + 1;
Query OK, 2 rows affected (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'Cynthia', 'Seoul', 1)
ON DUPLICATE KEY UPDATE inserted_cnt = inserted_cnt + 1;
Query OK, 2 rows affected (0.00 sec)
mysql> INSERT INTO person VALUES (NULL, 'James', 'Incheon', 1)
ON DUPLICATE KEY UPDATE inserted_cnt = inserted_cnt + 1;
Query OK, 2 rows affected (0.00 sec)
SELECT를 해 보면inserted_cnt에는 해당 중복 값이 몇 번 INSERT 시도가 되었는지 기록되어 있을 것이다.
mysql> SELECT * FROM person;
+----+---------+---------+--------------+
| id | name | address | inserted_cnt |
+----+---------+---------+--------------+
| 1 | James | Seoul | 3 |
| 2 | Cynthia | Yongin | 2 |
+----+---------+---------+--------------+
2 rows in set (0.00 sec)
inserted_cnt필드의 값을 보면 알겠지만, 레코드가 몇 번 입수되었는지 저장되어 있다.
주의해야 할 점은 새로온 레코드의 값으로 UPDATE하고자 할 때는 항상 “VALUES(column)”과 같이VALUES()로 감싸야 한다는 점이다. 만약 다음과 같이VALUES()` 없이 필드명만 사용하는 것은 기존 레코드의 컬럼 값을 의미하게 된다.
INSERT INTO person VALUES (NULL, 'James', 'Incheon')
ON DUPLICATE KEY UPDATE address = address;
Cannot change the ExecutorType when there is an existing transaction in _____Processor.
1. Set a Consistent ExecutorType
Ensure that the ExecutorType is consistently set across all MyBatis operations. Typically, ExecutorType.BATCH is used for batch processing, but you can choose the appropriate one for your use case (SIMPLE, REUSE, BATCH).
In runtime, Spring Batch was excuted with ExecuteType.Batch, but annotation-base queries will be excuted with ExecuteType.SIMPLE, which is having differentition point.
1.1 Trouble Shooting and Solution
Implement MyBatisConfig with returning SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
Let's show e.g
import org.apache.ibatis.session.ExecutorType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
@MapperScan("your.package.com.model.mapper")
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// Ensure that the mapper XML files are found
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mappers/*.xml")
);
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
}
}
@MapperScan Annotation could be matched with interface of mapper location. if non-correct path was assigned, mapper interface do not find real object.
setMapperLocation method will be matched with mybatis.mapper-location in application.yml. So, We need to match mapper-location path like classpath:*mappers/*.xml
Since I use absolute path in application.yml, which is unusuall case, classpath solution could not be fitted.
In theory, we would use classpath with ExecutorType.SIMPLE.
[Concenturate to understand issue environment]
MyBatisPageReaderBuilder will return ExecutorType.Batch, and this part causes Spring Batch ExecutorType not to be matched issue.
@Bean
@StepScope
public MyBatisCursorItemReader<______Dto__or__Entity> ___ItemReader(
@Value("#{jobParameters[UUID]}") String uuid) throws Exception {
Map<String, Object> parameterValues = new HashMap<>();
parameterValues.put("uuid", uuid);
MyBatisCursorItemReaderBuilder<YourDto> readerBuilder = new MyBatisCursorItemReaderBuilder<>();
readerBuilder.sqlSessionFactory(sqlSessionFactory);
readerBuilder.queryId("your.package.com.model.mapper.InterfaceMapper.select");
readerBuilder.parameterValues(parameterValues);
return readerBuilder.build();
}