b

Slick 본문

language/scala

Slick

dev.bistro 2014. 10. 5. 01:47

어떤 언어를 사용하던지, 무엇을 만들던지 DB와의 연결은 중요한 포인트임이 틀림없다. 각 언어별로 대표적인 프레임웍이 있듯이 스칼라 역시 마찬가지이다. 스칼라를 위한 typesafe는 몇몇 표준 기술 셋을 정의하고 있다 ( http://typesafe.com/platform )

이 중에 오늘 얘기하고자 하는 것은 Slick ( http://typesafe.com/platform/tools/scala/slick ) 이다.
처음 접했을 때 생소한 개념이기도 했고, 정확한 사용방법을 숙지 할 수가 없었기에 현재 사용하고 있는 방법들을 공유하면서 어떤식으로 쓰면 되는지를 공유할 예정이다.

개념등이나 자세한 사용법은 래퍼런스 문서를 참고하면 될것이고, 이렇게 쓰는 거구나~ 정도만 참고 하시면 될듯하다. Play 1에 있던 Anorm이나 Squeryl보다는 좀 더 일반적으로 사용되고, 래퍼런스도 많고, 표준 스택이라 배움에 후회는 없을 것이다. 


* 주의 *
Slick은 2013년 중순경 2.0 이 발표되면서 약간의 문법이 변화되었다. 구글링을 할 때 하더라도 버전을 주의해서 보고 아래의 문서를 참고하면 좋을 것이다.

- http://slick.typesafe.com/news/all-news.html : http://typesafe.com/platform/tools/scala/slick
- MIGRATION GUIDE FROM SLICK 1.0 TO 2.0 : http://slick.typesafe.com/doc/2.0.0-RC1/migration.html


Slick은  기존의 JPA와 Hibernate, Mybatis와는 달리  " Slick is a Functional Relational Mapping (FRM) library  " 으로 소개한다. (FRM ???) FRM이라고는 하지만 전통적인 쿼리를 지원하므로 다양한 사용 방법을 알아보겠다.


* 아래 코드에 사용된 정보 *

- Scala 2.10.4
- val slickVersion = "2.1.0"
- val slick = "com.typesafe.slick" %% "slick" % slickVersion
- val slickext = "com.typesafe.slick" %% "slick-extensions" % slickVersion



Story1 - Database connection

1-1) http://slick.typesafe.com/doc/2.1.0/gettingstarted.html#database-connection 처럼 사용 할 수 있다.

1-2) Java개발자에 익숙한 DataSource를 사용 할 수도 있다.

  lazy val OSlickDataSource = com.typesafe.slick.driver.oracle.OracleDriver.simple.Database.forDataSource( OBasicDataSource)

  lazy val HSlickDataSource = scala.slick.driver.MySQLDriver.simple.Database.forDataSource( HBasicDataSource )

* HBasicDataSource와 OBasicDataSource는 DBCP의 BasicDataSource 이다.
* Oracle, MySQL 두개를 사용하고 있고, DataSource가 필요한 Java-Project도 있기 때문에 위와 같이 사용하고 있다.


Story2 - Plain Query

일반적인 Query Base방식을 지원한다. Mybatis나 JDBC에 익숙한 개발자라면 무리없이 이해할 수 있을 것이기 떄문에 이것부터 시작을 해보자.

HSlickDataSource withSession {
implicit  session=> 
Q.updateNA(s"drop table if exists $readyTableName").execute
(Q.u + "drop table a").execute

처럼 drop table명령을 실행 할 수 있다. 둘 다 같은 방식이다. insert 또한 마찬가지이다.

def insert(c: SynObject) = (Q.u + "INSERT INTO $tempTableName(SVALUE) VALUES (" + c.svalue + ")" ).execute
data.foreach(insert) //data List[SynObject]

처럼 batch insert가 가능하지만



val psmt = session.prepareInsertStatement(  s"INSERT INTO $tempTableName(SCODE, SVALUE, STATUS) VALUES (?, ?, ?)" )

for(line <- data.zipWithIndex ) {
  psmt.setString(1, line._1.scode)
  psmt.setString(2, line._1.svalue)
  psmt.setString(3, line._1.status)
  psmt.addBatch()
  if(line._2 % 1000 == 0) psmt.executeBatch
}
psmt.executeBatch

처럼 PreparedInsert BatchInsert를 이용하여 성능을 향상 시킬수 있다.



Story3 -  Lifted Embedding API

Plain Scala의 특징과 장점을 활용하려면 이 방법으로 사용하면 된다.
우선은 1개의 DB 테이블을 Scala Class로 매핑을 하자.


3-1) case class의 선언

case class ForbiddenKeyword( id:Int, collId:String, keyword:String, itemStatus:Option[String])

금칙 키워드 정보를 가지고 있는 Class를 선언한다.


3-2) Table 매핑

class ForbiddenKeywordTable(tag:Tag) extends Table[ForbiddenKeyword](tag, "FORBIDDEN_KEYWORD") {

  def id = column[Int]("id", O.PrimaryKey, O.NotNull)
  def collId = column[String]("collId", O.NotNull)
  def keyword = column[String]("keyword", O.NotNull)
  def itemStatus = column[String]("itemStatus")
  def * = (id, keyword, itemStatus) <> (ForbiddenKeyword.tupled, ForbiddenKeyword.unapply)
}

