├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── framework-compare.md ├── groovy-config.md ├── images ├── groovy-eclipse.png ├── idea-groovy.png ├── sts-groovy-support.png └── typing-error.png ├── lazy-loading.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── spring-jdbc-core.md ├── spring-jdbc-extensions.md ├── sql-management.md └── src ├── main ├── java │ └── net │ │ └── benelog │ │ └── spring │ │ ├── AppConfig.java │ │ ├── domain │ │ ├── LazySeller.java │ │ ├── Product.java │ │ └── Seller.java │ │ └── persistence │ │ ├── ProductRepository.java │ │ ├── ProductSqls.groovy │ │ ├── SellerProductExtractor.java │ │ ├── SellerRepository.java │ │ └── SellerSqls.groovy └── resources │ ├── db.properties │ └── schema.sql └── test └── java └── net └── benelog └── spring ├── ProductIntegrationTest.java ├── SellerIntegrationTest.java └── persistence └── SellerSqlsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | bin 4 | target 5 | .classpath 6 | .project 7 | .settings/ 8 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benelog/spring-jdbc-tips/875f8872dd24b70eaa16ab8144aaf521e88b165b/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | - [MyBatis와 대비한 Spring JDBC의 장점](framework-compare.md) 3 | - [Spring JDBC의 핵심 사용법](spring-jdbc-core.md) 4 | - [Spring JDBC 확장 라이브러리](spring-jdbc-extensions.md) 5 | - [SQL 관리 방안](sql-management.md) 6 | - [Java 프로젝트에 Groovy를 섞어 쓰는 설정](groovy-config.md) 7 | 8 | -------------------------------------------------------------------------------- /framework-compare.md: -------------------------------------------------------------------------------- 1 | ## MyBatis 대비 Spring JDBC의 장점 2 | 아래와 같은 이유로 Spring JDBC는 MyBatis와 비교해서 학습 비용은 낮고 생산성은 높습니다. 3 | 4 | - 초기 설정이 단순합니다. 5 | - 쿼리 결과를 변환하는 구성요소(`RowMapper`)가 인터페이스로 정의되어 Java 컴파일러의 장점을 활용할 수 있습니다. 6 | - SQL 쿼리를 Groovy 파일 안에 선언하면 Java 컴파일러의 장점을 활용할 수 있습니다. 7 | - 간단한 ORMapping 기능을 제공합니다. 8 | - Collection 파라미터를 더 편하게 사용할 수 있습니다. 9 | - MyBatis의 일부 기능들은 단순히 쿼리를 실행하기만을 원하는 사람에게는 불필요하고 디버깅을 어렵게 합니다. 10 | - 세션관리, batch update 추상화 등 11 | 12 | 13 | ### 초기 설정 14 | Spring JDBC의 `NamedParameterJdbcTemplate`은 `DataSource` 객체만 미리 정의되어 있다면 코드 1줄로 생성할 수 있습니다. 15 | 16 | ```java 17 | NamedParameterJdbcOperations jdbc = new NamedParameterJdbcTemplate(dataSource); 18 | ``` 19 | 20 | `DataSource`만 외부에서 주입받아서 DAO 클래스나 공통 상위클래스에서 직접 `NamedParameterJdbcTemplate`를 생성해서 사용을 해도 됩니다. `DataSource` 선언은 어느 프레임워크를 쓰더라도 필요한 부분입니다. 따라서 Spring JDBC만을 위한 설정에 드는 시간은 거의 없습니다. 21 | 22 | MyBatis를 쓴다면 `SqlSessionFactory`, `SqlSessionTemplate`, 매퍼XML 혹은 매퍼 인터페이스와 같은 더 많은 구성요소가 중간에 들어갑니다. 초기 설정을 하기 위해서는 그런 구성요소의 개념과 역할을 이해해야 합니다. 23 | 24 | ### 쿼리 결과 변환 인터페이스 25 | Spring JDBC의 `RowMapper`라는 인터페이스는 JDBC의 `ResultSet`에서 원하는 자바 객체로 변환하는 역할을 합니다. 기본적으로 제공되는 `RowMapper`의 구현체로는 객체의 setter를 활용하는 `BeanPropertyRowMapper`, `java.util.Map`으로 변환하는 `ColumnMapRowMapper`가 있습니다. 컬럼명과 객체의 속성명을 직접 지정해 줄수 밖에 없을 때에는 `RowMapper` 인터페이스를 직접 구현합니다. 예를 들면 DB의 컬럼은 'desc'인데 Java객체에서는 'description'으로 매핑하고 싶을 때 같은 경우입니다. 26 | 27 | `RowMapper`를 구현할 때는 당연히 setter나 생성자 호출 같은 Java 문법을 그대로 활용하면 됩니다. `RowMapper`는 메서드가 하나인 인터페이스이기 때문에 람다표현식과 메서드 레퍼런스를 활용할 수도 있습니다. 아래 코드는 `seller`라는 속성을 포함한 `Product` 클래스에 대한 매핑 로직을 람다 표현식으로 작성한 예제입니다. 28 | 29 | ```java 30 | public class Product { 31 | private Integer id; 32 | private Integer name; 33 | private String description; 34 | private Seller seller; 35 | // getter, setter 생략 36 | ... 37 | 38 | } 39 | ``` 40 | 41 | ```java 42 | RowMapper productMapper = (rs, rowNum) -> { 43 | Product product = new Product(); 44 | product.setId(rs.getInt("id")); 45 | product.setName(rs.getString("name")); 46 | product.setDescription(rs.getString("desc")); 47 | 48 | Seller seller = new User( 49 | rs.getInt("sell_id"), 50 | rs.getString("sell_name") 51 | ); 52 | product.setSeller(seller); 53 | 54 | return product; 55 | }; 56 | ``` 57 | 58 | 아래 코드는 매핑 로직을 private 메서드 안에 두고 메서드 레퍼런스로 호출한 예입니다. 59 | 60 | ```java 61 | 62 | public Product findById(Integer id) { 63 | Map params = Collections.singletonMap("id", id); 64 | return jdbc.queryForObject(ProductSqls.SELECT_BY_ID, params, this::toProduct); 65 | } 66 | 67 | private Product toProduct(ResultSet rs, int rowNum) { 68 | // 매핑로직 69 | } 70 | 71 | ``` 72 | 73 | MyBatis에서는 아래와 같이 XML로 쿼리의 결과를 매핑하는 선언을 할 수 있습니다. 74 | 75 | ```xml 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ``` 89 | 90 | 위와 같은 XML형식에서는 `property="id"`와 같은 부분에 오타가 있어도 어플리케이션은 빌드, 배포될 수 있습니다. 그런 오류는 프레임워크가 실행되는 되는 시점에 확인되기 때문입니다. 반면 `RowMapper`의 구현 예제에서 `setId()`, `setName()`과 같은 메서드들은 오타를 친다면 아예 컴파일이 되지 않습니다. 속성명 중 일부만 입력해도 자동완성을 할 수 있습니다. 91 | 92 | `RowMapper`를 직접 구현하면 적극적인 타입변환을 간편하게 할 수도 있습니다. 날짜 변환이나 Enum, Java8의 `Optional`과 같은 클래스를 쓰기에도 좋습니다. 예를 들면 DB에는 'reg_time'이라는 컬럼이 VARCHAR(14)의 형식으로 '20150101120000'처럼 저장되어 있을 때 Java 객체는 LocalDateTime으로 쓰고 싶다고 합시다. 이 때 `RowMapper`안에서 바로 `LocalDateTime.parse()` 메서드를 호출하면 그런 작업은 간단히 끝납니다. 93 | 94 | ```java 95 | private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); // DB에 문자열로 들어간 날짜 데이터 변환에 사용 96 | 97 | RowMapper productMapper = (rs, rowNum) -> { 98 | Product product = new Product(); 99 | ... 100 | LocalDateTime regTime = LocalDateTime.parse(rs.getString("reg_time"), formatter); 101 | product.setRegisteredTime(regTime); 102 | return product; 103 | }; 104 | 105 | ``` 106 | 107 | MyBatis/iBatis를 쓴다면 타입의 변환은 DAO 안에서 하거나 별도의 [TypeHandler](http://www.mybatis.org/mybatis-3/ko/configuration.html#typeHandlers)를 작성해야 합니다. Spring JDBC에서도 [Converter](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html)를 구현해서 반복되는 타입 변환 로직은 별도의 클래스로 뺄 수도 있습니다. 하지만 1회성 작업이라면 직접 `RowMapper`안에서 구현할 수도 있으므로 MyBatis에 비해서 더 편리합니다. 108 | 109 | 일반화하면 XML선언과 비교해서 Java 코드의 장점은 아래와 같습니다. 110 | 111 | - 별도의 태그를 학습할 필요가 없음 112 | - IDE 기능을 활용할 수 있는 범위가 넓어짐 113 | - 코드 추적, 자동 완성, 오타 표시, 리팩토링 114 | - 빌드툴에 의한 검증 115 | - 변수명, 메소드 등에 오타가 있으면 아예 배포될 수 없음. 116 | - 구조화, 기능 확장에 용이 117 | - 테스트 코드 커버리지를 측정 가능 118 | - 성능에 유리 119 | - 별도의 XML 파싱비용이 없음. 120 | - 컴파일러의 최적화 대상이 됨. 121 | 122 | Spring JDBC의 `RowMapper`는 인터페이스 기반이기에 MyBatis의 ``과 대비하면 위와 같은 장점을 누릴 수 있습니다. 123 | 124 | MyBatis에서는 애너테이션으로 쿼리 결과를 매핑하는 방법도 있습니다. 125 | 126 | ```java 127 | @Select(SELECT_PRODUCT) 128 | @Results(value = { 129 | @Result(property="id", column="id"), 130 | @Result(property="name", column="name"), 131 | @Result(property="price", column="price") 132 | @Result(property="seller.id", column="seller_id") 133 | @Result(property="seller.name", column="seller_name") 134 | }) 135 | 136 | ``` 137 | 138 | 하지만 직접 getter나 생성자를 호출하는 것보다는 컴파일러의 장점을 활용하지 못합니다. `property="id"`와 같은 애노테이션의 속성값에 오타가 있어도 컴파일러는 에러를 내지 않고, 이름을 바꾸는 리팩토링을 할 때도 주의를 기울여야 합니다. 139 | 140 | 141 | ### SQL 선언 142 | Spring JDBC에서는 실행할 SQL을 String 타입으로 받습니다. SQL을 주로 XML안에 선언하는 MyBatis에 익숙한 분들은 이 점을 가장 Spring JDBC의 단점으로 여깁니다. 그러나 응용을 한다면 Spring JDBC에서는 MyBatis보다 편리한 방법으로 SQL을 관리할 수 있습니다. 143 | 144 | SQL을 Groovy의 Multiline String으로 선언하는 방법을 가장 권장합니다. Groovy로 만든 클래스는 Java 클래스와 동일한 방식으로 참조됩니다. 그 때문에 쿼리를 저장한 변수 이름을 IDE에서 자동 완성하고 코드를 추적하기에도 좋습니다. 추가적인 학습 비용도 없습니다. Groovy의 클래스, 변수 선언 방식을 Java와 동일한 방식으로 할 수 있습니다. `"""`을 쓰면 여러 줄로 문자열을 선언할 수 있다는 것 1가지만 알면 됩니다. 아래의 Groovy 코드를 보면 멀티라인 스트링을 제외하고는 기존 Java 문법과 동일한 것을 알 수 있습니다. 145 | 146 | ```groovy 147 | class SellerSqls { 148 | public static final String SELECT = """ 149 | SELECT name, address 150 | FROM seller 151 | WHERE name = :name 152 | """; 153 | } 154 | ``` 155 | `NamedParameterJdbcTemplate`을 호출할때 아래와 같이 String 상수를 참조하는 방식으로 SQL을 전달할 수 있습니다. `static final`로 선언된 상수이므로 컴파일 타임에 최적화될 가능성이 높다는 것도 장점입니다. 156 | 157 | ```java 158 | public List findByName(String name) { 159 | SqlParameterSource params = new MapSqlParameterSource("name", name); 160 | return jdbc.query(IssueSqls.SELECT_BY_ONWER_AND_NAME, params, sellerMapper); 161 | } 162 | ``` 163 | 164 | 같이 쿼리를 저장한 변수명을 잘 못 입력했을 경우 아래와 같이 IDE 안에서 바로 알려줍니다. 165 | 166 | ![typeing_error](images/typing-error.png) 167 | 168 | Dynamic SQL을 생성할 때도 Java의 조건/반복문과 다른 유틸리티 메서드 호출을 자연스럽게 쓸 수 있습니다. 169 | 170 | ```groovy 171 | import static org.apache.commons.lang.StringUtils.*; 172 | 173 | public static String buildSelectSql(Seller seller) { 174 | StringBuilder sql = new StringBuilder(); 175 | sql.append(""" 176 | SELECT name, address 177 | FROM seller 178 | WHERE 1=1 179 | """); 180 | 181 | if (isNotBlank(seller.getName())) { 182 | sql.append("AND name = :name \n"); 183 | } 184 | 185 | if (isNotBlank(seller.getAddress())) { 186 | sql.append("AND address = :address \n"); 187 | } 188 | 189 | if (isNotBlank(seller.getTelNo())) { 190 | sql.append("AND tel_no = :telNo \n"); 191 | } 192 | 193 | return sql.toString(); 194 | } 195 | ``` 196 | 197 | 자주 사용 될법한 메서드는 별도의 유틸리티 클래스로 빼고 static import 를 도입하는 등 추가적인 리팩토링 하기에도 좋습니다. 테스트 코드를 짠 후 Emma와 같은 코드커버리지 측정 도구를 사용할 수도 있습니다. 예를 들면 아래와 같이 where절을 생성하는 메서드를 따로 추출할 수도 있습니다. 198 | 199 | ```groovy 200 | public static String selectByCondition(Seller seller) { 201 | return """ 202 | SELECT id, name, tel_no, address, homepage 203 | FROM seller 204 | """ + 205 | whereAnd ( 206 | notEmpty(seller.getName(), "name = :name"), 207 | notEmpty(seller.getAddress(), "address = :address"), 208 | notEmpty(seller.getTelNo(), "tel_no = :telNo") 209 | ); 210 | } 211 | 212 | private static String notEmpty(String param, String condition) { 213 | return StringUtils.isEmpty(param)? null: condition; 214 | } 215 | 216 | private static String whereAnd(String ... conditions) { 217 | List finalCond = conditions.findAll({it != null}); 218 | Assert.notEmpty(finalCond); 219 | return "WHERE " + finalCond.join("\nAND "); 220 | } 221 | ``` 222 | 223 | 위의 예시에서는 `{it != null}` 와 같은 Groovy의 문법을 활용했지만, 기본 Java 문법으로도 같은 구현을 할 수 있습니다. 'where'절에 아무런 조건이 걸리지 않고 전체 열이 조회되는 것을 막기 위한 `Assert.notEmpty(finalCond);` 도 추가했습니다. 이런 코드를 공통 유틸리티에 넣어두어서 where 절에는 하나의 조건이상은 필수로 넣도록 프로젝트 정책으로 통제할 수도 있습니다. 224 | 225 | 226 | 또는 Groovy나 Kotlin의 String interpolation기능을 이용하면 더 간단한 코드를 쓸 수 있습니다. 227 | 위의 코드는 아래와 같이 바꿀수 있습니다. 228 | 229 | ```groovy 230 | public static String selectByCondition(Seller seller) { 231 | return """ 232 | SELECT id, name, tel_no, address, homepage 233 | FROM seller 234 | ${whereAnd( 235 | notEmpty(seller.name, "name = :name"), 236 | notEmpty(seller.address, "address = :address"), 237 | notEmpty(seller.telNo, "tel_no = :telNo") 238 | )} 239 | ); 240 | } 241 | 242 | ``` 243 | 244 | String interpolation안에서도 245 | 246 | Maven, Eclipse등에서 Groovy를 쓰기 위해 필요한 설정은 [Groovy 설정 방법](./groovy-config.md)을 참조하시기 바랍니다. Plugin을 설정, 설치해야하는 것이 MyBatis를 쓸 때와 비교하면 추가되는 비용이라고 생각하실 수도 있습니다. 하지만 `pom.xml`이나 `builde.gradle`에 groovy plugin을 추가하는 작업은 MyBatis를 쓰기 위한 설정을 하는 것과 비교하면 간단한 일입니다. Eclipse에서는 Groovy plugin을 각 개발자가 PC에 설치해야하는 추가 작업이 필요하기는 합니다. 그러나 Eclipse plugin 설치는 다운로드 받는 시간이 오래 걸릴 뿐 손이 많이 각는 작업은 아닙니다. IntelliJ IDEA Ultimate Edition에서는 Groovy plugin이 기본적으로 설치되어 있습니다. 247 | 248 | #### MyBatis의 XML 선언과의 비교 249 | MyBatis의 XML로는 비슷한 선언을 아래와 같이 해야합니다. 250 | 251 | 252 | ```xml 253 | 266 | 267 | ... 268 | 269 | 270 | 271 | 272 | 273 | ``` 274 | 275 | 여러 XML 태그의 사용법을 익혀야합니다. 오타가 뒤늦게 발견될 가능성도 높고, 코드를 추적하거나 리팩토링을 할 때도 IDE에서 할 수 있는 영역이 줄어듭니다. 자동 완성 되는 범위가 좁기에 유사한 코드를 복사해서 붙여넣기를 하는 경우가 많습니다. 붙여넣기한 코드를 미처 다 수정하지 않아서 생기는 오류도 많을 것입니다. 276 | 277 | 물론 MyBatis에서도 아래와 같은 IDE plugin을 이용하면 자동완성과 오류 검증에 도움을 받을 수 있습니다. 278 | 279 | - (Eclipse) [MyBatipse](https://marketplace.eclipse.org/content/mybatipse) 280 | - (Eclipse) [mybatiseditor](https://marketplace.eclipse.org/content/mybatiseditor) 281 | - (IntelliJ) [MyBatis plugin for IntelliJ IDEA](https://plugins.jetbrains.com/idea/plugin/7293-mybatis-plugin) 282 | 강점이 있습니다. 283 | 284 | 그렇지만 자바 컴파일러가 하는 검증은 Maven과 같은 빌드툴에 의한 빌드, IDE 등 여러 단계에서 동작한다는 장점이 더 있습니다. 예를 들면 IDE 밖에서 소스 충돌을 해결하면서 실수를 남긴 경우에도 컴파일 타임에 오류가 난다면 그런 코드가 배포되지 않도록 막을 수 있습니다. 285 | 286 | 287 | #### MyBatis의 애너테이션 사용법과의 비교 288 | 이전에는 iBatis/MyBatis가 SQL파일을 XML에 선언하는 것을 강제할 수 있다는 점에서 선호되는 경향도 있었습니다. 그러나 최근의 MyBatis에서는 Java파일 안에 SQL을 넣는 기능이 더 들어가서 그런 강제화의 효과는 없습니다. MyBatis에서는 아래와 같이 애너테이션으로 쿼리를 지정할 수 있습니다. 289 | 290 | ```java 291 | @Select("SELECT id, name, address FROM seller WHERE id = #{id}") 292 | Seller findById(Integer id) 293 | ``` 294 | 295 | Spring JDBC와 마찬가지로 Java 코드 안에서 SQL을 관리해야하는 과제가 생깁니다. 296 | 297 | 동적쿼리를 생성하기 위한 `org.apache.ibatis.jdbc.SQL` 클래스도 제공됩니다. 298 | 299 | ```java 300 | class SellerSqlBuilder { 301 | public String selectSeller(Seller seller) { 302 | return new SQL() {{ 303 | SELECT("name, address"); 304 | FROM("seller"); 305 | if (StringUtils.isNotEmpty(seller.getName())) { 306 | WHERE("name = #{name}"); 307 | } 308 | if (StringUtils.isNotEmpty(seller.getAddress())) { 309 | WHERE("address = #{address}"); 310 | } 311 | }}.toString(); 312 | } 313 | } 314 | ``` 315 | 316 | `@SelectProvider`라는 선언을 통해 SQL을 생성하는 메서드를 참조합니다. 317 | 318 | ```java 319 | @SelectProvider(type = SellerSqlBuilder.class, method = "selectSeller") 320 | List findSeller(Seller seller); 321 | 322 | ``` 323 | 324 | 위의 코드에서 `selectSeller` 속성값은 String 이므로, `SellerSqlBuilder.selectSeller()` 메서드의 이름을 바꾸는 리팩토링을 할때 따로 신경을 써줘야합니다. Spring JDBC를 쓸 때처럼 `SellerSqlBuilder.selectSeller()` 메서드를 직접 호출을 했다면 IDE의 리팩토링 기능을 안심하고 쓸 수 있을 것입니다. 325 | 326 | 정리하면, MyBatis에서 애너테이션 방식으로 쿼리를 쓴다고해도 Spring JDBC대비 장점은 보이지 않습니다. 327 | 328 | #### 활용법의 상호 응용 329 | MyBatis에서도 Java코드에 SQL을 선언할수 있게 됨에 따라 Spring JDBC와 MyBatis의 쿼리 관리 기법들은 상호 응용이 가능합니다. 2가지 예를 들어보겠습니다. 330 | 331 | 첫째, MyBatis에서도 Groovy의 멀티라인 스트링을 이용하는 것입니다. Groovy 클래스 안에 `public static final`로 선언된 SQL은 애너테이션 안에서도 참고가 가능합니다. SQL 선언은 Groovy로 된 상수 클래스에 모으고 아래와 같이 `@Select`와 같은 애너테이션에서는 상수를 참조하도록 선언할 수 있습니다. 332 | 333 | ```java 334 | @Select(SellerSqls.SELECT_BY_ID) 335 | Seller findById(Integer id) 336 | ``` 337 | 338 | 둘째, MyBatis의 Sql builder를 Spring JDBC에서 활용하는 것입니다. 앞선 예제의 `SellerSqlBuilder.selectSeller()`를 직접 호출한 후 SQL이 단긴 String을 `NamedParameterJdbcTemplate`에 파라미터로 넘기는 방식입니다. `SellerSqlBuilder.selectSeller()`의 메서드 이름을 바꿀 때 더 안전해진다는 장점이 생깁니다. 339 | 340 | SQL 쿼리 관리에 대한 더 다양하고 자세한 내용은 [SQL 관리방법](sql-management.md)을 참조하시기 바랍니다. 341 | 342 | 343 | ### 간단한 ORMapping 기능 344 | Spring JDBC와 이를 확장한 라이브러리를 이용하면 간단한 수준으로 ORM의 기능을 쓸 수도 있습니다. 345 | 346 | - Create 쿼리 자동생성 : `SimpleJdbcInsert` 347 | - Update 쿼리 자동생성 : `SimpleJdbcUpdate` (확장 라이브러리) 348 | - 1:다 관계를 1개의 쿼리로 조회하는 경우 객체 매핑 : `OneToManyResultSetExtractor` (확장 라이브러리) 349 | 350 | 위의 정도로도 충분히 생산성에 도움이 됩니다 `SimpleJdbcInsert`, `SimpleJdbcUpdate`를 이용하면 'CRUD' 중 'CU'(Create, Update)에 대한 쿼리를 직접 작성할 필요가 없습니다. 'D'(Delete)는 어짜피 쿼리가 단순해서 큰 부담이 되지 않습니다. 'R'(READ) 중 여러 건을 조화하는 쿼리는 ORM을 쓰더라도 직접 SQL과 비슷한 추상화수준으로 기술해주어야하는 경우가 많습니다. Native SQL을 쓸때는 Spring JDBC쪽이 더 편리한 면도 있습니다. 351 | 352 | 나름대로의 관계 매핑 전략도 수립할 수 있습니다. 예를 들면 1대1, 다대1 관계는 RowMapper안에서 직접 직접해주고 1대다 관계에는 `OneToManyResultSetExtractor`를 이용하는 식입니다. 지연로딩은 람다표현식을 이용해서 수동으로 할 수도 있습니다. 보다 자세한 내용은 아래 링크를 참조하시기 바랍니다. 353 | 354 | - [Spring JDBC 핵심 사용법](spring-jdbc-core.md) 중 SimpleJdbcInsert 단락 355 | - [Spring JDBC 확장 라이브러리](spring-jdbc-extensions.md) : `SimpleJdbcUpdate`와 `OneToManyResultSetExtractor`에 대한 소개 356 | - [연관관계 지연로딩 기법](lazy-loading.md) 357 | 358 | JPA 같은 본격 ORM에 비하면 빈약한 기능이지만 더 단순하고 명확하다는 장점은 있습니다. 359 | 360 | ### 단순한 Collection 파라미터 매핑 361 | 하나의 파라미터로 여러개의 값을 넘길 때에는 `java.util.List` 값으로 넣으면 됩니다. `IN` 절에서 여러개의 파라미터를 받는 코드를 예로 들겠습니다. MyBatis를 쓸때는 아래와 같이 XML 안에서 foreach태그로 반복문을 써줘야합니다. XML 태그로 제어문 프로그래밍을 하는 격입니다. 362 | 363 | ```xml 364 | 372 | ``` 373 | 374 | Spring JDBC의 `NamedParameterJdbcTemplate`에서 참조할 쿼리라면 아래와 같이 옮길 수 있습니다. 375 | 376 | ```groovy 377 | public static final String SELECT_SELLER_IN = """ 378 | SELECT id, name, tel_no, address, homepage 379 | FROM seller 380 | WHERE id IN (:idList) 381 | """; 382 | ``` 383 | 384 | ``선언 같은 것을 없이 `Collection` 타입을 바로 파라미터로 받을 수 있습니다. 385 | 386 | ### MyBatis의 Session 관리 387 | MyBatis의 `SqlSession`은 단순한 쿼리 실행기 이상의 역할을 합니다. 세션의 생명주기 내에서 추상화된 레이어를 제공해서 나름대로의 최적화를 합니다. 예를 들면 `SqlSession`의 생명 주기 내에서 같은 쿼리가 같은 파라미터로 조회 요청이 오면 실제로는 쿼리를 날리지 않고 캐쉬된 값을 반환합니다. `ExecutorType.BATCH` 모드로 `SqlSession`이 열리게 되면 INSERT, UPDATE, DELETE 구문은 바로 실행되지 않고 다음에 SELECT문이 들어오면 JDBC의 batchUpdate로 실행하게 됩니다. 388 | 389 | 이런 특성을 잘 활용해서 성능을 개선할수도 있는 장점이 있을수도 있습니다. 그러나 단순히 쿼리를 실행하는 프레임워크를 기대한 사람에게는 이런 특성이 부담이 될 수도 있습니다. 경우에 따라서 언제 쿼리가 날아갈지 예측을 해야하기 때문입니다. 390 | 391 | Spring JDBC는 단순히 쿼리만 실행하기를 원하는 사람의 기대에 맞게 동작합니다. batchUpdate같은 동작도 의도적으로 구분해서 실행을 할 수 있습니다. 세션 관리 등 JDBC를 더 추상화한 API를 원하는 사람에게는 JPA가 더 풍부한 기능을 제공합니다. MyBatis는 그 중간이라서 표지셔닝이 모호합니다. 392 | 393 | ## 더 발전한 프레임워크를 찾는다면 394 | 395 | ### SQL 구문을 쓰면서 컴파일러와 IDE의 장점을 이용하고 싶다면? 396 | Spring JDBC에서 컴파일타임에 검증되는 부분이 많은 점이 마음에 들었다면 그 특징을 더 강화한 프레임워크를 검토해볼만합니다. [Querydsl Sql](https://github.com/querydsl/querydsl/tree/master/querydsl-sql)과 397 | [JOOQ](http://www.jooq.org/)는 SQL의 선언도 Java의 타입을 살린 코드로 작성하는 프레임워크입니다. 그래서 Spring JDBC에서는 문자열일 뿐이였던 SQL을 작성할 때도 오타를 더 많이 검증하고 자동 완성을 활용할 수 있습니다. 398 | 399 | ### 객체매핑 로직을 더 추상화하고 싶다면? 400 | Spring JDBC는 JDBC를 단순하고 편리하게 쓰는 것에 초점을 맞춘 프레임워크이기에 풍부한 객체 매핑 기능을 제공하지는 않습니다. 본격적인 ORM을 사용해보고 싶다면 JPA 스펙과 Hibernate, [Spring Data JPA](http://projects.spring.io/spring-data-jpa/)에 관심을 가질만합니다. 401 | 402 | 경험으로는 ORM을 도입한다고 해서 초기 생산성이 바로 올라가지는 않습니다. MyBatis에 비해서도 초기에 알아야 지식이 많습니다. 어떤 전략을 써서 객체를 매핑할지에 대해서는 오히려 더 많은 고민이 생깁니다. 단순히 쿼리 생성을 자동화해서 생산성을 올리겠다는 기대만으로 ORM을 도입한다면 어긋날 가능성이 높습니다. 403 | 404 | ORM을 경험해보지 못했다면 앞에서 소개한 Spring JDBC의 간단한 ORM 기능을 먼저 사용해보시기를 추천드립니다. 그 과정에서 INSERT 쿼리가 자동 생성되는 것이나 테이블간의 관계를 살린 객체의 장점 정도는 느낄 수 있습니다. DB와 대응되는 객체를 어떻게 설계할지에 대한 노하우를 쌓을 수도 있습니다. 프로젝트의 상황이나 구성원의 성향에 따라서는 그 정도로도 충분히 만족스러울 수도 있습니다. Spring JDBC에서 자동으로 해주는 부분이 많지 않아서 아쉽다면 ORM의 필요성이 느껴진 것입니다. 그럴 때 점진적으로 JPA 같은 ORM을 도입할 수도 있습니다. 405 | -------------------------------------------------------------------------------- /groovy-config.md: -------------------------------------------------------------------------------- 1 | ## Java 프로젝트에 Groovy를 섞어 쓰는 설정 2 | 3 | ### Maven 4 | `/src/main/java` 아래에 `.groovy` 파일을 놓아도 컴파일을 할 수 있는 설정입니다. 5 | 6 | `` 태그의 하위 요소로 groovy에 대한 의존성을 추가합니다. 7 | 8 | ```xml 9 | 10 | org.codehaus.groovy 11 | groovy 12 | 2.4.7 13 | 14 | ``` 15 | 16 | `` 태그의 하위 요소로 ``에 대한 선언을 아래와 같이 추가합니다. 17 | 18 | ```xml 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 23 | 1.8 24 | 1.8 25 | groovy-eclipse-compiler 26 | 128m 27 | 512m 28 | utf-8 29 | true 30 | 31 | 32 | 33 | org.codehaus.groovy 34 | groovy-eclipse-compiler 35 | 2.9.1-01 36 | 37 | 38 | org.codehaus.groovy 39 | groovy-eclipse-batch 40 | 2.3.7-01 41 | 42 | 43 | 44 | ``` 45 | 46 | 참고로 groovy-eclipse-compiler 2.7.x대의 버전을 쓸때는 `groovy-eclipse-batch`에 대한 의존성을 따로 추가하지 않아도 됩니다. 47 | 48 | ### Gradle 49 | `/src/main/java` 아래에 `.groovy` 파일을 놓아도 컴파일을 할 수 있는 설정입니다. 50 | 51 | ```groovy 52 | apply plugin: 'java' 53 | apply plugin: 'groovy' 54 | 55 | [compileGroovy, compileTestGroovy]*.options*.encoding = 'UTF-8' 56 | 57 | compileJava { 58 | sourceCompatibility = 1.8 59 | targetCompatibility = 1.8 60 | } 61 | 62 | compileJava { 63 | dependsOn compileGroovy 64 | } 65 | 66 | sourceSets { 67 | main { 68 | groovy { 69 | srcDirs = ['src/main/java'] 70 | } 71 | java { 72 | srcDirs = [] 73 | } 74 | } 75 | } 76 | 77 | tasks.withType(GroovyCompile) { 78 | dependsOn = [] 79 | } 80 | 81 | tasks.withType(JavaCompile) { task -> 82 | dependsOn task.name.replace("Java", "Groovy") 83 | } 84 | 85 | ``` 86 | 87 | ### Eclipse 88 | 89 | #### (권장) 방법1 : Update site 주소 이용 90 | 1. `[Help] > [Install New Software ...]` 메뉴로 이동. 91 | 2. 플러그인 업데이트 사이트(Update site)의 주소를 'Work With’란에 입력한 후 `Add` 버튼을 누른다. 92 | - Eclipse 4.6 (Neon) : http://dist.springsource.org/snapshot/GRECLIPSE/e4.6/ 93 | - Eclipse 4.5 (Mars) : http://dist.springsource.org/snapshot/GRECLIPSE/e4.5/ 94 | - Eclipse 4.4 (Luna) : http://dist.springsource.org/release/GRECLIPSE/e4.4/ 95 | - Eclipse 4.3 (Kepler) : http://dist.springsource.org/release/GRECLIPSE/e4.3/ 96 | - Eclipse 4.2 (Juno) : http://dist.springsource.org/release/GRECLIPSE/e4.2/ 97 | - Eclipse 3.7 (Indigo) : http://dist.springsource.org/release/GRECLIPSE/e3.7/ 98 | - Eclipse 3.6 (Helios) : http://dist.springsource.org/release/GRECLIPSE/e3.6/ 99 | 3. 아래 2개의 Plugin을 선택 100 | - Groovy Eclipse 101 | - m2e Configurator for Groovy-Eclipse 102 | 103 | ![eclipse groovy plugin](images/groovy-eclipse.png) 104 | 105 | 설치가 끝난 후에도 pom.xml에 빨간 줄이 남아 있다면 아래 동작을 수행하다. 106 | 107 | - 프로젝트명에서 우클릭 `> [Maven] > [Disable Maven Nature]` 108 | - 프로젝트명에서 우클릭 `> [Configure] > [Convert to Maven Project]` 109 | 110 | #### 방법2 : STS(SpringSource Tools Suite)의 Extension 기능으로 설치 111 | STS를 쓰고 있다면 사용할 수 있는 방법입니다. 그러나 이 방법은 STS 3.8.3 (Eclipse 4.6 기반)에서는 잘 동작하지 않습니다. 112 | 113 | 1. `[Help] > [Dashboard]`메뉴로 이동 114 | 2. `[Extensions]` 탭 선택 115 | 3. `Find:`란에 groovy를 입력 116 | 4. 아래 2가지 plugin을 선택해서 설치합니다. 117 | - Groovy-Eclipse 118 | - Groovy-Eclipse Configurator for M2Eclipse 119 | 120 | 이미 설치된 Plugin이 있을지도 모르므로 'Show installed' 옵션을 켜고 확인을 하는 것이 좋습니다. 121 | 122 | ![sts-groovy-support](images/sts-groovy-support.png) 123 | 124 | #### 방법3 : `pom.xml`에서 Quick fix 125 | pom.xml에 `maven-compiler-plugin`빨간 줄이 뜨면 Ctrl +1 을 누른 후 안내에 따라 플러그인을 찾아서 설치합니다. 126 | 127 | 이 방법도 STS 3.8.3 (Eclipse 4.6 기반)에서는 잘 동작하지 않습니다. 128 | 129 | ### IntelliJ 130 | JetBrains의 Groovy plugin은 IDEA community Edition에서도 사용할 수 있습니다. 131 | 132 | 'File > Settings'(단축키 Ctrl + Alt + S) 로 이동해서 Plugin 메뉴를 클립합니다. Groovy Plugin이 보인다면 선택합니다. 133 | 134 | ![idea groovy plugin](images/idea-groovy.png) 135 | 136 | 없다면 `[Install JetBrains plugins...]`를 클릭합니다. Groovy로 검색해서 설치합니다. 137 | 138 | 참고로 pom.xml에 `groovy-eclipse-compiler`에 대한 선언이 있어도 IntelliJ에서는 이를 잘 지원합니다. 139 | 140 | -------------------------------------------------------------------------------- /images/groovy-eclipse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benelog/spring-jdbc-tips/875f8872dd24b70eaa16ab8144aaf521e88b165b/images/groovy-eclipse.png -------------------------------------------------------------------------------- /images/idea-groovy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benelog/spring-jdbc-tips/875f8872dd24b70eaa16ab8144aaf521e88b165b/images/idea-groovy.png -------------------------------------------------------------------------------- /images/sts-groovy-support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benelog/spring-jdbc-tips/875f8872dd24b70eaa16ab8144aaf521e88b165b/images/sts-groovy-support.png -------------------------------------------------------------------------------- /images/typing-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benelog/spring-jdbc-tips/875f8872dd24b70eaa16ab8144aaf521e88b165b/images/typing-error.png -------------------------------------------------------------------------------- /lazy-loading.md: -------------------------------------------------------------------------------- 1 | ## 연관 관계 지연 로딩 기법 2 | 지연 로딩(lazy loading)은 객체를 로딩하는 시점을 실제로 그 객체가 쓰이는 시점까지 미루는 패턴입니다. ORM에서는 불필요한 SQL 쿼리가 미리 호출되지 않도록 이 기법이 자주 사용됩니다. 3 | 4 | 예를 들어 설명해보겠습니다. 아래와 같이 'Seller'객체가 'List' 타입의 속성을 참조를 하는 객체 관계가 있습니다. 5 | 6 | ```java 7 | public class Seller { 8 | private Integer id; 9 | private String name; 10 | .... 11 | private List productList; 12 | // getter, setter 생략 13 | ``` 14 | 15 | DB에는 'seller'와 'product'가 각각의 테이블에 저장되어 있습니다. 'Seller' 객체를 로딩할 때 'productList'까지 함께 로딩한다면 2번의 쿼리가 필요합니다. 그런데 'productList'속성이 필요한 화면도 있고, 아닌 곳도 있다고 가정을 해보겠습니다. 따라서 'productList'를 미리 로딩해두는 것은 불필요한 쿼리를 실행하는 것일수도 있습니다. 16 | 17 | 이런 문제를 해결하는 가장 전통적인 방법은 화면에 따라서 서비스 레이어의 메서드를 분리하는 것입니다. 'SellerService.findSellerWithProductList()'처럼 'productList'를 함께 로딩하는 메서드를 따로 하나 더 두는 것입니다. 이런 방식은 코드를 추적할 때 SQL 호출의 흐름이 명확히 보인다는 장점이 있습니다. 반면 서비스 레이어의 메서드가 많이 늘어날 수 있고, UI 레이어의 변경이 있을 때 서비스 레이어까지 신경을 써야한다는 단점도 있습니다. 지연 로딩 패턴은 이런 단점을 보완할 수 있습니다. 18 | 19 | Hibernate 같은 ORM에서는 간단한 옵션으로 'getProductList()'가 호출될 때 SQL 쿼리를 실행하는 지연 로딩 기능을 제공합니다. Spring JDBC같이 본격적인 ORM이 아닌 프레임워크를 쓸 때도 수동으로 이를 흉내내어 볼 수도 있습니다. 아래와 같이 Seller를 상속한 하위 클래스를 만듭니다. 20 | 21 | 22 | ```java 23 | public class LazySeller extends Seller { 24 | private Supplier> productLoader; 25 | 26 | public LazySeller(Seller seller, Supplier> productLoader) { 27 | this.productLoader = productLoader; 28 | BeanUtils.copyProperties(seller, this); 29 | } 30 | 31 | @Override 32 | public List getProductList() { 33 | if(super.getProductList() != null) { 34 | return super.getProductList(); 35 | } 36 | 37 | List productList = productLoader.get(); 38 | super.setProductList(productList); 39 | return productList; 40 | } 41 | } 42 | ``` 43 | 44 | 'productList'를 로딩하는 역할을 'productLoader' 멤버 변수에 위임을 했습니다. 'productLoader'은 `java.util.function.Supplier`타입으로 정의했습니다. 45 | 46 | 서비스 레이어 혹은 Repository 레이어에서 'productLoader'를 람다표현식으로 선언할 수 있습니다. 47 | 48 | ```java 49 | public Seller findById(Integer id) { 50 | Seller seller = dao.findSellerById(id); 51 | return new LazySeller( 52 | seller, 53 | () -> dao.findProductListBySellerId(id); 54 | ); 55 | } 56 | ``` 57 | 58 | 위의 예제를 실행하면 `LazySeller`가 생성되는 시점에서는 `LazySeller.productLoader` 변수에 람다표현식으로 정의한 객체를 할당하기만 합니다. `LazySeller.getProductList()`가 호출되는 시점에서야 `productLoader.get();`이 실행되어서 `dao.findProductListBySellerId(id);`이 호출됩니다. 59 | 60 | 아래 링크에서 같은 패턴으로 구현한 예제를 참조하실 수 있습니다. 61 | 62 | - [LazySeller](/src/main/java/net/benelog/spring/domain/LazySeller.java) 63 | - [SellerRepository](/src/main/java/net/benelog/spring/persistence/SellerRepository.java) 64 | - [SellerIntegrationTest](/src/test/java/net/benelog/spring/SellerIntegrationTest.java) 65 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | net.benelog.spring 6 | spring-jdbc-examples 7 | 0.0.1-SNAPSHOT 8 | jar 9 | spring-jdbc-examples 10 | spring jdbc 예제 11 | 12 | 4.3.5.RELEASE 13 | 14 | 15 | 16 | org.apache.commons 17 | commons-dbcp2 18 | 2.1.1 19 | 20 | 21 | org.springframework 22 | spring-context 23 | ${springframework.version} 24 | 25 | 26 | org.springframework 27 | spring-jdbc 28 | ${springframework.version} 29 | 30 | 31 | org.springframework.data 32 | spring-data-jdbc-core 33 | 1.2.1.RELEASE 34 | 35 | 36 | com.h2database 37 | h2 38 | 2.1.210 39 | runtime 40 | 41 | 42 | junit 43 | junit 44 | 4.13.1 45 | test 46 | 47 | 48 | org.springframework 49 | spring-test 50 | ${springframework.version} 51 | test 52 | 53 | 54 | org.codehaus.groovy 55 | groovy 56 | 2.4.21 57 | 58 | 59 | commons-lang 60 | commons-lang 61 | 2.3 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-compiler-plugin 70 | 71 | 1.8 72 | 1.8 73 | groovy-eclipse-compiler 74 | 128m 75 | 512m 76 | utf-8 77 | true 78 | 79 | 80 | 81 | org.codehaus.groovy 82 | groovy-eclipse-compiler 83 | 2.9.1-01 84 | 85 | 86 | org.codehaus.groovy 87 | groovy-eclipse-batch 88 | 2.3.7-01 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /spring-jdbc-core.md: -------------------------------------------------------------------------------- 1 | ## Spring JDBC의 핵심 사용법 2 | Spring JDBC에서 자주 쓰이는 클래스/인터페이스의 사용법을 정리합니다. 3 | 4 | (링크는 Javadoc) 5 | 6 | - [NamedParameterJdbcTemplate](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html) : 이름이 붙여진 파라미터가 들어간 SQL을 호출 7 | - [RowMapper](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/RowMapper.html) : ResultSet 에서 값을 추출하여 원하는 객체로 변환 8 | - [BeanPropertyRowMapper](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/BeanPropertyRowMapper.html) : ResultSet -> Bean 으로 변환 9 | - [ColumnMapRowMapper](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/ColumnMapRowMapper.html) : ResultSet -> Map 으로 변환 10 | - [SqlParameterSource](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/SqlParameterSource.html) : SQL에 파라미터 전달 11 | - [BeanPropertySqlParameterSource](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.html) : Bean 객체로 파리미터 전달 12 | - [MapSqlParameterSource](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.html) : Map으로 파라미터 전달 13 | - [SimpleJdbcInsert](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/simple/SimpleJdbcInsert.html) : Insert 쿼리를 자동생성 14 | 15 | 16 | ### NamedParameterJdbcTemplate 17 | `NamedParameterJdbcTemplate`을 사용하면 SQL 쿼리 안에서 `?`로 표현되던 파라미터를 `:productName`과 같이 이름을 붙여서 지정할 수 있습니다. 여러 개의 파라미터가 있는 쿼리를 실행할 때는 `JdbcTemplate`보다 `NamedParameterJdbcTemplate`을 사용하기를 권장합니다. 18 | 19 | `NamedParameterJdbcTemplate`의 동작은 `NamedParameterJdbcOpertaion`라는 인터페이스에 정의되어 있기도 합니다. 참조타입으로 그 인터페이스를 활용할 수도 있습니다. 20 | 21 | NamedParameterJdbcTemplate은 DataSource 객체를 필요로 합니다. 생성자에서 DataSource를 전달받을 수도 있습니다. 22 | 23 | ```java 24 | NamedParameterJdbcOperations jdbc = new NamedParameterJdbcTemplate(dataSource); 25 | ``` 26 | 27 | JdbcTemplate 계열은 멀티스레드에서 접근해도 안전합니다. 따라서 매번 객체를 생성할 필요는 없습니다. DAO등에서는 멤버변수로 저장해 둡니다. DataSoure 객체만 외부에서 주입받아서 아래와 같이 설정할 수 있습니다. 28 | 29 | ```java 30 | public class SellerRepository { 31 | private NamedParameterJdbcOperations jdbc; 32 | public setDataSource(DataSource dataSource) { 33 | this.jdbc = new NamedParameterJdbcTemplate(dataSource); 34 | } 35 | ... 36 | ``` 37 | 38 | [NamedParameterJdbcDaoSupport](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.html) 를 활용하면 `setDataSource()` 메서드과 멤버변수를 직접 선언하지 않아도 됩니다. 대신 `getNamedParameterJdbcTemplate()`으로 NamedParameterJdbcTemplate을 얻어옵니다. `getNamedParameterJdbcTemplate()`는 메서드 이름이 긴 편이라 짧은 이름으로 따로 멤버변수를 지정하는 편이 편할수도 있습니다. 39 | 40 | 41 | ```java 42 | public class SellerRepository extends NamedParameterJdbcDaoSupport { 43 | public int update(Seller seller) { 44 | SqlParameterSource params = new BeanPropertySqlParameterSource(seller); 45 | return getNamedParameterJdbcTemplate().update(SellerSqls.UPDATE, params); 46 | } 47 | 48 | ``` 49 | 50 | `component-scan`과 생성자 주입을 같이 쓸 때는 아래와 같이 선언합니다. 51 | 52 | ```java 53 | @Repository 54 | public class SellerRepository { 55 | private NamedParameterJdbcOperations jdbc; 56 | @Autowired 57 | public SellerRepository(DataSource dataSource) { 58 | this.jdbc = new NamedParameterJdbcTemplate(dataSource); 59 | } 60 | ``` 61 | 62 | DataSource가 여러 개일때는 `@Qualifier`나 `@Resource` 선언을 이용해서 원하는 DataSource를 하나만 찍어서 지정해야 합니다. 63 | 64 | ### RowMapper : 쿼리 결과를 객체로 변환 65 | `RowMapper`는 JDBC의 인터페이스인 `ResultSet`에서 원하는 객체로 타입을 변환하는 역할을 합니다. 기본적인 전략을 구현한 클래스는 Spring JDBC에서 제공을 합니다. 66 | 67 | #### BeanPropertyRowMapper 68 | DB의 컬럼명과 bean 객체의 속성명이 일치하다면 `BeanPropertyRowMapper`를 이용하여 자동으로 객체변환을 할 수 있습니다. DB 컬럼명이 'snake_case'로 되어 있어도 'camelCase'로 선언된 클래스의 필드로 매핑이 됩니다. 69 | 70 | 예를 들어 아래와 같은 `Seller` 객체가 있을 때, 71 | 72 | ```java 73 | public class Seller { 74 | private Integer id; 75 | private String name; 76 | private String address;` 77 | private String telNo; 78 | private String homepage; 79 | ... 80 | } 81 | ``` 82 | 83 | 다음과 같은 코드로 `ResultSet`에서 `Seller`로 타입을 변환하여 쿼리 결과를 받아옵니다. 84 | ```java 85 | public static final String SELECT_BY_ID = 86 | "SELECT id, name, tel_no, address, homepage FROM seller WHERE id = :id"; 87 | 88 | private RowMapper sellerMapper = BeanPropertyRowMapper.newInstance(Seller.class); 89 | 90 | public Seller findById(Integer id) { 91 | Map params = Collections.singletonMap("id", id); 92 | return jdbc.queryForObject(SELECT_BY_ID, params, sellerMapper); 93 | } 94 | ``` 95 | 96 | DB컬럼의 이름인 `tel_no`는 snake_case였는데, `Seller`객체의 속성이름인 `telNo`는 camelCase입니다. 별다른 설정이 없어도 자동으로 매핑이 되었습니다. 97 | 98 | 'BeanPropertyRowMapper'의 타입 변환 전략을 확장하고 싶다면 99 | [BeanPropertyRowMapper.setConversionService()](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/BeanPropertyRowMapper.html#setConversionService-org.springframework.core.convert.ConversionService-) 메서드를 통해 직접 구현한 `Converter`를 등록할 수 있습니다. `java.sql.TimeStamp`를 `ZonedDateTime`으로 변환하는 예를 들어보겠습니다. 100 | 101 | 아래와 같이 `Converter`를 구현하고, 102 | 103 | ```java 104 | public class ZonedDateTimeConverter implements Converter { 105 | @Override 106 | public ZonedDateTime convert(Timestamp source) { 107 | return ZonedDateTime.ofInstant(source.toInstant(), ZoneId.of("UTC")); 108 | } 109 | } 110 | ``` 111 | 112 | `DefaultConversionService.addConverter()`, `BeanPropertyRowMapper.setConversionService()`를 호출해서 `Converter` 구현체를 등록합니다. 113 | 114 | ```java 115 | public static RowMapper getRowMapper(Class mappedClass) { 116 | BeanPropertyRowMapper mapper = BeanPropertyRowMapper.newInstance(mappedClass); 117 | DefaultConversionService service = new DefaultConversionService(); 118 | service.addConverter(new ZonedDateTimeConverter()); 119 | mapper.setConversionService(service); 120 | return mapper; 121 | } 122 | 123 | ``` 124 | 125 | 위의 getRowMapper메서드를 Utiltity 클래스 같은 곳에 넣어두고 `BeanPropertyRowMapper.newInstance(...)` 메서드 대신에 호출해서 쓰면 됩니다. 126 | 127 | 128 | #### ColumnMapRowMapper 129 | `ColumnMapRowMapper`은 `ResultSet`을 `java.util.Map`으로 반환합니다. 앞선 예제에서 'Seller' 타입 대신에 `java.util.Map`으로 변환을 하려면 다음과 같이 코드를 씁니다. 130 | 131 | ```java 132 | private RowMapper> sellerMapper = new ColumnMapRowMapper(); 133 | 134 | public Map findById(Integer id) { 135 | Map params = Collections.singletonMap("id", id); 136 | return db.queryForObject(SELECT_BY_ID, params, sellerMapper); 137 | } 138 | ``` 139 | 140 | #### 수동변환 141 | 자동변환을 할 수 없다면 `RowMapper`를 직접 구현합니다. `RowMapper`는 메서드가 1개인 인터페이스이기 때문에 Java 8에서는 람다표현식으로 간단히 선언할 수 있습니다. 142 | 143 | ```java 144 | RowMapper productMapper = (rs, rowNum) -> { 145 | Product product = new Product(); 146 | product.setId(rs.getInt("id")); 147 | product.setName(rs.getString("name")); 148 | product.setDescription(rs.getString("desc")); 149 | return product; 150 | }; 151 | ``` 152 | 153 | ### SqlParameterSource : 쿼리의 파라미터 지정 154 | 앞선 예제에서는 SQL에 `:id`와 같은 이름이 붙여진 파라미터(named parameter)가 포함되어 있습니다. `SqlParmameterSource` 인터페이스는 그런 파라미터에 값을 지정하는 역할을 합니다. 앞선 예제에서는 `java.util.Map`으로 파리미터에 값을 지정했었습니다. 155 | 156 | #### BeanPropertySqlParameterSource 157 | 기본 구현체인 `BeanPropertySqlParameterSource`은 getter/setter가 있는 bean 객체로부터 파라미터를 추줄합니다. 158 | 159 | 아래와 같은 Update 구문을 실행할 때를 예를 들어보겠습니다. 160 | 161 | ``` 162 | public class SellerSqls { 163 | public static final String UPDATE = """ 164 | UPDATE seller \n 165 | SET name = :name, 166 | tel_no = :telNo, 167 | address = :address, 168 | homepage = :homepage 169 | WHERE id = :id 170 | """; 171 | ``` 172 | 173 | 파라미터인 `:name`, `:telNo` 등은 Seller 객체의 속성명과 동일합니다. 이럴 때는 `BeanPropertySqlParameterSource`을 활용하면 파리미터 이름과 대응되는 getter 메서드를 호출하여 값을 전달하게 됩니다. 174 | 175 | ```java 176 | public int update(Seller seller) { 177 | SqlParameterSource params = new BeanPropertySqlParameterSource(seller); 178 | return jdbc.update(SellerSqls.UPDATE, params); 179 | } 180 | ``` 181 | 182 | `BeanPropertySqlParameterSource`의 타입변환 전략이 충분하지 않은 경우도 있습니다. 예를 들면 Mysql에서 DB컬럼이 timestamp 타입일때 이에 대응된는 java객체의 필드가 ZonedDateTime로 선언되었을 경우입니다. `BeanPropertySqlParameterSource`의 기본적인 동작으로는 ZonedDateTime-> timstamp 변환이 되지 않습니다. 그런 때에는 `BeanPropertySqlParameterSource`를 상속한 클래스를 만들어서 문제를 해결할 수 있습니다. 183 | 184 | 185 | ```java 186 | static class ExtendedSqlParameterSource extends BeanPropertySqlParameterSource { 187 | public BridgeSqlParameterSource(Object object) { 188 | super(object); 189 | } 190 | 191 | @Override 192 | public Object getValue(String paramName) throws IllegalArgumentException { 193 | Object value = super.getValue(paramName); 194 | if ( value instanceof ZonedDateTime) { 195 | value = Timestamp.from(((ZonedDateTime) value).toInstant() ); 196 | } 197 | return value; 198 | } 199 | } 200 | ``` 201 | 202 | #### MapSqlParameterSource 203 | `MapSqlParameterSource`는 이름처럼 Map과 비슷한 형식으로 파라미터를 지정할 때 쓸 수 있습니다. `NamedParameterJdbcTemplate`에서는 직접 Map을 파라미터로 받는 메서드가 많기에 `MapSqlParameterSource`를 쓰지 않고 Map으로 바로 파라미터를 넘길 수도 있습니다. `MapSqlParameterSource` 클래스를 사용하면 메서드 체인 형식으로 파라미터를 정의할수 있는 장점이 있기는 합니다. 204 | 205 | ```java 206 | SqlParameterSource params = new MapSqlParameterSource() 207 | .addValue("name", "판매자1") 208 | .addValue("address", "마포구 용강동"); 209 | ``` 210 | 211 | ### SimpleJdbcInsert : INSERT 구문을 자동 생성 212 | `SimpleJdbcInsert` 클래스를 활용하면 직접 INSERT 구문을 쓰지 않고도 DB에 데이터를 저장할 수 있습니다. DB 컬럼명과 객체의 속성명이 일치한다면 아래와 같은 단순한 코드로 DB에 데이터 1건을 입력할 수 있습니다. 213 | 214 | ```java 215 | SimpleJdbcInsertOperations insertion = new SimpleJdbcInsert(dataSource).withTableName("seller") 216 | SqlParameterSource params = new BeanPropertySqlParameterSource(seller); 217 | insertion.execute(params); 218 | ``` 219 | 220 | `BeanPropertyRowMapper`로 마찬가지로 이때 DB컬럼명의 snake_case는 java객체에서는 caseCase로 자동으로 바꾸어줍니다. 221 | 222 | 데이터를 입력하는 시점에 DB에서 값을 증가시켜서 자동으로 PK가 결정되는 경우가 있습니다. 예를 들면 DB스키마가 아래와 같을 경우입니다. 223 | 224 | ```sql 225 | CREATE TABLE seller ( 226 | id INT IDENTITY NOT NULL PRIMARY KEY AUTO_INCREMENT, 227 | name VARCHAR(20) , 228 | tel_no VARCHAR(50), 229 | address VARCHAR(255), 230 | homepage VARCHAR(255) 231 | ); 232 | 233 | ``` 234 | 235 | 그런 경우에는 `usingGeneratedKeyColumns()`, `executeAndReturnKey()` 메서드를 활용하면 됩니다. 236 | 237 | ```java 238 | SimpleJdbcInsertOperations insertion = new SimpleJdbcInsert(dataSource) 239 | .withTableName("seller") 240 | .usingGeneratedKeyColumns("id"); 241 | SqlParameterSource params = new BeanPropertySqlParameterSource(seller); 242 | Integer id = insertion.executeAndReturnKey(params).intValue(); 243 | ``` 244 | 245 | 로그로 어떤 SQL이 실행되었는지 확인할 수 있습니다. `org.springframework.jdbc` 패키지의 로그레벨을 'DEBUG'로 설정하면 아래와 같이 실행된 쿼리가 나옵니다. 246 | 247 | ``` 248 | 2016-01-11 06:22:54.551 DEBUG 300 --- [ main] o.s.jdbc.core.simple.SimpleJdbcInsert : 249 | The following parameters are used for call INSERT INTO product (NAME, DESC, PRICE, SELLER_ID, REG_TIME) VALUES(?, ?, ?, ?, ?) with: [키보드, 좋은 상품, 130000, null, 20160111062254] 250 | ``` 251 | 252 | `SimpleJdbcInsert`의 `excute()`, `executeAndReturnKey()` 메서드는 멀티스레드에서 접근해도 안전합니다. 따라서 아래와 같이 클래스의 멤버변수로 지정해도 됩니다. 253 | 254 | ```java 255 | public class SellerRepository { 256 | private SimpleJdbcInsertOperations sellerInsertion; 257 | 258 | @Autowired 259 | public SellerRepository(DataSource dataSource) { 260 | this.sellerInsertion = new SimpleJdbcInsert(dataSource) 261 | .withTableName("seller") 262 | .usingGeneratedKeyColumns("id"); 263 | } 264 | ``` 265 | 266 | 만약 DB컬럼명과 클래스의 속성명이 자동으로 매핑될수 없다면 `java.util.Map`이나 `MapSqlParameterSource`을 이용해서 수동으로 선언할수 있습니다. Map을 쓰는 예제는 아래와 같습니다. 267 | 268 | ```java 269 | public Integer create(Product product) { 270 | Map params = mapColumns(product); 271 | return productInsertion.executeAndReturnKey(params).intValue(); 272 | } 273 | 274 | private Map mapColumns(Product product) { 275 | Map params = new HashMap<>(); 276 | ... 277 | params.put("desc", product.getDescription()); 278 | ... 279 | return params; 280 | } 281 | 282 | ``` 283 | 284 | 참고로 `SimpleJdbcInsert.execute()`를 호출할 때 `MapSqlParameterSource`를 파라미터로 넘기면 `BeanPropertySqlParameterSource`처럼 camelCase와 snake_case 간의 자동변환이 이루어집니다. 285 | 286 | 예를 들어 DB컬럼명이 'seller_id'로 되어 있더라도 아래와 같이 'sellerId'를 파리미터의 이름으로 지정할 수 있습니다. 287 | 288 | ````java 289 | SqlParameterSource params = new MapSqlParameterSource() 290 | .addValue("sellerId",1) 291 | .addValue("address", "마포구 용강동"); 292 | ```` 293 | -------------------------------------------------------------------------------- /spring-jdbc-extensions.md: -------------------------------------------------------------------------------- 1 | ## Spring JDBC 확장 라이브러리 2 | 3 | ### SimpleJdbcUpdate : Update 구문을 자동 생성 4 | `SimpleJdbcUpdate`는 Update 구문을 자동으로 생성해줍니다. Insert 구문을 자동으로 만들어주는 `SimpleJdbcInsert`와 유사합니다. 5 | 6 | 사용법은 https://github.com/florentp/spring-simplejdbcupdate 을 참조하시기 랍니다. 7 | 8 | 이 라이브러리는 Spring 4.3버전에 포함될 예정이였다가 현재 보류 중에 있습니다. 9 | 10 | - 관련 이슈 : [SPR-4691](https://jira.spring.io/browse/SPR-4691) 11 | - 관련 Pull Request : https://github.com/spring-projects/spring-framework/pull/1075 12 | 13 | ### OneToManyResultSetExtractor : 1개의 SELECT문에서 1대 다 관계의 객체 추출 14 | [OneToManyResultSetExtractor](http://docs.spring.io/spring-data/jdbc/docs/current/api/org/springframework/data/jdbc/core/OneToManyResultSetExtractor.html)는 [Spring Data JDBC Extensions](http://projects.spring.io/spring-data-jdbc-ext/) 라는 별도의 모듈에서 제공되는 클래스입니다. '1 대 다' 관계의 객체를 조회할 때 쿼리 호출 횟수를 줄일 수 있습니다. 15 | 16 | 아래와 같이 `Seller`와 `Product`객체가 1대다 관계를 가지고 있는 경우를 예로 들어보겠습니다. 17 | 18 | ```java 19 | public class Seller { 20 | private Integer id; 21 | .... 22 | private List productList; 23 | 24 | public void addProduct(Product product) { 25 | if (productList == null) { 26 | productList = new ArrayList<>(); 27 | } 28 | productList.add(product); 29 | } 30 | ``` 31 | 32 | 위의 `Seller` 객체에 `productList`필드를 채워서 조회하기 위해서는 'seller 테이블'과 'product 테이블'를 각각 조회하는 쿼리를 호출해야 합니다. [연관 관계 지연 로딩 기법](lazy-loading.md)을 이용하면 불필요한 호출을 막을 수는 있습니다. 그러나 다수의 Seller를 productList와 함께 조회하는 화면이 있다면 지연 로딩도 효과적이지 않습니다. 그럴 때는 Seller 건수 만큼 Product를 조회하는 SQL이 실행됩니다. 이런 문제를 N+1 쿼리라고 부릅니다. 33 | 34 | N+1 쿼리를 효율하기 위해서 1대 다에서 다 쪽인 product의 건수만큼 데이터를 조회하는 SQL을 아래와 같이 작성할 수 있습니다. 35 | 36 | ```groovy 37 | public class SellerSqls { 38 | public static final String SELECT_BY_NAME_WITH_PRODUCT = """ 39 | SELECT 40 | S.id, S.name, S.tel_no, S.address, S.homepage, 41 | P.id AS product_id, P.name AS product_name, P.price, P.desc, P.seller_id, P.reg_time 42 | FROM seller S 43 | LEFT OUTER JOIN product P ON P.seller_id = S.id 44 | WHERE S.name = :name 45 | """; 46 | ... 47 | ``` 48 | 그런데 위와 같은 쿼리를 다시 `Seller` 객체에 매핑을 할 때 어려움이 생깁니다. `Seller` 1 건을 조회하고자 해도 연관된 `Product`가 2건이라면, 위의 쿼리는 2건을 반홥합니다. 49 | 50 | 'OneToManyResultSetExtractor'는 이런 상황에서 '1 대 다' 관계에 맞춰서 객체를 매핑합니다. 아래와 같이 `OneToManyResultSetExtractor`를 상속해서 Primary Key 등의 연관관계를 유추할 수 있는 정보들을 지정합니다. 51 | 52 | ```java 53 | public class SellerProductExtractor extends OneToManyResultSetExtractor { 54 | 55 | public SellerProductExtractor(RowMapper rootMapper, RowMapper childMapper, 56 | org.springframework.data.jdbc.core.OneToManyResultSetExtractor.ExpectedResults expectedResults) { 57 | super(rootMapper, childMapper, expectedResults); 58 | } 59 | 60 | @Override 61 | protected Integer mapPrimaryKey(ResultSet rs) throws SQLException { 62 | return rs.getInt("id"); 63 | } 64 | 65 | @Override 66 | protected Integer mapForeignKey(ResultSet rs) throws SQLException { 67 | return rs.getInt("seller_id"); 68 | } 69 | 70 | @Override 71 | protected void addChild(Seller seller, Product product) { 72 | seller.addProduct(product); 73 | } 74 | } 75 | ``` 76 | 77 | `OneToManyResultSetExtractor`는 Spring JDBC의 [ResultSetExtractor](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/ResultSetExtractor.html 78 | ) 인터페이스의 구현체입니다. `NamedParameterJdbcTemplate`, `JdbcTemplate`의 `query()`메서드는 이 `ResultSetExtractor`을 파라미터로 받아줍니다. 앞선 예제의 `SellerProductExtractor`는 아래와 같이 활용할 수 있습니다. 79 | 80 | ```java 81 | public List findByNameWithProduct(String name) { 82 | Map params = Collections.singletonMap("name", name); 83 | 84 | ResultSetExtractor> extractor = 85 | new SellerProductExtractor(sellerMapper, productMapper, ExpectedResults.ONE_AND_ONLY_ONE); 86 | return jdbc.query(SellerSqls.SELECT_BY_NAME_WITH_PRODUCT, params, extractor); 87 | } 88 | ``` 89 | 90 | 참고로 `OneToManyResultSetExtractor`의 객체 매핑 방식은 JPA의 'fetch join'과도 유사합니다. 91 | -------------------------------------------------------------------------------- /sql-management.md: -------------------------------------------------------------------------------- 1 | ## SQL 관리 방안 2 | Spring JDBC의 `NamedParameterJdbcTemplate`을 쓸 때 SQL관리를 하는 방법을 정리해봤습니다. 3 | 4 | ### (추천) 다른 JVM 언어의 Multiline String 활용 5 | Groovy, Scala, Kotlin, xTend, Ceylon 등의 JVM 언어에는 여러 줄에 걸친 문자열 선언을 하는 문법이 있습니다. 이 중 Groovy를 활용하는 것을 가장 권장합니다. Java와 동일한 문법이 가장 많이 지원되고, Maven, IntelliJ, Maven, Gradle 등의 Plugin들도 안정되었기 때문입니다. 6 | 7 | 아래와 같이 따옴표 세 개 문법만 쓰고, 나머지는 Java와 동일한 문법으로 사용할 수도 있습니다. 8 | 9 | ```groovy 10 | // 정적 쿼리 11 | public staic final String DELETE_BY_ID = """ 12 | DELETE FROM seller 13 | WHERE id = :id 14 | """; 15 | 16 | // 동적 쿼리 17 | public static String selectByCondition(Seller seller) { 18 | return """ 19 | SELECT id, name, tel_no, address, homepage 20 | FROM seller 21 | """ + 22 | whereAnd ( 23 | notEmpty(seller.getName(), "name = :name"), 24 | notEmpty(seller.getAddress(), "address = :address"), 25 | notEmpty(seller.getTelNo(), "tel_no = :telNo") 26 | ); 27 | } 28 | private static String notEmpty(String param, String condition) { 29 | return StringUtils.isEmpty(param)? null: condition; 30 | } 31 | 32 | private static String whereAnd(String ... conditions) { 33 | List finalCond = conditions.findAll({it != null}); 34 | Assert.notEmpty(finalCond); 35 | return "WHERE " + finalCond.join("\nAND "); 36 | } 37 | ``` 38 | 39 | Java에도 여러줄에 걸친 문자열을 쓸 수 있는 [JEP 326: Raw String Literals](http://openjdk.java.net/jeps/326) 이라는 스펙이 제안되어 있습니다. 2018년 9월에 릴리즈되는 JDK11에 이 스펙이 포함될 가능성이 높습니다. 그 시점에는 Groovy에 대한 의존을 제거할 수도 있습니다. 다만 JEP 326에 제안된 문법은 Groovy와 같은 따옴표3개가 아닌 "`"(backticks) 입니다. 따옴표 새 개를 backticks으로 'Replace all' 한후에 아래 명령어로 일괄적으로 Groovy 파일을 Java로 전환하면 됩니다. 40 | 41 | ``` 42 | find . -name '*.groovy' -print0 | xargs -0 rename 's/.groovy$/.java/' 43 | ``` 44 | 45 | - 장점 46 | - 컴파일 타임의 검증 47 | - 쿼리 ID가 자동완성 되고 오타를 칠 수가 없음 48 | - 쿼리 문자열로 IDE에서 Ctrl + 클릭으로 바로 이동 가능 49 | - XML 파싱 비용이 없음 50 | - 다른 도구에서 SQL 쿼리를 복사&붙여넣기에 편함 51 | - 학습 비용이 거의 없음. ( `"""` 문법만 알면 됨) 52 | - Java 조건/반복문으로 동적 쿼리를 만들 수 있음. 53 | - 테스트 코드 커버리지를 측정 가능 54 | - 단점 55 | - Eclipse와 IntelliJ Community Edition에서는 별도의 IDE Plugin 설치 필요 56 | 57 | IDE와 빌드도구에서 설정하는 방법은 [Java 프로젝트에 Groovy를 섞어 쓰는 설정](groovy-config.md)을 참조하실 수 있습니다. 58 | 59 | ### Spring의 속성 관리 기능 활용 60 | Spring의 속성관리 기능으로 SQL파일을 따로 빼서 선언할 수도 있습니다. 61 | 62 | #### Properties XML 선언활용 63 | 보통 속성관리에는 .properties 파일을 쓰지만 SQL을 편집하기에는 XML파일이 SQL편집에는 더 편리합니다. 아래와 같은 형식으로 선언을 합니다. 64 | 65 | ```xml 66 | 67 | 68 | 69 | 70 | SELECT id, name, address, email 71 | FROM seller 72 | WHERE id = :id 73 | 74 | 75 | DELETE FROM seller 76 | WHERE id = :id 77 | 78 | 79 | ``` 80 | 81 | 위의 파일은 Spring 설정에서 `@PropertySource("classpath:/sellerSqls.xml")` , `` 같은 선언으로 로딩할 수 있습니다. 82 | 83 | 쿼리 문자열은 아래와 같이 `@Value("#{key}")` 선언으로 참조합니다. 84 | 85 | ```java 86 | @Value("${seller.selectById}") 87 | private String selectById; 88 | ``` 89 | 90 | #### `` 활용 91 | Spring의 ApplicationContext 설정 파일에 아래와 같이 쿼리를 선언할 수 있습니다. 92 | 93 | ```xml 94 | 95 | 96 | SELECT id, name, address, email 97 | FROM seller 98 | WHERE id = :id 99 | 100 | 101 | DELETE FROM seller 102 | WHERE id = :id 103 | 104 | 105 | ``` 106 | 107 | 또는 ``와 같이 다른 .propeties 파일이나 propeties XML파일을 지정할 수도 있습니다. 108 | 109 | 참조하는 쪽에서는 `@Value("#{beanId.key}")`로 선언을 합니다. 110 | 111 | ```java 112 | @Value("#{sellerSqls.selectById}") 113 | private String selectById; 114 | ``` 115 | 116 | Spring 속성 관리 기능을 활용할 때의 장단점은 아래와 같습니다. 117 | 118 | - 장점 119 | - 라이브러리 의존성을 더 추가할 필요없음. 120 | - SQL 쿼리를 복사&붙여넣기에 편함 121 | - 쿼리 ID가 틀리면 어플리케이션 시작 시점에 알려주게 할 수 있음. 122 | - 단점 123 | - 쿼리 ID가 틀려도 컴파일 타임에 검증되지 않음. 124 | - 동적 쿼리는 XML 안에 선언할 수 없음 125 | - `@Value`로 참조된 쿼리 문자열은 IDE에서 Ctrl + 클릭으로 바로 이동할 수 없음 126 | 127 | ### 템플릿 엔진 도입 128 | HTML 파일을 만들 때 템플릿 엔진을 쓰는 것과 유사한 방식으로 SQL을 관리할 수도 있습니다. Freemarker를 활용한 사례는 http://kwon37xi.egloos.com/7048211 을 참조하실 수 있습니다. 이 글에서는 SQL 전용 템플릿 엔진 2가지를 소개합니다. 129 | 130 | 131 | #### ElSql 132 | EqSql의 자체적인 문법으로 Query의 Id와 조건문 등을 기술할수 있습니다. 133 | 134 | ``` 135 | -- ========================================================================== 136 | @NAME(deleteSeller) 137 | DELETE FROM seller 138 | WHERE id = :id 139 | 140 | -- ========================================================================== 141 | @NAME(selectSeller) 142 | SELECT id, name, address, email 143 | FROM seller 144 | @WHERE oid = :doc_oid 145 | @AND(:name) 146 | name = :name 147 | @AND(:address) 148 | address = :address 149 | ``` 150 | 151 | 자세한 사용법은 https://github.com/OpenGamma/ElSql 을 참조하시기 바랍니다. 152 | 153 | #### JIRM-Core 154 | JIRM-Core에서는 별도의 파일로 선언된 SQL파일을 String으로 읽어오는 기능을 제공합니다. 155 | 156 | ```java 157 | String sql = PlainSql.fromResource(TestBean.class, "select-test-bean.sql").getSql(); 158 | 159 | ``` 160 | 161 | JIRM-Core는 자체적으로도 Named parameter를 지원합니다. 그렇지만, NamedParameterJdbcTemplate을 함께 쓴다면 굳이 그 기능이 필요하지는 않습니다. 162 | 163 | 자세한 사용법은 https://github.com/agentgt/jirm/tree/master/jirm-core 164 | 165 | 템플릿 엔진을 활용할 때의 장단점은 아래와 같습니다. 166 | 167 | - 장점 168 | - SQL 쿼리를 복사&붙여넣기에 편함. 169 | - 쿼리 ID가 틀리면 어플리케이션 시작 시점에 알려주게 할 수 있음. 170 | - (ELSql) 동적 쿼리까지 템플릿 파일 안에 선언할 수 있음. 171 | 172 | - 단점 173 | - 쿼리 ID가 틀려도 컴파일 타임에 검증되지 않음. 174 | - 동적 쿼리를 만드는 표현식이 틀려도 컴파일 타임에 검증되지 않음 175 | - IDE 안에서 Ctrl + 클릭으로 바로 이동을 할 수 없음. 176 | 177 | 178 | ### SQL Builder 도입 179 | MyBatis나 JOOQ는 Spring JDBC와 마찬가지로 SQL 쿼리를 실행하는 프레임워크입니다. 그런데 이 프레임워크들의 일부 기능인 SQL Builder만을 활용할 수 있습니다. SQL 쿼리가 단긴 String 문자열은 다른 프레임워크의 기능으로 생성하고, 쿼리를 실행하는 역할은 Spring JDBC에 맡기는 방식입니다. 180 | 181 | 182 | #### MyBatis의 SQL 클래스 183 | MyBatis에서 제공하는 SQL클래스도 SQL구문을 Java의 String으로 생성하는것을 도와줍니다. 184 | 185 | ```java 186 | // 정적쿼리 187 | public String deleteById() { 188 | return new SQL() 189 | .DELETE_FROM("seller"); 190 | .WHERE("id = :id"); 191 | .toString(); 192 | } 193 | 194 | 195 | // 동적쿼리 196 | public String selectByCondition(Seller seller) { 197 | return new SQL() {{ 198 | SELECT("name, address"); 199 | FROM("seller"); 200 | if (StringUtils.isNotEmpty(seller.getName())) { 201 | WHERE("name = #{name}"); 202 | } 203 | if (StringUtils.isNotEmpty(seller.getAddress())) { 204 | WHERE("address = #{address}"); 205 | } 206 | }}.toString(); 207 | } 208 | ``` 209 | 210 | 자세한 사용법은 http://www.mybatis.org/mybatis-3/ko/statement-builders.html 을 참고합니다. 위의 동적쿼리에서 사용한 내부 익명 클래스 문법이 클래스 로더에 부담이 된다는 주장도 있습니다. 그 주장에 대해서는 https://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/ 을 참조하시기 바랍니다. MyBatis의 라이벌기술이라고 할 수 있는 JOOQ의 블로그에 올라온 글이라는 점이 흥미롭습니다. 211 | 212 | ### JOOQ 213 | JOOQ에서도 아래와 같이 SQL 구문을 String으로 생성할 수 있습니다. 214 | 215 | ```java 216 | public String selectById() { 217 | return create.select(field("name"), field("address") 218 | .from(table("seller")) 219 | .where(field("id").equal(":id")) 220 | .getSQL(); 221 | } 222 | ``` 223 | 224 | 자세한 소개는 https://www.jooq.org/doc/3.9/manual/getting-started/use-cases/jooq-as-a-standalone-sql-builder/ 을 참조하시기 바랍니다. 225 | 226 | MyBatis나 Jooq의 SQL Builder를 도입할 때의 장단점은 아래와 같습니다. 227 | 228 | - 장점 229 | - 컴파일 타임의 검증 230 | - 쿼리를 생성하는 메서드는 자동 완성 되고 오타를 칠 수가 없음 231 | - `SELECT`, `FROM` 같은 SQL 예약어의 오타까지 예방해 줌 232 | - 쿼리를 생성하는 메서드로 IDE에서 Ctrl + 클릭으로 바로 이동 가능 233 | - Java 조건/반복문으로 동적쿼리를 만들 수 있음 234 | - 단점 235 | - Native SQL을 다른 툴에서 바로 복사&붙여넣기 하기가 불편함 236 | 237 | 위와 같은 라이브러리를 쓰지 않더라도 StringBuilder를 이용한 SqlBuilder를 직접 만드는 것은 어렵지 않습니다. 아래 사례를 참조하실 수 있습니다. 238 | 239 | - [동적 Native SQL 생성 어떻게 할까 - 순수 Java 코드로 생성하기 ](http://kwon37xi.egloos.com/7092965) 240 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/AppConfig.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.apache.commons.dbcp2.BasicDataSource; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.PropertySource; 11 | import org.springframework.core.io.ClassPathResource; 12 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 13 | import org.springframework.jdbc.datasource.init.DataSourceInitializer; 14 | import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; 15 | import org.springframework.transaction.PlatformTransactionManager; 16 | 17 | @Configuration 18 | @ComponentScan 19 | @PropertySource("db.properties") 20 | public class AppConfig { 21 | 22 | @Value("${datasource.driver-class-name}") 23 | private String driverClassName; 24 | 25 | @Value("${datasource.url}") 26 | private String url; 27 | 28 | @Value("${datasource.username}") 29 | private String username; 30 | 31 | @Value("${datasource.password}") 32 | private String password; 33 | 34 | @Bean 35 | public DataSource dataSource() { 36 | BasicDataSource dataSource = new BasicDataSource(); 37 | dataSource.setDriverClassName(driverClassName); 38 | dataSource.setUrl(url); 39 | dataSource.setUsername(username); 40 | dataSource.setPassword(password); 41 | return dataSource; 42 | } 43 | 44 | @Bean 45 | public PlatformTransactionManager transactionManger() { 46 | return new DataSourceTransactionManager(dataSource()); 47 | } 48 | 49 | @Bean 50 | public DataSourceInitializer dataSourceInitializer(DataSource dataSource) { 51 | ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); 52 | databasePopulator.addScript(new ClassPathResource("schema.sql")); 53 | databasePopulator.setContinueOnError(true); 54 | 55 | DataSourceInitializer initializer = new DataSourceInitializer(); 56 | initializer.setDataSource(dataSource); 57 | initializer.setDatabasePopulator(databasePopulator); 58 | return initializer; 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/domain/LazySeller.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.domain; 2 | 3 | import java.util.List; 4 | import java.util.function.Supplier; 5 | 6 | import org.springframework.beans.BeanUtils; 7 | 8 | public class LazySeller extends Seller { 9 | private Supplier> productLoader; 10 | 11 | public LazySeller(Seller seller, Supplier> productLoader) { 12 | this.productLoader = productLoader; 13 | BeanUtils.copyProperties(seller, this); 14 | } 15 | 16 | @Override 17 | public List getProductList() { 18 | if(super.getProductList() != null) { 19 | return super.getProductList(); 20 | } 21 | 22 | List productList = productLoader.get(); 23 | super.setProductList(productList); 24 | return productList; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/domain/Product.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class Product { 6 | private Integer id; 7 | private String name; 8 | private Long price; 9 | private String description; 10 | private LocalDateTime registeredTime; 11 | private Seller seller = new Seller(); 12 | 13 | public Integer getId() { 14 | return id; 15 | } 16 | 17 | public void setId(Integer id) { 18 | this.id = id; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | 29 | public Long getPrice() { 30 | return price; 31 | } 32 | 33 | public void setPrice(Long price) { 34 | this.price = price; 35 | } 36 | 37 | public String getDescription() { 38 | return description; 39 | } 40 | 41 | public void setDescription(String description) { 42 | this.description = description; 43 | } 44 | 45 | public LocalDateTime getRegisteredTime() { 46 | return registeredTime; 47 | } 48 | 49 | public void setRegisteredTime(LocalDateTime registeredTime) { 50 | this.registeredTime = registeredTime; 51 | } 52 | 53 | public Seller getSeller() { 54 | return seller; 55 | } 56 | 57 | public void setSeller(Seller seller) { 58 | this.seller = seller; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "Product [id=" + id + ", name=" + name + ", price=" + price 64 | + ", description=" + description + ", registeredTime=" 65 | + registeredTime + "]"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/domain/Seller.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Seller { 7 | private Integer id; 8 | private String name; 9 | private String address; 10 | private String telNo; 11 | private String homepage; 12 | private List productList; 13 | 14 | public Integer getId() { 15 | return id; 16 | } 17 | public void setId(Integer id) { 18 | this.id = id; 19 | } 20 | public String getName() { 21 | return name; 22 | } 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | public String getAddress() { 27 | return address; 28 | } 29 | public void setAddress(String address) { 30 | this.address = address; 31 | } 32 | public String getTelNo() { 33 | return telNo; 34 | } 35 | public void setTelNo(String telNo) { 36 | this.telNo = telNo; 37 | } 38 | public String getHomepage() { 39 | return homepage; 40 | } 41 | public void setHomepage(String homepage) { 42 | this.homepage = homepage; 43 | } 44 | 45 | public List getProductList() { 46 | return productList; 47 | } 48 | 49 | public void setProductList(List productList) { 50 | this.productList = productList; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return "Seller [id=" + id + ", name=" + name + ", address=" + address 56 | + ", telNo=" + telNo + ", homepage=" + homepage + "]"; 57 | } 58 | public void addProduct(Product product) { 59 | if (productList == null) { 60 | productList = new ArrayList<>(); 61 | } 62 | productList.add(product); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/persistence/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.persistence; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import javax.sql.DataSource; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.jdbc.core.RowMapper; 13 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; 14 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 15 | import org.springframework.jdbc.core.simple.SimpleJdbcInsert; 16 | import org.springframework.jdbc.core.simple.SimpleJdbcInsertOperations; 17 | import org.springframework.stereotype.Repository; 18 | 19 | import net.benelog.spring.domain.Product; 20 | import net.benelog.spring.domain.Seller; 21 | 22 | @Repository 23 | public class ProductRepository { 24 | private NamedParameterJdbcOperations db; 25 | private SimpleJdbcInsertOperations productInsertion; 26 | private RowMapper productMapper; 27 | private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); 28 | 29 | @Autowired 30 | public ProductRepository(DataSource dataSource) { 31 | this.db = new NamedParameterJdbcTemplate(dataSource); 32 | 33 | this.productInsertion = new SimpleJdbcInsert(dataSource) 34 | .withTableName("product") 35 | .usingGeneratedKeyColumns("id"); 36 | 37 | this.productMapper = (rs, rowNum) -> { 38 | Product product = new Product(); 39 | product.setId(rs.getInt("product_id")); // PK는 필수값. getInt를 써도 문제는 없음 40 | product.setName(rs.getString("product_name")); 41 | product.setPrice((Long) rs.getObject("price")); 42 | product.setDescription(rs.getString("desc")); 43 | LocalDateTime regTime = LocalDateTime.parse(rs.getString("reg_time"), formatter); 44 | product.setRegisteredTime(regTime); 45 | 46 | Seller seller = new Seller(); 47 | seller.setId((Integer)rs.getObject("seller_id")); 48 | seller.setName(rs.getString("seller_name")); 49 | seller.setHomepage(rs.getString("homepage")); 50 | seller.setAddress(rs.getString("address")); 51 | product.setSeller(seller); 52 | 53 | return product; 54 | }; 55 | } 56 | 57 | private Map mapColumns(Product product) { 58 | Map params = new HashMap<>(); 59 | params.put("id", product.getId()); 60 | params.put("name", product.getName()); 61 | params.put("desc", product.getDescription()); 62 | params.put("price", product.getPrice()); 63 | params.put("seller_id", product.getSeller().getId()); 64 | params.put("reg_time", product.getRegisteredTime().format(formatter)); 65 | return params; 66 | } 67 | 68 | public Integer create(Product product) { 69 | Map params = mapColumns(product); 70 | return productInsertion.executeAndReturnKey(params).intValue(); 71 | } 72 | 73 | public Product findById(Integer id) { 74 | Map params = Collections.singletonMap("id", id); 75 | return db.queryForObject(ProductSqls.SELECT_BY_ID, params, productMapper); 76 | } 77 | 78 | public boolean update(Product product) { 79 | Map params = mapColumns(product); 80 | int affected = db.update(ProductSqls.UPDATE, params); 81 | return affected == 1; 82 | } 83 | 84 | public boolean delete(Integer id) { 85 | Map params = Collections.singletonMap("id", id); 86 | int affected = db.update(ProductSqls.DELETE_BY_ID, params); 87 | return affected == 1; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/persistence/ProductSqls.groovy: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.persistence; 2 | 3 | public class ProductSqls { 4 | public static final String SELECT_BY_ID = """ 5 | SELECT 6 | P.id AS product_id, P.name AS product_name, P.price, P.desc, P.seller_id, P.reg_time, 7 | S.name AS seller_name, S.tel_no, S.address, S.homepage 8 | FROM product P LEFT OUTER JOIN seller S ON S.id = P.seller_id 9 | WHERE P.id = :id 10 | """; 11 | 12 | public static final String DELETE_BY_ID = """ 13 | DELETE FROM product 14 | WHERE id = :id 15 | """; 16 | 17 | public static final String UPDATE = """ 18 | UPDATE product 19 | SET name = :name, 20 | desc = :desc, 21 | price = :price, 22 | seller_id = :seller_id, 23 | reg_time = :reg_time 24 | WHERE id = :id 25 | """; 26 | 27 | public static final String SELECT_PRODUCT_LIST_BY_SELLER_ID = """ 28 | SELECT 29 | P.id AS product_id, P.name AS product_name, P.price, P.desc, P.seller_id, P.reg_time, 30 | S.name AS seller_name, S.tel_no, S.address, S.homepage 31 | FROM product P LEFT OUTER JOIN seller S ON S.id = P.seller_id 32 | WHERE P.seller_id = :seller_id 33 | """; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/persistence/SellerProductExtractor.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.persistence; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | import org.springframework.data.jdbc.core.OneToManyResultSetExtractor; 7 | import org.springframework.jdbc.core.RowMapper; 8 | 9 | import net.benelog.spring.domain.Product; 10 | import net.benelog.spring.domain.Seller; 11 | 12 | public class SellerProductExtractor extends OneToManyResultSetExtractor { 13 | 14 | public SellerProductExtractor(RowMapper rootMapper, RowMapper childMapper, 15 | org.springframework.data.jdbc.core.OneToManyResultSetExtractor.ExpectedResults expectedResults) { 16 | super(rootMapper, childMapper, expectedResults); 17 | } 18 | 19 | @Override 20 | protected Integer mapPrimaryKey(ResultSet rs) throws SQLException { 21 | return rs.getInt("id"); 22 | } 23 | 24 | @Override 25 | protected Integer mapForeignKey(ResultSet rs) throws SQLException { 26 | return rs.getInt("seller_id"); 27 | } 28 | 29 | @Override 30 | protected void addChild(Seller seller, Product product) { 31 | seller.addProduct(product); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/persistence/SellerRepository.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.persistence; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import javax.sql.DataSource; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.jdbc.core.OneToManyResultSetExtractor.ExpectedResults; 13 | import org.springframework.jdbc.core.BeanPropertyRowMapper; 14 | import org.springframework.jdbc.core.ResultSetExtractor; 15 | import org.springframework.jdbc.core.RowMapper; 16 | import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; 17 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; 18 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 19 | import org.springframework.jdbc.core.namedparam.SqlParameterSource; 20 | import org.springframework.jdbc.core.simple.SimpleJdbcInsert; 21 | import org.springframework.jdbc.core.simple.SimpleJdbcInsertOperations; 22 | import org.springframework.stereotype.Repository; 23 | 24 | import net.benelog.spring.domain.LazySeller; 25 | import net.benelog.spring.domain.Product; 26 | import net.benelog.spring.domain.Seller; 27 | 28 | @Repository 29 | public class SellerRepository { 30 | private NamedParameterJdbcOperations db; 31 | private SimpleJdbcInsertOperations sellerInsertion; 32 | private RowMapper sellerMapper = BeanPropertyRowMapper.newInstance(Seller.class); 33 | 34 | private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); 35 | private RowMapper productMapper = (rs, rowNum) -> { 36 | Product product = new Product(); 37 | product.setId(rs.getInt("product_id")); // 필수값 38 | product.setName(rs.getString("product_name")); 39 | product.setPrice(rs.getLong("price")); 40 | product.setDescription(rs.getString("desc")); 41 | LocalDateTime regTime = LocalDateTime.parse(rs.getString("reg_time"), formatter); 42 | product.setRegisteredTime(regTime); 43 | return product; 44 | }; 45 | 46 | @Autowired 47 | public SellerRepository(DataSource dataSource) { 48 | this.db = new NamedParameterJdbcTemplate(dataSource); 49 | 50 | this.sellerInsertion = new SimpleJdbcInsert(dataSource) 51 | .withTableName("seller") 52 | .usingGeneratedKeyColumns("id"); 53 | } 54 | 55 | public Integer create(Seller seller) { 56 | SqlParameterSource params = new BeanPropertySqlParameterSource(seller); 57 | return sellerInsertion.executeAndReturnKey(params). intValue(); 58 | } 59 | 60 | public Seller findById(Integer id) { 61 | Map params = Collections.singletonMap("id", id); 62 | return db.queryForObject(SellerSqls.SELECT_BY_ID, params, sellerMapper); 63 | } 64 | 65 | public List findByIdList(List idList) { 66 | Map params = Collections.singletonMap("idList", idList); 67 | return db.query(SellerSqls.SELECT_BY_ID_LIST, params, sellerMapper); 68 | } 69 | 70 | public Seller findByIdWithProduct(Integer id) { 71 | Map params = Collections.singletonMap("id", id); 72 | 73 | ResultSetExtractor> extractor = 74 | new SellerProductExtractor(sellerMapper, productMapper, ExpectedResults.ONE_AND_ONLY_ONE); 75 | return db.query(SellerSqls.SELECT_BY_ID_WITH_PRODUCT, params, extractor) 76 | .get(0); 77 | } 78 | 79 | public Seller findByIdWithLazyProduct(Integer id) { 80 | Seller seller = findById(id); 81 | Map params = Collections.singletonMap("seller_id", id); 82 | 83 | return new LazySeller( 84 | seller, 85 | () -> db.query(ProductSqls.SELECT_PRODUCT_LIST_BY_SELLER_ID, params, productMapper) 86 | ); 87 | } 88 | 89 | public List findBy(Seller condition) { 90 | SqlParameterSource params = new BeanPropertySqlParameterSource(condition); 91 | String sql = SellerSqls.selectByCondition(condition); 92 | return db.query(sql, params, sellerMapper); 93 | } 94 | 95 | public boolean update(Seller seller) { 96 | SqlParameterSource params = new BeanPropertySqlParameterSource(seller); 97 | int affected = db.update(SellerSqls.UPDATE, params); 98 | return affected == 1; 99 | } 100 | 101 | public boolean delete(Integer id) { 102 | Map params = Collections.singletonMap("id", id); 103 | int affected = db.update(SellerSqls.DELETE_BY_ID, params); 104 | return affected == 1; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/net/benelog/spring/persistence/SellerSqls.groovy: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.persistence; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | import org.springframework.util.Assert; 5 | 6 | import static org.apache.commons.lang.StringUtils.*; 7 | 8 | import net.benelog.spring.domain.Seller; 9 | 10 | public class SellerSqls { 11 | public static final String SELECT_BY_ID = """ 12 | SELECT id, name, tel_no, address, homepage 13 | FROM seller 14 | WHERE id = :id 15 | """; 16 | 17 | public static final String SELECT_BY_ID_LIST = """ 18 | SELECT id, name, tel_no, address, homepage 19 | FROM seller 20 | WHERE id IN (:idList) 21 | """; 22 | 23 | public static final String DELETE_BY_ID = """ 24 | DELETE FROM seller 25 | WHERE id = :id 26 | """; 27 | 28 | public static final String UPDATE = """ 29 | UPDATE seller \n 30 | SET name = :name, 31 | tel_no = :telNo, 32 | address = :address, 33 | homepage = :homepage 34 | WHERE id = :id 35 | """; 36 | 37 | public static final String ADDRESS_CONDITION = 38 | "AND address = :address \n"; 39 | 40 | public static final String NAME_CONDITION = 41 | "AND name = :name \n"; 42 | 43 | public static final String TEL_NO_CONDITION = 44 | "AND tel_no = :telNo \n"; 45 | 46 | 47 | public static String selectByCondition(Seller seller) { 48 | return """ 49 | SELECT id, name, tel_no, address, homepage 50 | FROM seller 51 | """ + 52 | whereAnd ( 53 | notEmpty(seller.getName(), "name = :name"), 54 | notEmpty(seller.getAddress(), "address = :address"), 55 | notEmpty(seller.getTelNo(), "tel_no = :telNo") 56 | ); 57 | } 58 | 59 | public static final String SELECT_BY_ID_WITH_PRODUCT = """ 60 | SELECT 61 | S.id, S.name, S.tel_no, S.address, S.homepage, 62 | P.id AS product_id, P.name AS product_name, P.price, P.desc, P.seller_id, P.reg_time 63 | FROM seller S 64 | LEFT OUTER JOIN product P ON P.seller_id = S.id 65 | WHERE S.id = :id 66 | """; 67 | 68 | private static String notEmpty(String param, String condition) { 69 | return StringUtils.isEmpty(param)? null: condition; 70 | } 71 | 72 | private static String whereAnd(String ... conditions) { 73 | List finalCond = conditions.findAll({it != null}); 74 | Assert.notEmpty(finalCond); 75 | return "WHERE " + finalCond.join("\nAND "); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/resources/db.properties: -------------------------------------------------------------------------------- 1 | datasource.driver-class-name=org.h2.Driver 2 | datasource.url=jdbc:h2:~/exam/db;AUTO_SERVER=TRUE 3 | datasource.username=sa 4 | datasource.password=sa -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE seller ( 2 | id INT IDENTITY NOT NULL PRIMARY KEY AUTO_INCREMENT, 3 | name VARCHAR(20) , 4 | tel_no VARCHAR(50), 5 | address VARCHAR(255), 6 | homepage VARCHAR(255) 7 | ); 8 | 9 | CREATE TABLE product ( 10 | id INT IDENTITY NOT NULL PRIMARY KEY AUTO_INCREMENT, 11 | name VARCHAR(50) , 12 | desc VARCHAR(200), 13 | price BIGINT, 14 | seller_id INT, 15 | reg_time VARCHAR(14) 16 | ); 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/net/benelog/spring/ProductIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.junit.Assert.assertThat; 5 | 6 | import java.time.LocalDateTime; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import net.benelog.spring.domain.Product; 17 | import net.benelog.spring.persistence.ProductRepository; 18 | 19 | @RunWith(SpringRunner.class) 20 | @ContextConfiguration(classes = AppConfig.class) 21 | public class ProductIntegrationTest extends AbstractTransactionalJUnit4SpringContextTests { 22 | 23 | @Autowired 24 | private ProductRepository repo; 25 | private Product product = new Product(); 26 | 27 | @Before 28 | public void setUp() { 29 | product.setPrice(130000L); 30 | product.setRegisteredTime(LocalDateTime.now()); 31 | product.setDescription("좋은 상품"); 32 | } 33 | 34 | @Test 35 | public void shouldBeCreatedAndFound() { 36 | // given 37 | product.setName("키보드"); 38 | 39 | // when 40 | Integer id = repo.create(product); 41 | 42 | // then 43 | Product found = repo.findById(id); 44 | assertThat(found.getName(), is("키보드")); 45 | } 46 | 47 | @Test 48 | public void shouldBeUpdated() { 49 | // given 50 | product.setName("키보드"); 51 | product.setRegisteredTime(LocalDateTime.now().minusDays(1)); 52 | 53 | Integer id = repo.create(product); 54 | 55 | // when 56 | product.setId(id); 57 | product.setName("무선 키보드"); 58 | product.setRegisteredTime(LocalDateTime.now()); 59 | boolean updated = repo.update(product); 60 | 61 | // then 62 | assertThat(updated, is(true)); 63 | Product found = repo.findById(id); 64 | assertThat(found.getName(), is("무선 키보드")); 65 | System.out.println(found); 66 | } 67 | 68 | @Test 69 | public void shouldBeDeleted() { 70 | // given 71 | Integer id = repo.create(product); 72 | 73 | // when 74 | boolean deleted = repo.delete(id); 75 | 76 | // then 77 | assertThat(deleted, is(true)); 78 | int countById = countRowsInTableWhere("product", "id = " +id); 79 | assertThat(countById, is(0)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/net/benelog/spring/SellerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.junit.Assert.assertThat; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.test.context.ContextConfiguration; 15 | import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | 18 | import net.benelog.spring.domain.Product; 19 | import net.benelog.spring.domain.Seller; 20 | import net.benelog.spring.persistence.ProductRepository; 21 | import net.benelog.spring.persistence.SellerRepository; 22 | 23 | @RunWith(SpringRunner.class) 24 | @ContextConfiguration(classes = AppConfig.class) 25 | public class SellerIntegrationTest extends AbstractTransactionalJUnit4SpringContextTests { 26 | 27 | @Autowired 28 | private SellerRepository repo; 29 | @Autowired 30 | private ProductRepository productRepo; 31 | private Seller seller = new Seller(); 32 | 33 | @Before 34 | public void setUp() { 35 | seller.setHomepage("http://blog.naver.com"); 36 | seller.setTelNo("010-1111-1111"); 37 | seller.setAddress("서울시 마포구 용강동"); 38 | } 39 | 40 | @Test 41 | public void shouldBeCreatedAndFound() { 42 | // given 43 | seller.setName("정유하"); 44 | 45 | // when 46 | Integer id = repo.create(seller); 47 | 48 | // then 49 | Seller found = repo.findById(id); 50 | assertThat(found.getName(), is("정유하")); 51 | } 52 | 53 | @Test 54 | public void shouldBeFoundByIdList() { 55 | // given 56 | Integer id1 = repo.create(seller); 57 | Integer id2 = repo.create(seller); 58 | 59 | // when 60 | List sellerList = repo.findByIdList(Arrays.asList(id1,id2)); 61 | 62 | // then 63 | assertThat(sellerList.size(), is(2)); 64 | } 65 | 66 | @Test 67 | public void shouldBeFoundByTelNo() { 68 | // given 69 | String telNo = "0101112123123123"; 70 | seller.setTelNo(telNo); 71 | repo.create(seller); 72 | 73 | // when 74 | Seller condition = new Seller(); 75 | condition.setTelNo(telNo); 76 | List selected = repo.findBy(condition); 77 | 78 | // then 79 | assertThat(selected.size(), is(1)); 80 | } 81 | 82 | @Test 83 | public void shouldBeFoundByIdWithProduct() { 84 | // given 85 | Integer id = insertSellerAndProduct(); 86 | 87 | // when 88 | Seller selected = repo.findByIdWithProduct(id); 89 | 90 | // then 91 | List productList = selected.getProductList(); 92 | assertThat(productList.size(), is(1)); 93 | } 94 | 95 | @Test 96 | public void shouldBeFoundByIdWithLazyProduct() { 97 | Integer id = insertSellerAndProduct(); 98 | 99 | // when 100 | Seller selected = repo.findByIdWithLazyProduct(id); 101 | 102 | // then 103 | List productList = selected.getProductList(); 104 | assertThat(productList.size(), is(1)); 105 | } 106 | 107 | @Test 108 | public void shouldBeUpdated() { 109 | // given 110 | seller.setName("정유하"); 111 | Integer id = repo.create(seller); 112 | 113 | // when 114 | seller.setId(id); 115 | seller.setName("정유하"); 116 | boolean updated = repo.update(seller); 117 | 118 | // then 119 | assertThat(updated, is(true)); 120 | Seller found = repo.findById(id); 121 | assertThat(found.getName(), is("정유하")); 122 | } 123 | 124 | @Test 125 | public void shouldBeDeleted() { 126 | // given 127 | Integer id = repo.create(seller); 128 | 129 | // when 130 | boolean deleted = repo.delete(id); 131 | 132 | // then 133 | assertThat(deleted, is(true)); 134 | int countById = countRowsInTableWhere("seller", "id = " +id); 135 | assertThat(countById, is(0)); 136 | } 137 | 138 | private Integer insertSellerAndProduct() { 139 | Integer id = repo.create(seller); 140 | seller.setId(id); 141 | Product product = new Product(); 142 | product.setPrice(130000L); 143 | product.setRegisteredTime(LocalDateTime.now()); 144 | product.setDescription("좋은 상품"); 145 | 146 | product.setSeller(seller); 147 | productRepo.create(product); 148 | return id; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/net/benelog/spring/persistence/SellerSqlsTest.java: -------------------------------------------------------------------------------- 1 | package net.benelog.spring.persistence; 2 | 3 | import org.junit.Test; 4 | 5 | import net.benelog.spring.domain.Seller; 6 | 7 | public class SellerSqlsTest { 8 | 9 | @Test 10 | public void selectByCondition() { 11 | Seller condition = new Seller(); 12 | condition.setName("jyh"); 13 | condition.setTelNo("010-2841-1383"); 14 | 15 | String sql = SellerSqls.selectByCondition(condition); 16 | System.out.println(sql); 17 | } 18 | 19 | @Test(expected = IllegalArgumentException.class) 20 | public void selectByConditionEmpty() { 21 | Seller condition = new Seller(); 22 | SellerSqls.selectByCondition(condition); 23 | } 24 | } 25 | --------------------------------------------------------------------------------