LoginSignup
30
29

More than 5 years have passed since last update.

Kotlinが健気にH2からJPAでJavaFXかわいい

Posted at

業務屋だってことりんを愛でたい

先日、日本ことりんユーザー会(自称)の勉強会に参加してきました。
モバイル系開発者さんが多く、話題もAndroidの中で動くことりんかわいい!ってな感じで業務屋さんのぼくは完全アウェーでしたが、業務屋さんだってことりんかわいいしたいのです。

というわけでJavaFX-JPA のスタンドアロンアプリを作ってみました。

青写真

超高機能なメニューエディタを作ります。
メニューと値段の一覧を見ることができ、さらに追加、削除までできてしまいます。

スクリーンショット 2014-07-05 16.32.10.png

まずはビルドの準備

ビルドはgradleで行います。
build.gradleにことりんの動く準備をします

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.8.11'
    }
}

apply plugin: "kotlin"

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib:0.8.11'
    compile 'org.apache.openjpa:openjpa-all:2.3.0'
    compile 'com.h2database:h2:1.4.179'
}

今回作るアプリはことりんがJPAを使ってH2からデータを読み書きします。
次にディレクトリたちを用意していきます。GradleでKotlinを動かすためには、ソースのルートディレクトリが「kotlin」である必要があるようなので、こんな感じの構成にしました。

スクリーンショット 2014-07-05 16.38.09.png

データベース関連のソースと設定をします。
今回はMenuエンティティが1つだけあるアプリなので、エンティティクラスとDAOクラスを作ります。

Menu.kt
package entity

import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.NamedQuery
import javax.persistence.Table

[Entity]
[Table(name = "MENUS")]
[NamedQuery(name = "Menu.findAll", query = "SELECT menu FROM Menu menu")]
public class Menu() {
    [Id]
    public var id : Int = 0
    public var name : String = ""
    public var price : Int = 0
}
MenuDAO.kt
package entity

import javax.persistence.EntityManager
import javax.persistence.Persistence

class MenuDAO() {
    val em :EntityManager? = Persistence.createEntityManagerFactory("kotlin")!!.createEntityManager()

    fun findAllMenu(): List<Menu> {
        return em?.createNamedQuery("Menu.findAll", Menu().javaClass)?.getResultList() as List<Menu>
    }

    fun create(menu: Menu): Unit {
        em?.getTransaction()?.begin()
        em?.persist(menu)
        em?.getTransaction()?.commit()
    }

    fun delete(menu: Menu?): Unit {
        em?.getTransaction()?.begin()
        em?.remove(menu)
        em?.getTransaction()?.commit()
    }
}

エンティティクラスを定義し、CRUDを担当するDAOクラスです。JPAを使ってDBとマッピングしています。
で、次にそのDB周りを作ります。

META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
             xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="kotlin" transaction-type="RESOURCE_LOCAL">
        <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
        <class>entity.Menu</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:h2:file:./db/test.db;INIT=RUNSCRIPT FROM 'db/create.sql'\;RUNSCRIPT FROM 'db/init.sql'"/>
            <property name="javax.persistence.jdbc.user" value=""/>
            <property name="javax.persistence.jdbc.password" value=""/>
        </properties>
    </persistence-unit>
</persistence>

先ほど作ったMenuクラスをエンティティとして使うよ宣言と、データソースに関する設定を行っています。今回の設定ではH2DBはプロジェクトのdbディレクトリ以下に作られます。さらにURLの指定で起動時にdb/create.sqldb/init.sqlを読み込むようにしています。

db/create.sql
DROP TABLE IF EXISTS MENUS;
CREATE TABLE MENUS (
    ID          INTEGER NOT NULL AUTO_INCREMENT
  , NAME VARCHAR2(20)
  , PRICE  NUMBER
  , PRIMARY KEY (ID)
);
db/init.sql
INSERT INTO MENUS (
  ID,
  NAME,
  PRICE)
  SELECT
    ID,
    NAME,
    PRICE
  FROM
    CSVREAD('./db/MENUS.csv', NULL, 'UTF-8');

init.sqlではdb/MENUS.csvを初期データとして読み込ませるようにしています。

db/MENUS.csv
ID,NAME,PRICE
1,牛丼,280
2,牛乳,100
3,ハラミ丼,400

テスト

ここまででCSVで作ったデータがH2にロードされてJPA経由でことりんが読み出してくれるところまで完成したので、動作確認をします。
以下のようなテストコードを作成して実行します。

test.kt
import entity.MenuDAO

fun main(args:Array<String>) {
    MenuDAO().findAllMenu().forEach {
        menu -> println("${menu.name} --- \\${menu.price}")
    }
}

ことりんのエントリポイントのシグニチャはfun main(args:Array<String>)です。で、先ほど作ったDAO経由から全Menuを取得して、それを順次出力します。
ことりんではスクリプト系言語のように変数を文字列中に展開してくれるのでJavaのString.format("%s", str)よりもお手軽に文字列が組み立てられます。
出力結果は以下の通りです。

STDOUT
(前略)
牛丼 --- \280
牛乳 --- \100
ハラミ丼 --- \400

Process finished with exit code 0

無事DBと疎通がとれました。

GUI

