Play2.4 로 웹 개발 시작하기 

쉽고, 재밌고, 강력하고, 편리한 웹 프레임워크 Play2  (scala 언어기반) 을 배워봅시다.

환경

- Windows 10 

- JDK 1.8

- IntelliJ 2016.2.2 

- Scala 2.11

- Play 2.48 


연재 순서 

1. Play2 웹 개발 시작하기 -  설치 및 프로젝트 만들기 

2. Play2 웹 개발 시작하기 -  프로젝트 살펴보기 

3. Play2 웹 개발 시작하기 -  나만의 프로젝트 만들기 

*  2.5 에 대한 한글 자료가 부족한듯 싶습니다. 경험 풍부한 분들의 적극적인 정보 공유가 필요합니다.



Play2 웹 개발 시작하기 -  환경 설치 및 프로젝트 만들기


1. JDK 1.8 버전 설치  

  Play2 를 하기 위해서 미리 깔려져 있어야 할것은  단지 JDK 1.8 뿐입니다.  jdk 1.8 설치하기 
  scala 나 play2 를 위한 것 들은 IntelliJ 에서 자체 제공합니다.
  

2. IntelliJ 설치   

  Play2 처음 시작 하려면 무조건 IntelliJ 씁시다. 편하니깐~ (내부에 대부분 준비되어 있습니다) 

 가)   IntelliJ 다운로드 받기  에서 Ultimate 버전으로 다운 받습니다.

  나)   IntelliJ 를 설치합니다. (다운받은 exe 클릭)

       - next ~ next 계속해주세요. 설치위치 및 옵션들은 알아서 잡으시고요. 

  다)  설치 끝.  IntelliJ 아이콘을 클릭하여 실행 시켜 봅시다.  


2. IntelliJ 실행 및 Play2 프로젝트 만들기

   중요포인트 :  Play2  설치하고 처음 실행 할 때 Scala 와 Sbt 에 대한 플러그인을 꼭 설치~

   출발~~~~!!

처음 IntelliJ 를 실행하면  IDE 세팅 설정을 물어보는데 처음 하는것이니 아래것 선택하고 다음~


평가판 선택하시고 다음~


 IDE 색상 선택하시고 다음 ~



 이런것들이 기본적으로 있다는 야그~ 다음~


Scala 기반의 Play2 를 할 것이므로 Scala 인스톨 추가 그리고 다음~


자 이제 시작 화면이 뜨는데 프로젝트 만들기 전에 설정에서 따로 플러그인을 설치해줘야합니다.
빨강색 박스의 Configure 누르고 플러그인 선택하세요.


 Broswe repositories.. 선택 


SBT 로 검색해서 모두 설치해 줍니다.


재 시작 ~

이제 진짜 새로운 프로젝트를 만들어 보죠.

Create New Project 선택 


왼쪽 Scala 선택하시고 오른쪽에 나온 목록중에 Play2.x 선택하고 다음~ 


프로젝트 이름쓰시고 
Scala 는 2.11  그리고 
Play 버전을 2.4.8 로 내려주세요. 
최근 버전은 2.5 이나 너무 최신 버전은 정보를 찾을 때 종종 어려움이..

Finish 클릭~


자 이제 프로젝트가 생성되고 있으며,
아래 빨강색에 Sbt 를 통해 각종 플러그인 모듈들이 다운로드 받아지게 됩니다.
왼쪽 창에 프로젝트 창에 프로젝트 구성이 보이지 않고 있습니다. 

* 시간이 굉장히 오래 걸림니다.


다운로드 받게 놔두고 환경 설정을 하겠습니다.
File - project structure .. 선택하면 위에 창이 뜹니다.
Project 의 Project SDK 에 JDK 1.8 을 설정해 줍니다.


모듈로 가서  JDK 1.8 을 설정해 줍니다.  OK 누르고 완료 


Sbt 를 통한 다운로드가 끝나고 왼쪽 프로젝트 창에 프로젝트가 제대로 인식되었습니다.


오른쪽 위의 빨강색 박스의 시작 버튼 (기울어진 삼각형) 을 누르면 웹어플리케이션이  시작됩니다.



