Anorm 2.5 문서에서 parser API 에 대해 번역했습니다.  원문 바로가기

Parser API 사용하기

일반적이고 재사용가능한 파서를 만들기 위해 paser API 를 이용할 수 있습니다. 그것은 어떤 SELECT 쿼리의 결과도 파싱 할 수 있죠.

Note: 웹 어플리케이션이 대부분 비슷한 데이터셋을 리턴한다는것을 볼때  파서 api 는 매우 실용적이다. 예를들어 만약 Country 라는 객체를 결과 셋으로 부터 파싱할 수 있게 파서를 정의해 놓으면 또다른 People 이라는 파서와 함께 쉽게 그것들을 구성하여 Count 와 People 의 조인 쿼리를 처리 할 수 있게됩니다.

먼저 anorm.SqlParser._ 를 임포트하는것으로 시작하죠.

단일 결과 얻기 

먼저 RowParser 가 필요하다. 하나의 로우를 파싱 해서 하나의 scala 값을 얻는다. 예를들어 싱글 컬럼 결과 셋 로우를 스칼라 Long 으로 변환하는 파서를 정의 해 보겠다. 

val rowParser = scalar[Long]


그리고 나서 그것을 ResultSetParser 로 변환해야한다.  싱글 로우를 파싱하는 파서를 만들것이다.

val rsParser = scalar[Long].single


그래서 이 파서는 결과 셋을 파싱 할것이다. 그리고 Long 을 리턴할것이다. 간단한 SQL 즉 SELECT count  같은 쿼리에 의해 만들어지는 결과를 파싱하는데 유용할 것이다.

val count: Long = 
  SQL("select count(*) from Country").as(scalar[Long].single)


만약 기대된 단일 결과가 옵셔널 (0 또는 1 로우) 라면 , scalar 파서는 singleOpt 로 엮여질것이다.

val name: Option[String] =
SQL"SELECT name FROM Country WHERE code = $code" as scalar[String].singleOpt


단일 옵셔널 결과 얻기

country 이름을 가지고 country_id 를 얻고 싶다고 해보자. 근데 쿼리는 널을 리턴할 수 있다고 하자. 우린 singleOpt 파서를 이용할 수 있을것이다.

val countryId: Option[Long] = 
  SQL("SELECT country_id FROM Country C WHERE C.country='France'")
  .as(scalar[Long].singleOpt)


좀더 복잡한 결과셋을  가지고 놀아보자.

좀 더 복잡한 파서를 만들어 볼까?

str("name") ~ int("population") 이것으로 RowParser 를 만들것인데 이 파서는 문자열 name 컬럼과 숫자형 population 컬럼을 파싱 할 수 있고 . 그리고 나서 ResultSetParser 을 만드는데 이것은 * 를 사용하여 이런 종류의 많은 로우를 파싱 할 겁니다.

val populations: List[String ~ Int] = 
  SQL("SELECT * FROM Country").as((str("name") ~ int("population")).*) 

보다시피, 이 쿼리 결과 타입은 List[String ~ Int] 인데 country name 과 population items 의 리스트이죠. 


동일한 코드를 다시 요렇게 쓸 수 있다. 

val result: List[String ~ Int] = SQL("SELECT * FROM Country").
                                             as((get[String]("name") ~ get[Int]("population")).*)


String~Int 타입은 뭘까?  이것은 Anorm 에서 만들어진 타입인데 다른데서 사용하기 편리한것은 아니다.  대신해서 (String, Int) 같은 같단한 튜플이 더 낫다. RowParser 에서 map 함수를 사용하여 그것들의 결과를 좀 더 편리한 타입으로 변환 시킬 수 있다. 

val parser = str("name") ~ int("population") map { case n ~ p => (n, p) }

Note: 여기선 (String, Int) 튜플을 만들었는데 커스텀 케이스 클래스같은 다른 타입으로 결과를 변환 할 수 있다. 


A ~ B ~ C  타입을 보통 (A,B,C) 로 변환하는 것 이 일반적이기 때문에 , 아예 flattern 함수같은 것을 미리 제공한다. 


val parser = str("name") ~ int("population")

val result: List[(String, Int)] = SQL("select * from Country").as(parser.flatten.*)

이제 리턴값이 정상적인 튜플의 리스트가 되었다. (이전엔 Anorm 타입의 리스트) 


RowParser  는 추출된 컬럼들과 함께 적용 될 어떤 함수와도 엮일  수  있다.

import anorm.SqlParser.{ int, str, to }

def display(name: String, population: Int): String = 
  s"The population in $name is of $population."

val parser = str("name") ~ int("population") map (to(display _))

Note:  매핑 함수는 반드시 부분적으로 적용 될 것이다. (syntax fn _)  (see SLS 6.26.2, 6.26.5 - Eta expansion).