FORBIDDEN_KEYWORD 테이블 정보를 ForbiddenKeywordTable 클래스로 선언하였다.
id필드는 PK, NotNull이고 Int타입이고, itemStatus 필드는 String 타입이다. 이후에...



val forbidden = TableQuery[ForbiddenKeywordTable]


def getForbiddenKeywordList(collId:String = null) = {
ds withSession {
 implicit session => {
   collId match {
     case e:String => forbidden.filter(_.collId === collId).list
     case _ => forbidden.list
   }
 }
}
}


select * from FORBIDDEN_KEYWORD  
select * from FORBIDDEN_KEYWORD  wehre collId = ?
를 수행 할 수 있다.


3-3) Null 허용

위에서 NotNull 필드는 column 정의부에 선언했지만 allow Null은 어떻게 해야할까? Scala에서는 Option으로 처리 할 수 있다. 예를 들어 keyword column이 Null을 허용한다고 할 경우  def * (class mapping 부분)을  아래처럼 .?를 붙이면 된다.

def * = (id, keyword.?, itemStatus) <> (ForbiddenKeyword.tupled, ForbiddenKeyword.unapply)

이 부분은 ColumnExtensionMethods trait class를 참고로 하면 된다. 아래에서 다시 설명 한다.


3-4) Column Extension Method

컬럼을 매핑할 때 컬럼마다 일정 로직을 태우면서 매핑 할 수 있다.
이 부분은 ColumnExtensionMethods trait class를 참고로 하면된다. 이 부분을 조금 더 훑어보면 NumericColumnExtensionMethods, StringColumnExtensionMethods 등을 볼 수 있는데 다양한 메소드를 컬럼에 적용할 수 있음을 볼 수 있다.
예를 들어 Int-Column필드는 절대값을 가져 올수 있고 ( ex: id.abs ) String-Column은 trim을 적용 할 수도 있다. (ex : keyword.trim )
ExtensionMethods.scala


3-5) 테이블 명을 파라미터로 변경

1 테이블과 1 Class로 매핑된다면 문제가 없지만. 1 Class가 여러 테이블에 매핑 (즉, 스키마가 동일한 여러 테이블)이 존재한다면 3-2)방식을 여러개 작성하는 것은 낭비이다. parameter로 테이블 명을 받도록 변경해보자.

class ForbiddenKeywordTable(tag:Tag, tableName:String) extends Table[ForbiddenKeyword](tag, tableName) {...} 

으로 선언한 이후에 아래 처럼 선언을 해서 사용을 한다.

val adminKeyword = TableQuery[ForbiddenKeywordTable]((tag:Tag) => new  ForbiddenKeywordTable(tag, "ADMIN_FORBIDDEN_KEYWORD"))

val forbidden = TableQuery[ForbiddenKeywordTable]((tag:Tag) => new ForbiddenKeywordTable(tag, "FORBIDDEN_KEYWORD"))


3-6) trait CRUD

이런식의 매핑 정보를 나열하다 보면 어쩔수 없이 중복 코드를 많이 작성하게 된다. 특히 기본적인 CRUD에서 많이 보이는데 이 부분을 공통하 시켜보자.

개인적 요구 사항

#1. MySQL/ Oracle DB2개를 쓰고 각 테이블의 Schema는 동일하다
#2. 동일한 스키마를 여러 테이블이 사용한다.
#3. 공통 CRUD는 가능한 1번만 작성하고 싶다.

trait CRUD[T] {

  implicit val driver: JdbcProfile
  import driver.simple._
  implicit val tableName:String
  implicit val prop:TableQuery[_<: Table[T]]

  def count(implicit  session:Session) =  prop.length.run
  def findAll(implicit  session:Session) = prop.list
  def insert(obj:T)(implicit  session:Session) = prop += obj
  def insert(obj:List[T])(implicit  session:Session) = prop.insertAll(obj: _*) 

  def drop(implicit  session:Session) =  if ( MTable.getTables(tableName).list.nonEmpty)   prop.ddl.drop

  def delete(obj:T )(implicit session:Session)
}

jdbcProfile과 tableName을 인자로 사용하도록 하였고, TableQuery를 내재화 시켜서 몇몇 메소드를 공통화 하였다. 
delete는 그렇게 할 수 없어서 각자 구현하도록 남겨놓았다.


Story 4 - 사용후기

생각보다 학습비용이 많이 든다. 기존에 사용하던 습관이 남아 있어서 인지... 이건 이렇게 했는데 Slick에서는 어떻게 해야하지 ? 라는 고민을 많이 했다. 생각보다 구글링/so에서 원하는 만큼 결과를 얻지 못해 github.com에서 많은 프로젝트를 참고하기도 하였다.

다른 익숙한 프레임웍만큼 코어까지 살펴보기엔 꽤 오랜 시일이 걸릴 것 같지만, Slick을 처음 적용해 가면서 조금씩 긍적적인 생각이 든다. ... 

'language > scala' 카테고리의 다른 글

spray는 멋지군..  (0) 2014.10.20
Enum Type을 Slick 에서 사용하기  (0) 2014.10.07
Functional Programming Principles in Scala 끝!  (1) 2013.06.04
Scala Tuple  (0) 2013.03.12
2.10을 위한 scala-ide plugin 재설치  (0) 2013.03.12
Comments