Slick?
Slick의 Document 페이지를 가면 아래와 같이 슬릭을 설명합니다.
Functional Relational Mapping for Scala
스칼라를 위한 함수적 관계 매핑 라이브러리
Slick is an advanced, comprehensive database access library for Scala with strongly-typed, highly composable APIs.
슬릭은 고급스러우면서도 포괄적인, 강타입이면서 추상적으로 구성 가능한 스칼라용 데이터베이스 접근 라이브러리이다
Slick을 사용하면 데이터베이스를 Scala의 collection처럼 접근하고, 필터링하고, 변환할 수 있으며, 이러한 접근방식 덕분에 마치 컴파일 타임에 쿼리를 짜듯 프로그래밍 할 수 있습니다.
Slick 쿼리들은 가장 바깥에서는 Future로 감싸여있어 비동기로 동작하면서, for comprehension을 사용할 수 있어 여러 동작을 하나로 묶을 수 있고, Akka-streams나 FS2, ZIO같은 다른 Stream 라이브러리에도 쉽게 연결할 수 있습니다.
실제 구성을 보면 Java의 Builder 패턴을 쓰듯이 코드를 짜게되고, Kotlin이나 Scala로 보면 Collection을 조작하듯이 코드를 짤 수 있습니다.
// 테이블 구성 예시
class T(tag: Tag) extends Table[(Int, String, Option[String], Double)](tag, Some("myschema"), "mytable") {
def id = column[Int]("id")
def myString = column[String]("myString")
def optString = column[Option[String]]("optString")
def price = column[Double]("PRICE")
def * = (id, myString, optString, price)
}
TableQuery[T] // FROM myTable
.filter(_.myString === "123") // WHERE myString = "123"
.filter(_.price === 1.23) // AND price = 1.23
.map(_.id) // SELECT id
.result
Slick 사용법
테이블 매핑
Slick은 테이블을 정의하기 위해 Table 타입을 상속한 클래스를 만들어야 합니다.
class MyTable(tag: Tag) extends Table[(Int, String, Option[String], Double)](tag, Some("mySchema"), "mytable") {
def id = column[Int]("id", O.PrimaryKey)
def myString = column[String]("myString")
def optString = column[Option[String]]("optString")
def price = column[Double]("PRICE")
def * = (id, myString, optString, price)
}
Table 코드
Table 코드를 보면 아래와 같이 나옵니다.
abstract class Table[T](_tableTag: Tag, _schemaName: Option[String], _tableName: String)
extends AbstractTable[T](_tableTag, _schemaName, _tableName) { table =>
final type TableElementType = T
...
관련된 필드들을 하나하나 풀어보면 아래와 같습니다
_tableTag: Tag
Table 타입을 상속받기 위해 설정해야 합니다. slick 내부적으로 Row 단위 정보를 구분하거나, Table 단위 정보를 구분하는 등 다양한 용도로 사용하는데, 사용자 입장에서는 크게 신경쓰지 않아도 되는 값입니다.
_schemaName: Option[String]
데이터베이스에서 요구하는 스키마의 이름을 말하며, 일반적으로 필요치 않아 정의할 필요는 없습니다.
_tableName: String
데이터베이스 테이블 이름을 의미합니다.
[T]
테이블의 Entity 타입을 정의합니다.
예시의 Table 정의는 id, myString, optString, price column이 존재하는데, 이를 별도의 entity 클래스가 아닌 tuple로 정의하여 Tuple 타입이 되었습니다.
column 코드
Table을 상속하기 위한 데이터들을 정의하였으면 이후에는 Table의 column 정보들을 설정해야 합니다.
Table의 column은 column type으로 정의되고, 데이터 타입이나 constraint 정보들을 정의할 수 있습니다.
column 코드를 보면 아래와 같습니다.
def column[C](n: String, options: ColumnOption[C]*)(implicit tt: TypedType[C]): Rep[C]: { ... }
[C]
컬럼으로 설정할 데이터의 타입을 의미합니다. Int, Double, DateTime, Option[_] 등 기본적으로 다양한 타입을 지원합니다.
n: String
컬럼의 이름을 의미합니다. DB의 Table에 정의된 컬럼 이름을 적으면 됩니다.
options: ColumnOption[C]*
정의할 컬럼의 contraint 정보를 의미합니다. O.PrimaryKey, O.AutoInc, O.Unique, O.Length의 값을 설정할 수 있습니다.
특별한 * 설정
Slick의 테이블을 설정하기 위해서는* 함수를 정의해야 합니다.
* 함수는 Table 클래스에서 정의한 T의 정의로 변환될 수 있어야 합니다.
예시에서 T는 (Int, String, Option[String], Double)이므로, * 역시 해당 컬럼들을 튜플로 묶어서 정의되어 있습니다. def * = (id, myString, optString, price)
Entity class를 T에 사용한 튜플 대신 사용하기
예시의 코드처럼 설정하면 됩니다.
case class MyEntity(id: Int, myString: String, optString: Option[String], price: Double)
class MyTable(tag: Tag) extends Table[MyEntity](tag, Some("mySchema"), "mytable") {
def id = column[Int]("id", O.PrimaryKey)
def myString = column[String]("myString")
def optString = column[Option[String]]("optString")
def price = column[Double]("PRICE")
def * = (id, myString, optString, price).mapTo[MyEntity]
}
쿼리 실행
Database
Slick의 쿼리를 실행하기 위해서는 Database instance가 필요합니다.
// MySQL이 필요하다면 import slick.jdbc.MySQLProfile.api._
import slick.jdbc.H2Profile.api._
// 필요한 설정은 slick document 페이지를 통해 확인할 수 있습니다.
// https://scala-slick.org/doc/3.0.0/gettingstarted.html#database-configuration
val db = Database.forConfig("h2")
MyTable에서 WHERE 조건 질의하기
SQL 쿼리를 실행하려면 우선 테이블의 참조를 만들어야 합니다.
val myTable = TableQuery[MyTable]
테이블의 참조를 만들었다면 filter 함수를 통해 where 조건을 설정할 수 있습니다.
// SELECT * FROM MyTable WHERE id = 1;
myTable.filter(_.id === 1)
filter 외에도 filterOpt, filterNot 등의 필터링 함수가 있으니 필요에 따라 사용하면 됩니다.
이제 실제로 쿼리를 실행하려면 Databse instance를 통해 실행하면 됩니다.
db.run(myTable.filter(_.id === 1).result)
MyTable에 Insert 실행하기
테이블에 Insert를 실행하려면 entity 정보를 만들고 += 연산자를 통해 데이터를 넣으면 됩니다.
db.run(myTable += (1, "str", None, 1.0))
여러 데이터를 한번에 넣고 싶으면 ++= 연산자를 실행하면 됩니다.
db.run(
myTable ++= Seq((1, "str", None, 1.0), (2, "str2", None, 2.0))
)
MyTable에 Update 실행하기
테이블에 Update를 실행하고 싶다면 원하는 조건으로 필터링하고 update 함수를 실행하면 됩니다.
db.run(
myTable.filter(_.id === 1)
.map(_.myString)
.update("test")
)
MyTable에 Delete 실행하기
테이블에 Delete를 실행하고 싶으면 원하는 조건으로 필터링하고 delete 함수를 실행하면 됩니다.
db.run(
myTable.filter(_.id === 2)
.delete
)
Slick에 대해 간략하게 알아보았습니다.
Slick을 사용하면 데이터베이스를 Scala의 Collection을 다루듯 조작할 수 있고 Scala의 타입 시스템을 활용할 수 있어 코드를 짜기에도 편리한데, 이런 장점 외에 기존의 Spring-Data를 개발하던 사용자 입장에서는 조금 불편한 단점들도 존재합니다.
이런 장점이나 단점들은 차근차근 적어보겠습니다.