만약 리스트가 비어있지 않으면, parser.+  는 parser.* 대신해서 사용 될 수 있다.. 

Anorm 은 파서 컴비네이터를 제공한다.  하나: ~><~.

import anorm.{ SQL, SqlParser }, SqlParser.{ int, str }

// Combinator ~>
val String = SQL("SELECT * FROM test").as((int("id") ~> str("val")).single)
  //  int 컬럼 id  와 문자열 val 컬럼을 가져야하며 결과로 val 을 유지

val Int = SQL("SELECT * FROM test").as((int("id") <~ str("val")).single)
  // int 컬럼 id  와 문자열 val 컬럼을 가져야하며 결과로 id 을 유지


 더 복잡한 예제 

좀 더 복잡한 예제를 시도해 볼까? 어떻게 다음 쿼리의 결과를 파싱할 수 있을까?  country code 로  country name 와 모든 spoken languages 를 가져오기 위해서 말이지..

select c.name, l.language from Country c  join CountryLanguage l 
on l.CountryCode = c.Code where c.code = 'FRA'

1 대 다 조인이다.


자 List[(String,String)] 으로 모든 로우를 파싱해보자. ( name, language 튜플의 리스트):

var p: ResultSetParser[ List[(String,String)] ] = {
  str("name") ~ str("language") map(flatten) *
}


이런 류의 결과를 얻게된다. 

List(
  ("France", "Arabic"), 
  ("France", "French"), 
  ("France", "Italian"), 
  ("France", "Portuguese"), 
  ("France", "Spanish"), 
  ("France", "Turkish")
)


그리고 나서 스칼라 콜렉션 API 를 사용 할 수 있게 된다. 우리가 원하는 걸로 변환 하는거다.

case class SpokenLanguages(country:String, languages:Seq[String])

languages.headOption.map { f =>
  SpokenLanguages(f._1,  languages.map(_._2))
}


마지막으로 우린 이렇게 편리한 함수를 얻게 되었다. (단일 객체 리턴) 

case class SpokenLanguages(country:String, languages:Seq[String])


def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {
  
 
  val languages: List[(String, String)] = SQL(
    """
      select c.name, l.language from Country c 
      join CountryLanguage l on l.CountryCode = c.Code 
      where c.code = {code};
    """
  )
  .on("code" -> countryCode)
  .as(str("name") ~ str("language") map(flatten) *)



  languages.headOption.map { f =>
    SpokenLanguages(f._1, languages.map(_._2))
  }
}

역주 )  headOption 은 리스트에서 첫번째 아이템을  Some(value) 타입으로 리턴받는다. 만약 리스트에 없으면 None 을 리턴한다. 따라서 위의 f 에는 하나의 튜플만이 담겨져서 SpokenLanguages 클래스의 인자로 "튜플의 첫번째 요소" "튜플리스트의 두번째 요소 만의  리스트" 들이 담겨져서 리턴된다. 이 예제는 1대 다 조인의 결과를 1대 다로 데이터를 객체로 저장할 때 사용된다. 즉 사용자 id  와 그 사용자가 구매한 목록 같은거 말이다. 


계속해서 우리의 예제를 좀 더 복잡 하게 해보자. 공식 언어를 분리 하기 위해서 ~

case class SpokenLanguages(
  country:String, 
  officialLanguage: Option[String], 
  otherLanguages:Seq[String]
)

def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {

  val languages: List[(String, String, Boolean)] = SQL(
    """
      select * from Country c 
      join CountryLanguage l on l.CountryCode = c.Code 
      where c.code = {code};
    """
  )
  .on("code" -> countryCode)
  .as {
    str("name") ~ str("language") ~ str("isOfficial") map {
      case n~l~"T" => (n,l,true)  
      case n~l~"F" => (n,l,false)
    } *
  }

 
 languages.headOption.map { f =>
    SpokenLanguages(
      f._1,                                               // 리스트의 첫번째 튜플의 첫번째 튜플 요소 
      languages.find(_._3).map(_._2),       // true 인것을 찾아서 두번째 튜플 요소 리턴 
      languages.filterNot(_._3).map(_._2)  // true 인것들을 거르고 두번째 튜플요소의 리스트리턴
    )
  }
}


MySQL의 sample database 라면 이런것을 얻을 수 있게 된다. 

$ spokenLanguages("FRA")

> Some(SpokenLanguages(France,
                                    Some(French),  // 공식 언어 
                                    List(Arabic, Italian, Portuguese, Spanish, Turkish // 나머지 언어들
    ))
)

역주  :  Play 2 사용자가 많아 졌으면 ...

 Anorm 2.5 공식 문서에서 번역했습니다.  원문 바로가기


