Exposed는 JetBrains에서 만든 Kotlin 언어 기반의 ORM 프레임워크입니다. Exposed는 두 가지 레벨의 데이터베이스 access를 제공합니다. SQL을 매핑 한 DSL 방식, 경량화한 DAO 방식을 제공하며 공식적으로 H2, MySQL, MariaDB, Oracle, PostgreSQL, SQL Server, SQLite 데이터베이스를 지원합니다.
object Payments : LongIdTable(name = "payment") { val orderId = long("order_id") val amount = decimal("amount", 19, 4) }
classExposedGettingStarted{
@Test fun `exposed DSL`() { // connection to MySQL Database.connect(dataSource)
transaction { // Show SQL logging addLogger(StdOutSqlLogger)
// CREATE TABLE IF NOT EXISTS payment (id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, amount DECIMAL(19, 4) NOT NULL) SchemaUtils.create(Payments)
classPayment(id: EntityID<Long>) : LongEntity(id) { companionobject : LongEntityClass<Payment>(Payments) var amount by Payments.amount var orderId by Payments.orderId }
classExposedGettingStarted{ @Test fun `exposed DAO`() { // connection to MySQL Database.connect(dataSource)
transaction { // Show SQL logging addLogger(StdOutSqlLogger)
// CREATE TABLE IF NOT EXISTS payment (id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, amount DECIMAL(19, 4) NOT NULL) SchemaUtils.create(Payments)
datasource에 대한 설정을 스프링 부트에서 사용하는 방식 그대로 사용할 수 있고 generate-ddl 설정이 활성화되어 있는 경우 데이터베이스 스키마를 생성하고, 특정 스키마를 제외하고 싶은 경우 excluded-packages 설정으로 제외할 수 있습니다. 실제 코드를 보면 매우 단순합니다. logging.level.Exposed: debug 경우 별도의 설정 없이 Show SQL Log를 볼 수 있습니다.
fundiscoverExposedTables(applicationContext: ApplicationContext, excludedPackages: List<String>): List<Table> { val provider = ClassPathScanningCandidateComponentProvider(false) provider.addIncludeFilter(AssignableTypeFilter(Table::class.java)) excludedPackages.forEach { provider.addExcludeFilter(RegexPatternTypeFilter(Pattern.compile(it.replace(".", "\\.") + ".*"))) } val packages = AutoConfigurationPackages.get(applicationContext) val components = packages.map { provider.findCandidateComponents(it) }.flatten() return components.map { Class.forName(it.beanClassName).kotlin.objectInstance as Table } }
스프링 표현식으로 제외 시킬 excludedPackages를 List로 받고, generate-ddl 여부에 따라 DatabaseInitializer 빈을 등록여부를 결정합니다. 만약 해당 빈을 등록하게 되면 DatabaseInitializer 객체가 ApplicationRunner을 구현하고 있기 때문에 스프링 어플리케이션이 실행하는 경우 run() 메서드에서 스키마를 생성하게 됩니다. (excludedPackages는 제외)
run() 메서드에 @Transactional 어노테이션이 있는 것을 볼 수 있습니다. 이것은 spring-transaction모듈을 통해서 TransactionSynchronizationManager를 기반으로 스프링의 트랜잭션 메커니즘을 그대로 사용할 수 있다는 의미 입니다.
스프링의 트랜잭션 동기화 메커니즘
토비의 스프링 3.1, 361 페이지
스프링은 위와 같은 방식으로 트랜잭션 동기화를 진행합니다. 해당 방식은 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 메서드에서 저장된 Connection을 가져다가 사용합니다.
@Test fun `exposed DAO`() { // connection to MySQL // Database.connect(dataSource) 스프링 Bean의 DataSource를 사용하기 때문에 주석
// transaction { 스프링 @Transactional 으로 트랜잭션을 시작하기 때문에 주석 // Show SQL logging // addLogger(StdOutSqlLogger) logging.level.Exposed: debug 으로 Show SQL logging 확인
// CREATE TABLE IF NOT EXISTS payment (id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, amount DECIMAL(19, 4) NOT NULL) // SchemaUtils.create(Payments) generate-ddl: true 으로 스키마 생성
// DELETE FROM payment WHERE payment.id = 1 // ... Payment.all() .forEach { it.delete() }
// DROP TABLE IF EXISTS payment // SchemaUtils.drop(Payments) // } }
@Test fun `exposed DSL`() { // connection to MySQL // Database.connect(dataSource) 스프링 Bean의 DataSource를 사용하기 때문에 주석
// transaction { 스프링 @Transactional 으로 트랜잭션을 시작하기 때문에 주석 // Show SQL logging // addLogger(StdOutSqlLogger) logging.level.Exposed: debug 으로 Show SQL logging 확인
// CREATE TABLE IF NOT EXISTS payment (id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, amount DECIMAL(19, 4) NOT NULL) // SchemaUtils.create(Payments) generate-ddl: true 으로 스키마 생성
// DELETE FROM payment WHERE payment.amount >= 1 Payments.deleteWhere { amount greaterEq BigDecimal.ONE }
// DROP TABLE IF EXISTS payment // SchemaUtils.drop(Payments) // } } }
DataSource는 스프링 Bean을 사용하기 때문에 제거했으며, transaction { ... }으로 트랜잭션을 시작했던 코드를 스프링의 @Transactional으로 대체했습니다. 또한 SchemaUtils.create(Payments)으로 스키마를 생성했던 부분을 generate-ddl: true 속성 파일로 대체했습니다. 그리고 ExposedTestSupport 객체에 @Transactional가 있어 테스트 코드의 최종 데이터는 모두 Rollback을 진행하게 됩니다. Spring 환경에서 Exposed를 사용하게 되면 보다 간결하게 사용 가능합니다.
Exposed는 batchInsert() 메서드를 지원하기 때문에 쉽게 batch insert를 진행할 수 있습니다. Mysql의 경우 JDBC 드라이버에 rewriteBatchedStatements=true 속성을 반드시 입력해야 batch insert가 가능합니다. shouldReturnGeneratedValues 값을 false로 지정하면 auto_increment으로 증가된 ID 값을 가져오지 않기에 성능이 향상될 수 있습니다.
로그에 출력되는 SQL은 batch insert가 진행되지 않고 개별 insert로 출력 됩니다.
하지만 실제 데이터베이스의 로그를 확인해보면 batch insert가 정상적으로 동작하는 것을 확인할 수 있습니다.
SQL Log 확인 방법 show variables like 'general_log%'; 확인 해서 general_log가 OFF인 경우 set global general_log = 'ON'; 설정 이후 general_log_file 경로에 로그 파일 확인
Variable_name
Value
general_log
OFF
general_log_file
/var/lib/mysql/2eb41ec6a5fe.log
연관 관계
1 2 3 4 5 6 7 8 9 10 11 12 13 14
object Books : LongIdTable("book") { val writer = reference("writer_id", Writers) val title = varchar("title", 150) val price = decimal("price", 10, 4) val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") }
object Writers : LongIdTable("writer") { val name = varchar("name", 150) val email = varchar("email", 150) val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") }