🕌
【Scala-CLI×SQLite】ワンファイルで作る “極小メモ帳” ── cs bootstrap に挫折したあなたへ
0. なにをつくるか
コマンド | 挙動 |
---|---|
./memo add "<本文>" | メモ追加 |
./memo list | 一覧表示 (id: 内容) |
./memo delete <id> | 指定DIを削除 |
SQLite に永続化し、1 ファイルのソースをそのままバイナリ化します。
1. 準備(1 分)
# Scala CLI
brew install scala-cli # v1.3 以上推奨
# (任意)SQLite CLI が欲しければ
brew install sqlite
2. Memo.scala を丸ごと書く
mkdir memo-app/bin && cd memo-app && touch Memo.scala
//> using scala "2.13.14"
//> using lib "org.xerial:sqlite-jdbc:3.50.3.0"
//> using jar resources
import java.sql.Connection
import java.sql.DriverManager
import scala.util.Using
object Memo {
private val DbUrl = "jdbc:sqlite:memo.db"
def main(args: Array[String]): Unit ={
if (args.isEmpty) {usage(); return}
initTable()
args.head match {
case "add" => add(args.drop(1).mkString(" "))
case "list" => list()
case "delete" => delete(args.lift(1).flatMap(_.toIntOption))
case _ => usage()
}
}
// サブコマンド
private def add(text: String): Unit =
if (text.isEmpty) println("No memo text.")
else{
exec("INSERT INTO memos(content) VALUES (?)")(_.setString(1, text))
println("Memo added.")
}
private def list(): Unit =
query("SELECT id, content FROM memos") { rs =>
while(rs.next()) println(s"${rs.getInt(1)}: ${rs.getString(2)}")
}
private def delete(idOpt: Option[Int]): Unit =
idOpt match {
case None => println("Need id.")
case Some(id) =>
val rows = exec("DELETE FROM memos WHERE id = ?")(_.setInt(1, id))
println(if (rows == 0) "Not found." else "Memo deleted.")
}
private def usage(): Unit =
println(
"""Usage:
| memo add <text>
| memo list
| memo delete <id>""".stripMargin
)
// DB helpers
private def withConn[A](f: Connection => A): A =
Using.resource(DriverManager.getConnection(DbUrl))(f)
private def exec(sql: String)(prep: java.sql.PreparedStatement => Unit): Int =
withConn { conn =>
val st = conn.prepareStatement(sql)
prep(st)
st.executeUpdate()
}
private def query(sql: String)(body: java.sql.ResultSet => Unit): Unit =
withConn {conn =>
val rs = conn.createStatement().executeQuery(sql)
body(rs)
}
private def initTable(): Unit =
exec(
"""CREATE TABLE IF NOT EXISTS memos(
| id INTEGER PRIMARY KEY AUTOINCREMENT,
| content TEXT NOT NULL
|)""".stripMargin
)(_ => ())
}
“//> using …” って?
- scala : コンパイルする Scala バージョン
- lib : 依存ライブラリ (ここでは SQLite JDBC)
3. 一撃バイナリ化
# JVM 版(5 秒で終わる)
scala-cli package Memo.scala -o bin/memo # bin/memo は JAR ランチャ
# 完全ネイティブ(GraalVM を自動取得 → 初回数分)
scala-cli package Memo.scala --native-image --output bin/memo
ネイティブ版は 実行ファイル1つだけ 配れば OK。
(GraalVM に 3 GB 近いキャッシュが必要なので初回は気長に)
4. 動作チェック:追加 → 一覧 → 削除
% ./bin/memo add "aaa"
Memo added.
% ./bin/memo add "bbb"
Memo added.
% ./bin/memo list
1: aaa
2: bbb
% ./bin/memo delete 1
Memo deleted.
% ./bin/memo list
2: bbb
ファイル実体はカレントに memo.db としてできあがり。
sqlite3 memo.db .dump
で中身も確認できます。
まとめ 💡
- Coursier bootstrap の依存地獄・シェル芸から解放
- 依存宣言は ソース先頭の 1 行
- scala-cli package だけで 配布可能バイナリ 完成
「Go みたいに単体バイナリを配りたい」──
Scala 2 でも普通にできます。ぜひ試してみてください!
Discussion