Play2 내부에는 Netty 기반 웹서버가 있기때문에 톰캣같은게 필요 없습니다.
웹브라우저 localhost:9000 을 통해 웹어플리케이션이 실행되었습니다.




 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 문서에서 parser API 에 대해 번역했습니다.  원문 바로가기

Streaming results


쿼리 결과는 하나의 로우씩 진행되는데  즉 메모리에 모두 적재되어서 진행하지 않습니다. 
다음 예제는 country 테이블의 행들의 숫자를 카운팅 하는  예 입니다.

val countryCount: Either[List[Throwable], Long] = 

  SQL"Select count(*) as c from Country".fold(0L) { (c, _) => c + 1 }

성공여부에 따라서 Long 이 오른쪽에 , 에러의 리스트가 왼쪽에 ~
역주1)  fold( 시작값 ) {  (누적값, 리스트의 값) => 누적값 + 1 }    
역주2) Either 는 다음 블로그글 참고 : http://coding-korea.blogspot.kr/2012/12/scala-either.html

Result can also be partially processed:

val books: Either[List[Throwable], List[String]] = 
  SQL("Select name from Books").foldWhile(List[String]()) { (list, row) => 
    if (list.size == 100) (list -> false) // stop with `list`
    else (list := row[String]("name")) -> true // continue with one more name
  }

It’s possible to use a custom streaming:

import anorm.{ Cursor, Row }

@annotation.tailrec
def go(c: Option[Cursor], l: List[String]): List[String] = c match {
  case Some(cursor) => {
    if (l.size == 100) l // custom limit, partial processing
    else {
      go(cursor.next, l :+ cursor.row[String]("name"))
    }
  }
  case _ => l
}

val books: Either[List[Throwable], List[String]] = 
  SQL("Select name from Books").withResult(go(_, List.empty[String]))

The parsing API can be used with streaming, using RowParser on each cursor .row. The previous example can be updated with row parser.

import scala.util.{ Try, Success => TrySuccess, Failure }

// bookParser: anorm.RowParser[Book]

@annotation.tailrec
def go(c: Option[Cursor], l: List[Book]): Try[List[Book]] = c match {
  case Some(cursor) => {
    if (l.size == 100) l // custom limit, partial processing
    else {
      val parsed: Try[Book] = cursor.row.as(bookParser)

      parsed match {
        case TrySuccess(book) => // book successfully parsed from row
          go(cursor.next, l :+ book)
        case Failure(f) => /* fails to parse a book */ Failure(f)
      }
    }
  }
  case _ => l
}

val books: Either[List[Throwable], Try[List[Book]]] = 
  SQL("Select name from Books").withResult(go(_, List.empty[Book]))

books match {
  case Left(streamingErrors) => ???
  case Right(Failure(parsingError)) => ???
  case Right(TrySuccess(listOfBooks)) => ???
}

Iteratee

It’s possible to use Anorm along with Play Iteratees, using the following dependencies.

libraryDependencies ++= Seq(
  "com.typesafe.play" %% "anorm-iteratee" % "ANORM_VERSION",
  "com.typesafe.play" %% "play-iteratees" % "ITERATEES_VERSION")

For a Play application, as play-iteratees is provided there is no need to add this dependency.

Then the parsed results from Anorm can be turned into Enumerator.

import java.sql.Connection
import scala.concurrent.ExecutionContext.Implicits.global
import anorm._
import play.api.libs.iteratee._

def resultAsEnumerator(implicit con: Connection): Enumerator[String] =
  Iteratees.from(SQL"SELECT * FROM Test", SqlParser.scalar[String])

Multi-value support

Anorm parameter can be multi-value, like a sequence of string.
In such case, values will be prepared to be passed to JDBC.

// With default formatting (", " as separator)
SQL("SELECT * FROM Test WHERE cat IN ({categories})").
  on('categories -> Seq("a", "b", "c")
// -> SELECT * FROM Test WHERE cat IN ('a', 'b', 'c')

// With custom formatting
import anorm.SeqParameter
SQL("SELECT * FROM Test t WHERE {categories}").
  on('categories -> SeqParameter(
    values = Seq("a", "b", "c"), separator = " OR ", 
    pre = "EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name=",
    post = ")"))
