Scala/Play framework

Play Framework 2.5 + Play Slick 2.1.0. 연동, 기본적인 쿼리

partner_jun 2017. 4. 12. 17:17

Slick은 ORM이 아닌 FRM이라고 부른다. 객체 그래프를 탐색하는 방식이 아니라 함수형 프로그래밍에서 친숙한 map이나 filter같은 고차함수로 쿼리하기 때문이다. (실제로는 고차함수를 SQL문으로 변경시켜 쿼리해준다) 그래서 ORM 프레임워크들과는 색다른 사용감을 느낄 수 있다. 하지만 테이블의 연관관계를 설정해 놓는 것이 아니라 기존 SQL문처럼 조인 구문을 사용해야 하기 때문에 때론 ORM보다 불편함을 느낀다. 


자바로 만들어진 인메모리 DB H2와 Play Framework 2.5.13, Play Slick 2.1.0 버전을 이용해 테이블 작성과 기본적인 쿼리 몇가지를 해보자.



1. libraryDependency 추가

Play framework에서 사용되는 Slick은 Play Slick이라는 이름으로 Maven Repository에서 찾을 수 있다. 검색한 Play Slick과 Play Slick Evolution을 Play Framework 프로젝트의 build.sbt에 추가하자.

libraryDependencies ++= Seq(cache, specs2 % Test,
"com.h2database" % "h2" % "1.4.194",
"com.typesafe.play" %% "play-slick" % "2.1.0",
"com.typesafe.play" %% "play-slick-evolutions" % "2.1.0")

기본으로 입력되어 있는 jdbc 라이브러리 디펜전시는 지워야 한다. 그대로 두면 Slick과 충돌이 일어난다.



2. DB 설정

conf/application.conf 파일에 DB url, driver등을 설정하자.

slick.dbs.default.driver="slick.driver.H2Driver$"
slick.dbs.default.db.driver="org.h2.Driver"
slick.dbs.default.db.url="jdbc:h2:mem:play"
slick.dbs.default.db.user="sa"
slick.dbs.default.db.password=""

여러개의 DB를 연결해야 한다면 default 대신 구분할 수 있는 이름을 입력한다.



3. Table 및 case class 작성

쿼리한 결과를 담을 case class를 만들고 DB 테이블의 구조와 매칭되는 Table 클래스를 만들어야 한다. 주의할 점은 먼저 자신이 사용할 DB의 API를 import 해야한다는 것이다. IntelliJ의 import 기능이 제대로 지원되지 않는다.


import slick.jdbc.H2Profile.api._

case class User(userSeq: Long, id: String, password: String, nickname: String, regDate: Timestamp)

class UserTable(tag: Tag) extends Table[User](tag, "USER") { // "USER"는 DB의 테이블 명
// column[컬럼 타입]("컬럼명", 속성)
def userSeq = column[Long]("user_seq", O.PrimaryKey, O.AutoInc)
def id = column[String]("id", O.Unique)
def password = column[String]("password")
def nickname = column[String]("nickname")
def regDate = column[Timestamp]("reg_date")

override def * = (userSeq, id, password, nickname, regDate) <> ((User.apply _).tupled, User.unapply)
}



case class가 아닌 Tuple로도 작성이 가능하다. 같은 테이블을 Tuple로 작성하면 아래와 같다.

class UserTable(tag: Tag) extends Table[(Long, String, String, String, Timestamp)](tag, "USER") {
def userSeq = column[Long]("user_seq", O.PrimaryKey, O.AutoInc)
def id = column[String]("id")
def password = column[String]("password")
def nickname = column[String]("nickname")
def regDate = column[Timestamp]("reg_date")

override def * = (userSeq, id, password, nickname, regDate)
}


튜플로 작성된 테이블 클래스를 보면 알 수 있듯  *  메소드는 결과를 객체로 apply하거나 unapply할 때 사용할 함수를 지정하는 메소드다. 이 메소드를 작성하기에 따라 case class가 아닌 class에도 결과를 담아낼 수 있다.



4. 기본적인 쿼리

기본적인 쿼리를 할 Repository를 만들어 보자. Repository에서도 마찬가지로 DB에 맞는 profile을 직접 import해야 한다.


import slick.jdbc.H2Profile
import slick.jdbc.H2Profile.api._

@Singleton
class UserSimpleRepository @Inject()
(dbConfigProvider: DatabaseConfigProvider) {
/*
application.conf에 default가 아닌 이름을 붙인 경우
dbConfigProvider에 @NamedDatabase("이름") 어노테이션 사용
*/

private val db = dbConfigProvider.get[H2Profile].db // dataConfigProvider로부터 db를 가져온다.
private val userTableQuery = TableQuery[UserTable] // 앞서 작성한 UserTable 클래스로 테이블쿼리 객체를 얻어온다.

}

Play Framework가 만든 DatabaseConfigProvider 객체를 주입받아 사용한다.



이제 설정은 모두 끝났다. 쿼리를 작성하면 된다. 기본적인 쿼리 몇 가지는 아래와 같다.

def selectAll: Future[Seq[User]] = db.run(userTableQuery.result)

def update(userSeq: Long, newNickname: String): Future[Int] = {
val updateQuery = userTableQuery.filter(_.userSeq === userSeq)
.map(user => user.nickname)
.update(newNickname)
db.run(updateQuery)
}

def insertUsers(users : Seq[User]): Future[Option[Int]] = db.run(userTableQuery ++= users)

def deleteUser(userSeq: Long): Future[Int] = db.run(userTableQuery.filter(_.userSeq === userSeq)
.delete)

더 자세한 쿼리는 공식 문서를 참조하면 된다.


함수형 콜렉션을 다루듯 쿼리를 작성하고 db.run 메소드에 쿼리를 전달함으로써 결과를 얻어온다. 

트랜잭션 처리도 특이한데, 아래와 같이 트랜잭션 처리할 쿼리들을 묶어 전달한다.


val transactionQuery = DBIO.seq(updateQuery, updateQuery2).transactionally
db.run(transactionQuery)


.result 함수에 추가적인 함수를 사용해 결과를 하나만 가져오거나 Option으로 가져오는 방법도 있다.


def selectOne: Future[User] = db.run(userTableQuery.result.head)
def selectOneOption: Future[Option[User]] = db.run(userTableQuery.result.headOption)



리턴 타입을 보면 알 수 있듯 Slick 쿼리는 Future를 리턴함으로 Non-blocking 메소드로 만들어진다. Future나 Option을 적극적으로 활용한다는 점은 재미있지만 처리를 위해 부분 함수들을 작성해야 한다는 점은 까다롭다.