Scalaでデータベースを操作する方法を紹介
[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
多くのプログラミング言語ではデータベースを操作する方法が用意されています。
この記事では、データベースから値を抽出する簡単な例をあげ、Scalaでデータベースを操作する方法を紹介します。
リレーショナルデータベースを操作する
データベースにはリレーショナルデータベース、ドキュメントデータベースなど多くの種類のデータベースが存在します。
この記事ではデータを表で管理するリレーショナルデータベース(RDB)を扱います。
この記事で扱うデータ
この記事では、以下のテーブルを例に年齢が20歳以上の人を抽出する方法を説明していきます。
テーブル名: person
id | name | age |
---|---|---|
1 | Taro | 15 |
2 | Hana | 20 |
3 | Kaz | 49 |
4 | Tomo | 12 |
5 | Yuko | 48 |
6 | Jhon | 32 |
7 | Anne | 31 |
8 | Chris | 56 |
9 | Shinichi | 18 |
10 | Megumi | 17 |
データベースからデータを抽出するにはSELECT文を使用する
リレーショナルデータベースは「SQL」を使用して操作します。
「personテーブル」から20歳以上の人を抽出する場合は、「SELECT文」を使用します。
SELECT name, age FROM person WHERE age >= 20 ORDER BY name ;
データベースから取得した値をオブジェクトにマッピングする
データベースから取得した値をプログラムで扱うには、ケースクラスを定義しておきデータベースから取得した値をオブジェクトにマッピングすると便利です。
本記事の例ではpersonテーブルから取得した値をオブジェクトにマッピングするために以下のケースクラスを使用します。
case class Person(id: Long, name: String, age: Int)
Scala標準のデータベースAPIはない
Scalaの標準ライブラリにはデータベースを操作するAPIは用意されていません。
ScalaではJavaのAPI(JDBC)を使用するか、外部ライブラリを使用してデータベースを操作します。
外部のライブラリにはSQLやJDBC APIを簡単に扱えるようにしたものや、データベースの値をオブジェクトにマッピングしてくれるようなものがあります。
JDBC APIを使う
JavaのライブラリにはJDBC(Java Database Connectivity)という、データベースを操作するAPIが用意されています。
ScalaからもJDBCを使用してデータベースにアクセスできます。
JDBCを使用してpersonテーブルから20歳以上の人を抽出する例を見てみます。
以下のコードはJDBCを使ってデータベースからデータを抽出する例です。
JDBCを使用する場合は、SQLをコードに直接記述してデータベースを操作します。
SQL中の「疑問符?
」は、値を動的に設定するための構文です。
このように、値を動的に設定する方法をプレースホルダーと呼びます。
// データベースから取得した値(ResultSet)をPersonクラスに変換する関数 implicit val toPerson: ResultSet => Person = (rs: ResultSet) => Person(rs.getLong("id"), rs.getString("name"), rs.getInt("age")) // SQL定義 // SQLを文字列で記述する val stmt: PreparedStatement = con.prepareStatement(""" SELECT id, name, age FROM person WHERE age >= ? ORDER BY name """.stripMargin) // WHERE句の条件式に値を設定 stmt.setInt(1, 20) // データベースから取得した値(ResultSet)をPersonクラスのリストに変換する @scala.annotation.tailrec def selectPersons(rs: ResultSet, persons: List[Person]): List[Person] = if(!rs.next()) persons else selectPersons(rs, toPerson(rs) :: persons) // SQLを実行して結果を取得する val persons = selectPersons(stmt.executeQuery(), List.empty).reverse persons.foreach(p => println(s"name=${p.name}, age=${p.age}")) stmt.close()
実行結果は以下のようになります。
name=Anne, age=31 name=Chris, age=56 name=Hana, age=20 name=Jhon, age=32 name=Kaz, age=49 name=Yuko, age=48
外部のライブラリを使う
次に外部のライブラリを使用する方法を説明します。
Scalaにも他のプログラミング言語のようにデータベースを操作するための外部ライブラリが存在します。
ここでは、以下の2つのライブラリを紹介します。
JDBCではSQLをコードに直接記述する必要がありますが、上記2つのライブラリはSQLをコードに直接記述する方法(Plain SQL)の他にDSL(domain specific language)を使ってデータベースを操作することができます。
Plain SQL
SQLをコードに直接記述する方法です。
SQLの文法を知っていれば利用できるので、DSLを使用する方法と比較して学習コストは低いです。
ただし、コンパイル時に以下がチェックがされない欠点があります。
- テーブルのカラムに対する型はコンパイル時にチェックされません。
- SQLに文法上の間違いがあってもコンパイル時にエラーになりません。
DSL
SQLに似た構文でデータベースにアクセスするルールを記載する方法です。
使用するライブラリに特化したDSLを覚える必要があるため、Plain SQLと比較して学習コストは高いです。
ただし、コンパイル時に以下がチェックされる利点があります。
- テーブルのカラムに対する型はコンパイル時にチェックされます。
- 文法の間違いはコンパイル時にエラーになります。
Slickを使う
注:SlickはまだScala 3に対応していません(2023年02月04日現在)。以下の記述はScala 2系用のものです。今後変わる見込みです。
Slickは、Lightbendによって開発されたScala用のライブラリです。
Scalaのコレクションを扱うようにデータベースを操作できることが特徴です。
また、データベースへの処理は非同期に実行されます。
Slickを使用した例を見てみます。
Slickの場合、まず最初にデータベーススキーマを定義します。
この定義は手動で作成することもできますが、Slickの「Code Generator」を使用して存在するデータベースから自動的に作成することができます。
class PersonTable(tag: Tag) extends Table[(Long, String, Int)](tag, "person") { def id = column[Long]("id", O.PrimaryKey) def name = column[String]("name") def age = column[Int]("age") def * = (id, name, age) } lazy val person = new TableQuery[PersonTable](tag => new PersonTable(tag))
以下はPlain SQLを使用した場合の例です。
// データベースから読み込んだデータをPersonクラスに変換する関数 implicit val getPersonResult = GetResult(r => Person(r.nextLong(), r.nextString(), r.nextInt())) // SQLの定義 val query = sql""" SELECT id, name, age FROM person WHERE age >= 20 ORDER BY name""".as[Person] // Queryを実行(非同期で実行するため、Awaitで処理の完了を待っている) Await.result(db.run(query), Duration.Inf) .foreach(p => println(s"name=${p.name}, age=${p.age}"))
以下はDSLを使用した場合の例です。
// DSLの定義 val query = person .filter(_.age >= 20) .sortBy(_.name) .result // Queryを実行(非同期で実行するため、Awaitで処理の完了を待っている) Await.result(db.run(query), Duration.Inf) .map(p => Person.tupled(p)) .foreach(p => println(s"name=${p.name}, age=${p.age}"))
Plain SQL, DSLの両方とも実行結果は以下のようになります。
name=Anne, age=31 name=Chris, age=56 name=Hana, age=20 name=Jhon, age=32 name=Kaz, age=49 name=Yuko, age=48
ScalikeJDBCを使う
ScalikeJDBCは、いかに効率的にSQLを記述するかを目的に開発されたライブラリです。
SQLを知っている人であればSlickよりも簡単に使えるようになっています。
ScalikeJDBCを使用した例を見てみます。
データベースから取得したレコードをケースクラスにマッピングするためにコンパニオンオブジェクトを定義します。
object Person extends SQLSyntaxSupport[Person]: override val tableName = "person" def apply(rs: WrappedResultSet): Person = Person(rs.long("id"), rs.string("name"), rs.int("age"))
以下はPlain SQLを使用した場合の例です。
val persons = sql""" SELECT id, name, age FROM person WHERE age >= 20 ORDER BY name""" .map(rs => Person(rs)).list() persons.foreach(p => println(s"name=${p.name}, age=${p.age}"))
以下はDSLを使用した場合の例です。
val a = Person.syntax("a") val persons: Seq[Person] = withSQL { select .from(Person.as(a)) .where.ge(a.age, 20) .orderBy(a.name) }.map(rs => Person(rs)).list() persons.foreach(p => println(s"name=${p.name}, age=${p.age}"))
Plain SQL, DSLの両方とも実行結果は以下のようになります。
name=Anne, age=31 name=Chris, age=56 name=Hana, age=20 name=Jhon, age=32 name=Kaz, age=49 name=Yuko, age=48
最後に
本記事では1つのテーブルから単純な条件に該当するデータを抽出するといった簡単な例をJDBC,Slick,ScalikeJDBCで実施する方法を説明しました。 これだけではアプリケーションを構築するには情報が足りないと思います。 複雑な条件で複数のテーブルからデータを抽出する方法や、データの新規作成、更新、削除など別の記事で紹介していきます。