/* ->
SELECT * FROM Test t WHERE 
EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name='a') 
OR EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name='b') 
OR EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name='c')
*/

On purpose multi-value parameter must strictly be declared with one of supported types (List, ’Seq,Set,SortedSet,Stream,VectorandSeqParameter`). Value of a subtype must be passed as parameter with supported:

val seq = IndexedSeq("a", "b", "c")
// seq is instance of Seq with inferred type IndexedSeq[String]

// Wrong
SQL"SELECT * FROM Test WHERE cat in ($seq)"
// Erroneous - No parameter conversion for IndexedSeq[T]

// Right
SQL"SELECT * FROM Test WHERE cat in (${seq: Seq[String]})"

// Right
val param: Seq[String] = seq
SQL"SELECT * FROM Test WHERE cat in ($param)"

In case parameter type is JDBC array (java.sql.Array), its value can be passed asArray[T], as long as element type T is a supported one.

val arr = Array("fr", "en", "ja")
SQL"UPDATE Test SET langs = $arr".execute()

A column can also be multi-value if its type is JDBC array (java.sql.Array), then it can be mapped to either array or list (Array[T] or List[T]), provided type of element (T) is also supported in column mapping.

import anorm.SQL
import anorm.SqlParser.{ scalar, * }

// array and element parser
import anorm.Column.{ columnToArray, stringToArray }

val res: List[Array[String]] =
  SQL("SELECT str_arr FROM tbl").as(scalar[Array[String]].*)

Convenient parsing functions is also provided for arrays with SqlParser.array[T](...)and SqlParser.list[T](...).

Batch update

When you need to execute SQL statement several times with different arguments, batch query can be used (e.g. to execute a batch of insertions).

import anorm.BatchSql

val batch = BatchSql(
  "INSERT INTO books(title, author) VALUES({title}, {author})", 
  Seq[NamedParameter]("title" -> "Play 2 for Scala", 
    "author" -> "Peter Hilton"),
  Seq[NamedParameter]("title" -> "Learning Play! Framework 2", 
    "author" -> "Andy Petrella"))

val res: Array[Int] = batch.execute() // array of update count

Batch update must be called with at least one list of parameter. If a batch is executed with the mandatory first list of parameter being empty (e.g. Nil), only one statement will be executed (without parameter), which is equivalent toSQL(statement).executeUpdate().

Edge cases

Type of parameter value should be visible, to be properly set on SQL statement.
Using value as Any, explicitly or due to erasure, leads to compilation error No implicit view available from Any => anorm.ParameterValue.

// Wrong #1
val p: Any = "strAsAny"
SQL("SELECT * FROM test WHERE id={id}").
  on('id -> p) // Erroneous - No conversion Any => ParameterValue

// Right #1
val p = "strAsString"
SQL("SELECT * FROM test WHERE id={id}").on('id -> p)

// Wrong #2
val ps = Seq("a", "b", 3) // inferred as Seq[Any]
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on('a -> ps(0), // ps(0) - No conversion Any => ParameterValue
    'b -> ps(1), 
    'c -> ps(2))

// Right #2
val ps = Seq[anorm.ParameterValue]("a", "b", 3) // Seq[ParameterValue]
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on('a -> ps(0), 'b -> ps(1), 'c -> ps(2))

// Wrong #3
val ts = Seq( // Seq[(String -> Any)] due to _2
  "a" -> "1", "b" -> "2", "c" -> 3)

val nps: Seq[NamedParameter] = ts map { t => 
  val p: NamedParameter = t; p
  // Erroneous - no conversion (String,Any) => NamedParameter
}

SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").on(nps :_*) 

// Right #3
val nps = Seq[NamedParameter]( // Tuples as NamedParameter before Any
  "a" -> "1", "b" -> "2", "c" -> 3)
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on(nps: _*) // Fail - no conversion (String,Any) => NamedParameter

In some cases, some JDBC drivers returns a result set positioned on the first row rather than before this first row (e.g. stored procedured with Oracle JDBC driver).
To handle such edge-case, .withResultSetOnFirstRow(true) can be used as following.

SQL("EXEC stored_proc {arg}").on("arg" -> "val").withResultSetOnFirstRow(true)
SQL"""EXEC stored_proc ${"val"}""".withResultSetOnFirstRow(true)

SQL"INSERT INTO dict(term, definition) VALUES ($term, $definition)".
  withResultSetOnFirstRow(true).executeInsert()
// Also needed on executeInsert for such driver, 
// as a ResultSet is returned in this case for the generated keys

Using Pattern Matching

You can also use Pattern Matching to match and extract the Row content. In this case the column name doesn’t matter. Only the order and the type of the parameters is used to match.

The following example transforms each row to the correct Scala type:

import java.sql.Connection
import anorm._

trait Country
case class SmallCountry(name:String) extends Country
case class BigCountry(name:String) extends Country
case object France extends Country

val patternParser = RowParser[Country] {
  case Row("France", _) => Success(France)
  case Row(name:String, pop:Int) if (pop > 1000000) => Success(BigCountry(name))
  case Row(name:String, _) => Success(SmallCountry(name))
  case row => Error(TypeDoesNotMatch(s"unexpected: $row"))
}

def countries(implicit con: Connection): List[Country] =
  SQL("SELECT name,population FROM Country WHERE id = {i}").
    on("i" -> "id").as(patternParser.*)

Using for-comprehension

Row parser can be defined as for-comprehension, working with SQL result type. It can be useful when working with lot of column, possibly to work around case class limit.

import anorm.SqlParser.{ str, int }

val parser = for {
  a <- str("colA")
  b <- int("colB")
} yield (a -> b)

val parsed: (String, Int) = SELECT("SELECT * FROM Test").as(parser.single)

Retrieving data along with execution context

Moreover data, query execution involves context information like SQL warnings that may be raised (and may be fatal or not), especially when working with stored SQL procedure.

Way to get context information along with query data is to use executeQuery():

import anorm.SqlQueryResult

val res: SqlQueryResult = SQL("EXEC stored_proc {code}").
  on('code -> code).executeQuery()

// Check execution context (there warnings) before going on
val str: Option[String] =
  res.statementWarning match {
    case Some(warning) =>
      warning.printStackTrace()
      None

    case _ => res.as(scalar[String].singleOpt) // go on row parsing
  }

Working with optional/nullable values

If a column in database can contain Null values, you need to parse it as an Optiontype.

For example, the indepYear of the Country table is nullable, so you need to match it asOption[Int]:

case class Info(name: String, year: Option[Int])

val parser = str("name") ~ get[Option[Int]]("indepYear") map {
  case n ~ y => Info(n, y)
}

val res: List[Info] = SQL("Select name,indepYear from Country").as(parser.*)

If you try to match this column as Int it won’t be able to parse Null values. Suppose you try to retrieve the column content as Int directly from the dictionary:

SQL("Select name,indepYear from Country")().map { row =>
  row[String]("name") -> row[Int]("indepYear")
}

This will produce an UnexpectedNullableFound(COUNTRY.INDEPYEAR) exception if it encounters a null value, so you need to map it properly to an Option[Int].

A nullable parameter is also passed as Option[T]T being parameter base type (seeParameters section thereafter).

Passing directly None for a NULL value is not supported, as inferred asOption[Nothing] (Nothing being unsafe for a parameter value). In this case,Option.empty[T] must be used.

// OK: 

SQL("INSERT INTO Test(title) VALUES({title})").on("title" -> Some("Title"))

val title1 = Some("Title1")
SQL("INSERT INTO Test(title) VALUES({title})").on("title" -> title1)

val title2: Option[String] = None
// None inferred as Option[String] on assignment
SQL("INSERT INTO Test(title) VALUES({title})").on("title" -> title2)

// Not OK:
SQL("INSERT INTO Test(title) VALUES({title})").on("title" -> None)

// OK:
SQL"INSERT INTO Test(title) VALUES(${Option.empty[String]})"


프로젝트에 Anorm 을 추가하자.

Anorm 과 JDBC 플러그인을 당신의 디펜던시에 추가해야합니다.

libraryDependencies ++= Seq(
  jdbc,
  "com.typesafe.play" %% "anorm" % "2.5.0"
)

SQL 쿼리 실행하기 

먼저 어떻게 SQL 쿼리를 실행하는지 알아 봅시다.

첫번째로 anorm._  를 임포트 하시고 SQL object 를 쿼리를 만들기 위해 사용합니다.
쿼리를 실행하기 위해서는 .  Connection 이 필요한데 play.api.db.DB 헬퍼로 얻을 수 있습니다.
(play.api.db.DB 는 Play 2.5 에서 deprecated 되었으며 아직 정확한 사용방법에 대해서 나중에 학습하게 되면 블로깅 하죵

import anorm._
import play.api.db.DB

DB.withConnection { implicit c =>
  val result: Boolean = SQL("Select 1").execute()
}

execute() 메소드는 Boolean 값을 리턴 받습니다. 실행의 성공 여부에 따라서 말이죠.

To execute an update, you can use executeUpdate(), which returns the number of rows updated.

val result: Int = SQL("delete from City where id = 99").executeUpdate()

If you are inserting data that has an auto-generated Long primary key, you can callexecuteInsert().

val id: Option[Long] = 
  SQL("insert into City(name, country) values ({name}, {country})")
  .on('name -> "Cambridge", 'country -> "New Zealand").executeInsert()

When key generated on insertion is not a single LongexecuteInsert can be passed aResultSetParser to return the correct key.

import anorm.SqlParser.str

val id: List[String] = 
  SQL("insert into City(name, country) values ({name}, {country})")
  .on('name -> "Cambridge", 'country -> "New Zealand")
  .executeInsert(str.+) // insertion returns a list of at least one string keys

Since Scala supports multi-line strings, feel free to use them for complex SQL statements:

val sqlQuery = SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = 'FRA';
  """
)

If your SQL query needs dynamic parameters, you can declare placeholders like{name} in the query string, and later assign a value to them:

SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = {countryCode};
  """
).on("countryCode" -> "FRA")

You can also use string interpolation to pass parameters (see details thereafter).

In case several columns are found with same name in query result, for example columns named code in both Country and CountryLanguage tables, there can be ambiguity. By default a mapping like following one will use the last column:

import anorm.{ SQL, SqlParser }

val code: String = SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = {countryCode}
  """)
  .on("countryCode" -> "FRA").as(SqlParser.str("code").single)

If Country.Code is ‘First’ and CountryLanguage is ‘Second’, then in previous examplecode value will be ‘Second’. Ambiguity can be resolved using qualified column name, with table name:

import anorm.{ SQL, SqlParser }

val code: String = SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = {countryCode}
  """)
  .on("countryCode" -> "FRA").as(SqlParser.str("Country.code").single)
// code == "First"

When a column is aliased, typically using SQL AS, its value can also be resolved. Following example parses column with country_lang alias.

import anorm.{ SQL, SqlParser }

val lang: String = SQL(
  """
    select l.language AS country_lang from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = {countryCode}
  """).on("countryCode" -> "FRA").
    as(SqlParser.str("country_lang").single)

Columns can also be specified by position, rather than name:

import anorm.SqlParser.{ str, float }
// Parsing column by name or position
val parser = 
  str("name") ~ float(3) /* third column as float */ map {
    case name ~ f => (name -> f)
  }

val product: (String, Float) = SQL("SELECT * FROM prod WHERE id = {id}").
  on('id -> "p").as(parser.single)

If the columns are not strictly defined (e.g. with types that can vary), theSqlParser.folder can be used to fold each row in a custom way.

import anorm.{ RowParser, SqlParser }

val parser: RowParser[Map[String, Any]] = 
  SqlParser.folder(Map.empty[String, Any]) { (map, value, meta) => 
    Right(map + (meta.column.qualified -> value))
  }

val result: List[Map[String, Any]] = SQL"SELECT * FROM dyn_table".as(parser.*)

If the columns are not strictly defined (e.g. with types that can vary), theSqlParser.folder can be used to fold each row in a custom way.

import anorm.{ RowParser, SqlParser }

val parser: RowParser[Map[String, Any]] = 
  SqlParser.folder(Map.empty[String, Any]) { (map, value, meta) => 
    Right(map + (meta.column.qualified -> value))
  }

val result: List[Map[String, Any]] = SQL"SELECT * FROM dyn_table".as(parser.*)

Table alias

With some databases, it’s possible to define aliases for table (or for sub-query), as in the following example.

=> SELECT * FROM test t1 JOIN (SELECT * FROM test WHERE parent_id ISNULL) t2 ON t1.parent_id=t2.id WHERE t1.id='bar';
 id  | value  | parent_id | id  | value  | parent_id 
-----+--------+-----------+-----+--------+-----------
 bar | value2 | foo       | foo | value1 | 
(1 row)

Unfortunately, such aliases are not supported in JDBC, so Anorm introduces theColumnAliaser to be able to define user aliases over columns.

import anorm._

val parser: RowParser[(String, String, String, Option[String])] = SqlParser.str("id") ~ SqlParser.str("value") ~ SqlParser.str("parent.value") ~ SqlParser.str("parent.parent_id").? map(SqlParser.flatten)

val aliaser: ColumnAliaser = ColumnAliaser.withPattern((3 to 6).toSet, "parent.")

val res: Try[(String, String, String, Option[String])] = SQL"""SELECT * FROM test t1 JOIN (SELECT * FROM test WHERE parent_id ISNULL) t2 ON t1.parent_id=t2.id WHERE t1.id=${"bar"}""".asTry(parser.single, aliaser)

res.foreach {
  case (id, value, parentVal, grandPaId) => ???
}

SQL queries using String Interpolation

Since Scala 2.10 supports custom String Interpolation there is also a 1-step alternative to SQL(queryString).on(params) seen before. You can abbreviate the code as:

val name = "Cambridge"
val country = "New Zealand"

SQL"insert into City(name, country) values ($name, $country)"

It also supports multi-line string and inline expresions:

val lang = "French"
val population = 10000000
val margin = 500000

val code: String = SQL"""
  select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where l.Language = $lang and c.Population >= ${population - margin}
    order by c.Population desc limit 1"""
  .as(SqlParser.str("Country.code").single)

This feature tries to make faster, more concise and easier to read the way to retrieve data in Anorm. Please, feel free to use it wherever you see a combination ofSQL().on() functions (or even an only SQL() without parameters).

By using #$value instead of $value, interpolated value will be part of the prepared statement, rather being passed as a parameter when executing this SQL statement (e.g. #$cmd and #$table in example bellow).

val cmd = "SELECT"
val table = "Test"

SQL"""#$cmd * FROM #$table WHERE id = ${"id1"} AND code IN (${Seq(2, 5)})"""

// prepare the SQL statement, with 1 string and 2 integer parameters:
// SELECT * FROM Test WHERE id = ? AND code IN (?, ?)

Generated parsers

The macro namedParser[T] can be used to create a RowParser[T] at compile-time, for any case class T.

import anorm.{ Macro, RowParser }

case class Info(name: String, year: Option[Int])

val parser: RowParser[Info] = Macro.namedParser[Info]
/* Generated as:
get[String]("name") ~ get[Option[Int]]("year") map {
  case name ~ year => Info(name, year)
}
*/

val result: List[Info] = SQL"SELECT * FROM list".as(parser.*)

The similar macros indexedParser[T] and offsetParser[T] are available to get column values by positions instead of names.

import anorm.{ Macro, RowParser }

case class Info(name: String, year: Option[Int])

val parser1: RowParser[Info] = Macro.indexedParser[Info]
/* Generated as:
get[String](1) ~ get[Option[Int]](2) map {
  case name ~ year => Info(name, year)
}
*/

val result1: List[Info] = SQL"SELECT * FROM list".as(parser1.*)

// With offset
val parser2: RowParser[Info] = Macro.offsetParser[Info](2)
/* Generated as:
get[String](2 + 1) ~ get[Option[Int]](2 + 2) map {
  case name ~ year => Info(name, year)
}
*/

val result2: List[Info] = SQL"SELECT * FROM list".as(parser2.*)

To indicate custom names for the columns to be parsed, the macro parser[T](names)can be used.

import anorm.{ Macro, RowParser }

case class Info(name: String, year: Option[Int])

val parser: RowParser[Info] = Macro.parser[Info]("a_name", "creation")
/* Generated as:
get[String]("a_name") ~ get[Option[Int]]("creation") map {
  case name ~ year => Info(name, year)
}
*/

val result: List[Info] = SQL"SELECT * FROM list".as(parser.*)

The RowParser exposed in the implicit scope can be used as nested one generated by the macros.

case class Bar(lorem: Float, ipsum: Long)
case class Foo(name: String, bar: Bar, age: Int)

import anorm._

// nested parser
implicit val barParser = Macro.parser[Bar]("bar_lorem", "bar_ipsum")

val fooBar = Macro.namedParser[Foo] /* generated as:
  get[String]("name") ~ barParser ~ get[Int]("age") map {
    case name ~ bar ~ age => Foo(name, bar, age)
  }
*/

val result: Foo = SQL"""SELECT f.name, age, bar_lorem, bar_ipsum 
  FROM foo f JOIN bar b ON f.name=b.name WHERE f.name=${"Foo"}""".
  as(fooBar.single)

The anorm.macro.debug system property can be set to true (e.g. sbt -Danorm.macro.debug=true ...) to debug the generated parsers.

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

 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 코드를 당신이 컨트롤함에 따라 코드를 단순화 시킬 수 있게 될 것이다. 

play2 에서 Anorm 으로 PostgreSQL 사용하기 


* 일단 IntelliJ 에 play2 개발 환경이 갖춰져 있다는 전제입니다. 

* play 2.48     ( 최신은 2.5 이나 정보를 찾는 면에서 있어서 어려움이 있다) 
* anorm 2.5
* posgresql driver  9.3-1102-jdbc41

1. application.conf 

- DB 접근 설정을 합니다.


db.default.url="jdbc:postgresql://userip/DatabaseName"

db.default.username= your user
db.default.password= your password
db.default.driver=org.postgresql.Driver

2. build.sbt 에서

- JDBC postgresql 라이브러리에 대한 종속성을 추가해줍니다. 자동으로 다운로드 받습니다.

- postgresql 드라이버 버전이 "9.1-901.jdbc4" 로  안되서 올렸더니 잘 됩니다.

libraryDependencies ++= Seq(
jdbc ,
"com.typesafe.play" %% "anorm" % "2.5.0",
cache ,
ws ,
specs2 % Test,
"org.postgresql" % "postgresql" % "9.3-1102-jdbc41")


3. 코딩 

- 모델 클래스 작성 

case class Thing (
thing_id : Int,
name : String
)


*  Anorm 다루기  (MyBatis 에 가까운 놈임, Slick 나 Squeryl 이 ORM. 개인적으로 ORM 은 별로 같아요..

(참고로 DB.withConnection 은 최근 버전에서는 deprecated 됬습니다만... 옛날버전이 자료가 많아요 OTL )


1) 스트림 API 사용해서 데이터 가져와서 객체에 담기 

def findThingByID_streaming(id : Int): List[Thing] = DB.withConnection {

implicit connection =>
val sql: SqlQuery = SQL("select * from tbl_thing_info where thing_id = {id}")
sql.on("id" -> id)

sql().map ( row =>

Thing(row[Int]("thing_id"), row[String]("name"))

).toList
}

2) paser 를 이용해서  데이터 가져와서 객체에 담기 


val thingParser: RowParser[Thing] = {

int("thing_id") ~ str("name") map {
case thing_id ~ name =>
Thing(thing_id, name)
}
}

import anorm.ResultSetParser

val thingsParser: ResultSetParser[List[Thing]] = {
thingParser *
}



def findThingByID_parser(id : Int): List[Thing] = DB.withConnection {
implicit connection =>
val sql = SQL("select * from tbl_thing_info where thing_id = {id}").on("id" -> id)
sql.as(thingsParser)
}



p.s

ORM 경우는 Slick 이 있는데 개인적으로 ORM 을 좋아하지 않아서 사용안한다. 대략적인 사용법은 아래에 정리함.



참고 :https://www.playframework.com/documentation/2.4.x/ScalaAnorm

1. EC2 에 Amazon Linux AMI 로 생성

     별개 없음

2. Java 8 로 업그레이드   (이거 말고 아무것도 필요 없음)   

sudo yum remove java-1.7.0-openjdk sudo yum install java-1.8.0


3. 자신의 PC 에서 새로운  Play-Scala 만들기 

        Activator 를 here서 다운받고 환경 설정한 후 에 적당한 위치의 콘솔에서 

activator new helloworld 엔터~!

  1) minimal-akka-java-seed
  2) minimal-akka-scala-seed
  3) minimal-java
  4) minimal-scala
  5) play-java
  6) play-scala


6번 선택하자. 그럼 기본적인 프로젝트가 만들어진다. 로컬에서 테스트하기위해 activator run 을 한 후에 브라우저에서 localhost:9000 해보면 나온다.

4.  EC2  에 디플로이 하기 

   - 자신의 PC 

    

      프로젝트 안에서

activator clean stage // 이거 치면 target/universal 가 생깁니다. target/universal // 이동해서 stage 를 zip 으로 압축해서 EC2 로 보냅니다.


여기서 stage 는 클라우드같은곳에 디플로이할때 activator 라든지 특별한 종속성이 필요없게 하기위한 방식입니다. 그냥 java 만 설치된 곳이면 실행할 수 있게 하는거죠.


      -   EC2 에서 

     받은 프로젝트의 압축을 풀고 , 프로젝트의 conf/application.conf  파일에서 

          play.crypto.secret = "changeme" 를 아래와 같이 아무거나 넣어서 수정 

          play.crypto.secret = "dsfwef#$#@F##F#@F@#RE#@F"

(키에 관한 자세한 것은 https://www.playframework.com/documentation/2.4.x/ApplicationSecret 참고) 

5.  EC2 의  Security Group 수정하기 

     아래처럼 9000 번 세팅해줍니다. 

Deploy Play-Scala Applications on Amazon EC2


 6. 실행

  /bin 으로 들어가서  sudo ./helloworld  하면 실행됩니다.

   자신의 브라우저에서  http://ec2ip:9000 으로 확인하세요.



Play 2.x with  IntelliJ 시작하기 

          이 포스트는  Play2 를 IntelliJ 를 이용하여 실행 하는 방법에 대해 말합니다. 

          Scala 기반이며 프로토타입 프로젝트는 나중에 AWS Beanstalk 에 올릴 예정입니다.

프로젝트 만들기 

프로젝트 만들기전에  Scala 플러그인을 다운로드 받자  downloaded and enabled  그리고 

    1. 만약 프로젝트가 현재 IDEA 에서 열려 있지 않으면 , 새로운 프로젝트를 만들자. 새 프로젝트 마법사를 연다. 

  1. 2. 왼쪽 패널에서 Scala를 선택하고 오른쪽에서 Play 2.x 를 선택하자

  2. play_new_project
    Next 클릭!
  3. 프로젝트와 모듈을 선택하자. 
    Play_new_wizard_page
    끝  !!   (역주 :  프로젝트폴더가 제대로 안보이다가 sbt 플러그인을 다시설치 하니 보이더라
    play_project_window

Play 2.x project 를 IntelliJ 에 임포트하기 

IntelliJ IDEA 에서는 이미 존재하는  Play 2.x project 를 임포트 할 수도 있다.


  1. 1. 메인 메뉴에서 select File | New | Project from Existing Sources 를 선택 
  2. 2. Import  모델을 선택한다. 
  3. sbt_import_project
  4. 다음 페이지에서  SBT options 를 선택하고 Finish 를 누른다.
    sbt_project_wizard_import

프로젝트 세팅들 체킹하기 

  1. 메인메뉴상에서  select File | Project Structure 로 가서 모듈 디펜던시에 경고가 없는지 확인하라.
  2. play_project_structure
      Scala compiler library  가 세팅되었는지도 확인하시고
    play_project_structure2

코드 어시스턴스 사용하기 

play_coding_assistance

Play 2.x 어플리케이션 실행하기 

  1.  빨강색 원안의 시작 삼각형의 시작 버튼을 누르면 됨 
  2. 그리고 웹브라우저 http://localhost:9000

Play 2.x 디버깅하기 

  1. 메인메뉴에서  Debug 클릭 혹은 아래 동그라미에서 벌레 클릭 ~




+ Recent posts