GUIにはJavaFXを使います。
JavaFXはSwingに替わる新しいGUIライブラリで、リッチクライアントを簡単に作れます。個人的には自由なレイアウトマネージャとコントローラーの強制分離が気に入っています。
まず、画面をデザインします。画面デザインはfxmlファイルというxmlファイルで行いますが、OracleがJavaFX Scene Builderなるものを配布しており、VB感覚でGUIを作り込んでいけます。
ここではMain.fxmlという名前で画面を作ります。

スクリーンショット 2014-07-05 17.02.57.png

Swingでレイアウトマネージャと戦ってた時代はなんだったんだと思うくらい楽ちんになりました。
出力されたfxmlは以下の通りです。

Main.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.Pane?>
<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="414.0"
      prefWidth="396.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="gui.MenuController">
    <children>
        <TextField fx:id="menuText" layoutX="14.0" layoutY="14.0" prefHeight="31.0" prefWidth="151.0"/>
        <TextField fx:id="priceText" layoutX="171.0" layoutY="14.0" prefHeight="31.0" prefWidth="151.0"/>
        <Button layoutX="328.0" layoutY="14.0" mnemonicParsing="false" onAction="#add" text="追加"/>
        <TableView fx:id="menuTable" layoutX="14.0" layoutY="57.0" prefHeight="308.0" prefWidth="364.0">
            <columns>
                <TableColumn fx:id="menuCol" prefWidth="251.0" text="メニュー"/>
                <TableColumn fx:id="priceCol" prefWidth="102.0" text="お値段"/>
            </columns>
        </TableView>
        <Button layoutX="328.0" layoutY="373.0" mnemonicParsing="false" onAction="#delete" text="削除"/>
    </children>
</Pane>

これに対するコントローラークラスを作ります。

MenuController.kt
package gui

import javafx.fxml.FXML
import javafx.scene.control.TextField
import javafx.scene.control.TableView
import javafx.scene.control.TableColumn
import javafx.scene.control.cell.PropertyValueFactory
import javafx.collections.FXCollections
import entity.Menu
import entity.MenuDAO

class MenuController() {
    val dao: MenuDAO = MenuDAO()
    [FXML] var menuText: TextField? = null
    [FXML] var priceText: TextField? = null
    [FXML] var menuTable: TableView<Menu>? = null
    [FXML] var menuCol: TableColumn<Menu, String>? = null
    [FXML] var priceCol: TableColumn<Menu, Int>? = null

    [FXML] fun initialize(): Unit {
        menuCol?.setProperty("name")
        priceCol?.setProperty("price")
        refresh()
    }

    fun refresh() {
        val menus = FXCollections.observableList(dao.findAllMenu())
        menuTable?.setItems(menus)
    }

    fun add(): Unit {
        val menu: Menu = Menu()
        menu.name = menuText?.getText() as String
        menu.price = priceText?.getText()?.toInt() as Int
        dao.create(menu)

        refresh()
    }

    fun delete(): Unit {
        val menu: Menu? = menuTable?.getSelectionModel()?.getSelectedItem()
        dao.delete(menu)

        refresh()
    }

    fun <T, S> TableColumn<S, T>.setProperty(property: String) {
        setCellValueFactory(PropertyValueFactory<S, T>(property))
    }
}

JavaFXのコントローラーはAspxのコードビハインドのように動作します。swingではごちゃごちゃにならざるを得なかったイベントハンドラを完全にびゅーから独立させて書く事ができます。
このコントローラーでは、初期表示時にメニューを表示し、追加ボタンでデータ追加、削除ボタンで削除をしています。それぞれ、先に作成したDAOを呼び出しているだけです。

最後にFXMLファイルから画面を起動させて動作確認をします。

Main.kt
package gui

import entity.MenuDAO
import javafx.application.Application
import javafx.stage.Stage
import javafx.fxml.FXMLLoader
import javafx.scene.Scene
import javafx.scene.Parent

fun main(args : Array<String>) {
    val dao : MenuDAO = MenuDAO()
    dao.findAllMenu()?.forEach { menu -> println(menu?.name) }
    Application.launch(MenuView().javaClass)
}

class MenuView() : Application() {
    override fun start(stage: Stage?) {
        val parent: Parent? = FXMLLoader.load(getClass().getResource("/Main.fxml"))
        stage?.setTitle("メニュー")
        stage?.setScene(Scene(parent))
        stage?.show()
    }
}

mainがfxmlを読み出し、FXMLローダーという人がコントローラーをインスタンス化してくれます。
これを実行すると青写真で紹介したような画面が起動するはずなので、思う存分データの追加、削除ができます。
ちなみに、この説明ではH2の設定で毎起動時にDBが初期かされてしまうので、動作確認をとるときはMETA-INF/persistence.xmlのurlを<property name="javax.persistence.jdbc.url" value="jdbc:h2:file:./db/test.db"/>と書き換えてください。

ことりん

今回はことりんからもちゃんとDBアクセスができてGUIが作れるという確認がメインで、とくにことりんであることのメリットをいかせていません。それどころかnull安全の副作用でそこらかしこに?がついてしまいました。これは既存のJava部分を呼び出すには仕方ないのでしょうか?まだまだことりんを触り始めたばかりなのでこの辺りはまだまだよくわかりません。
ただ自身でやってる業務で使えることはわかったので、バックオフィスで使う便利ツールなどをことりんで作ってみて、じわじわと征服していこうかと野望を抱いています。

さいごに

今回の成果物はhttps://github.com/pepepe/kotlinにあげてあるので、興味があればのぞいてみてください。

30
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
29