플레이는 간단한 데이터 접근 레이어를 포함한다. 그 이름은 Anorm 이며  plain SQL 을 데이터베이스와 상호 작용 하기 위해 사용하며 결과 데이타셋을 변환시키고 파싱할 수 있는 API 를 제공한다.

Anorm 은 ORM(Object Relational Mapper) 이 아니다.

다음 문서를 참고해서 예제 DB로 이용하자 .MySQL world sample database.

만약 당신의 웹 어플리케이션에서 이용하길 원하면 다음 MySQL 웹싸이트 지침서를 따르고 다음에 설명된 대로 설정해보자. on the Scala database page .

살펴보기

SQL 데이터베이스에 접근하기 위해 다시금 plain old SQL 로 회귀한다는건 좀 이상한 느낌이 들 수 있겠다.특히 고차원의 ORM  (하이버네이트 같은)을 사용이 익숙한 자바 개발자라면 더더욱...

우리는 이런 툴들의 사용의 이점에  일부분 동의 할지라도 모든 곳에서 필요가 없음에 대해 알고 있으며  특히 스칼라 같은 고차원적인 언어의 힘을 가지고 있을 때는 더더욱 필요가 없게되며 빠르게 개발하는것에 대해 역효과만 초래 할 뿐이다. 

 

JDBC 를  pain 하게 사용하게 하자. 그러나 더 나은 API 를 제공할것이다.

우리는 JDBC API 를 직접적으로 사용한다는게 얼마나 순진한 생각이고 지루한 일이 될지 충분히 알고 있습니다. 특히 자바에서 말이죠.  체크드 예외를 다루어야하며 어디서나 결과셋을 돌고 또 돌아야하죠. 데이타셋의 로우 데이터를 자신의 데이터 구조에 맞추기 위해서 말입니다.

우리는 더 간단한 API 를 JDBC 를 위해 제공하며, 스칼라를 이용하면 더 이상 예외에 괴롭힘을 당하지 않아도 되고, 데이터 변환에 있어서 함수형 언어를 이용한 엄청 편리한 개발이 가능합니다. 

 플레이 Scala SQL 접근 레이어의 목적은 다양한 API 들을 효과적으로 JDBC 데이터에서 다른 스칼라 구조로 변환하는데 있습니다.


더이상 DSL 을 관계형 데이타베이스에 접근하기위해 사용할 필요가 없습니다.

SQL 은 이미 훌륭한 DSL 입니다. 바퀴를 또 다시 발명하고 익혀야 할 필요가 없습니다.게다가  SQL 구문은 데이타베이스 벤더들 마다 조금 씩 다릅니다. 

만약 각종 ORM 의 DSL 을 배우는것은 또 다른 방언들을 배우게 되는것이며 (거기서 헤맬수도 있으며 ) 특정 데이타베이스의 흥미로운 부분들의 사용을 포기하는것을 의미합니다.

플레이는 때때로 당신에게 미리 만들어진 SQL 구문을 제공할것입니다. 그렇다고 이게 SQL 자체의 순수한 면을 감추는데 이용되지는 않을 겁니다. 플레이는 낭비적인 코딩의 양을 줄여줄 것이며 항상 plan old SQL 를 지향할 것입니다.


SQL 가 만들어지는데  타입안전 DSL 은 실수다.

타입안전 DSL 이 더 나은지에 대해서 몇몇 논쟁이 있어왔다. 모든 당신의 쿼리들이 컴파일러에 의해 체크되어야하는지 말이다. 불운하게도  컴파일러는  당신의  메타모델 정의기반으로 정의된 쿼리를 체크하는데 그것은  당신의 데이터 구조를 데이타스키마로 매핑하도록 자체적으로 코딩한다. 

거기에는 메타모델이 얼마나 정확하냐에 대한 보장이 없다.심지어 컴파일러는 당신의 코드와 쿼리가 정확한 타입을 가지고 있다고 말하지만 그것은 런타임에 데이타베이스와의 타입 미스매치로 인한 심각한 문제를 도출 시킬 수 있다. 


당신의 SQL 코드를 컨트롤 하라.

ORM 은 간단한 케이스에서는 잘 작동하지만 복잡한 스키마를 다루거나 이미 존재하는 데이타베이스를 다룰때 당신의 ORM 프레임워크와 기나긴 사투를 벌여야 할 가능성이 크다. 스스로 짠 SQL 쿼리는 간단한 헬로월드 프로그램에서는 지루한 일이 겠지만 실제세계의 어플리케이션에서는 시간을 절약해주며 SQL 코드를 당신이 컨트롤함에 따라 코드를 단순화 시킬 수 있게 될 것이다. 

+ Recent posts