관리 메뉴

HAMA 블로그

Anorm 2.5 (4) - Parser API 사용하기 본문

PlayFramework2

Anorm 2.5 (4) - Parser API 사용하기

[하마] 이승현 (wowlsh93@gmail.com) 2016. 8. 26. 21:32


 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 사용자가 많아 졌으면 ...

Comments