背景
- 1週間前にKotlinを始めた(詳細は[Kotlin × SpringBootでHelloWorldしてみる]
(https://qiita.com/yut_arrows/items/203fb544ff52d89d7e3f)をみてください) - なにか動くものを作りたいと思ったので、簡単なものをつくる
用意したもの
- IntelliJ Idea
- Java1.8
- kotlin1.2.71
- SpringBoot2.0.4
やってみる
- つくるものは簡単なLGTMサイトみたいなもの
- 実装は取得・登録・削除だけを一旦はスコープとする
####プロジェクトの立ち上げ方は前回と同じなので割愛
###DBを用意する
table.sql
CREATE TABLE lgtm(
image_url VARCHAR(256) NOT NULL,
image_name VARCHAR(10),
image_lgtm_url VARCHAR(256),
update_datetime DATE NOT NULL,
PRIMARY KEY(image_url)
);
- 画像は適当に用意しました
###gradleをいじる
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-reflect"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.webjars:bootstrap:4.1.3'
implementation 'org.projectlombok:lombok:1.16.10'
runtime 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- 追加したのは
- JPAとmysql-connector-java(DBアクセスしてくれるやつ)
- Thymeleafとbootstrap(フロントで使います)
###参照系の実装
####実装イメージ
- DBからまず取得してくる
- 取得したレスポンスを自前のレスポンスクラスに詰め替えて返却する
- 新しいやつ(一旦、登録してから7日以内のものとする)からフロントに表示する
- リロードした際に画像がシャッフルされるようにする
####Entityの実装
- まずは、全件取得したかったので、DBのテーブルと同じようになるようEntityクラスをさくせいしました
LgtmEntity.kt
import java.util.*
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "lgtm")
class LgtmEntity {
@Id
@Column(name = "image_url")
var imageUrl = String()
@Column(name = "image_name")
var imageName = String()
@Column(name = "image_lgtm_url")
var imageLgtmUrl = String()
@Column(name = "update_datetime")
var updateDatetime = Date()
}
- Entityクラスでは主キーに@Idをつけるのとクラスにアノテーションを付与してください
- 各変数に@Columnをつけているのですが、DBと一致していればアノテーションを付与しなくてもちゃんと動きます
####Repositoryの実装
- 次にRepositoryを作る
LgtmRepository.kt
import com.example.sample_kotlin.controller.resources.LgtmEntity
import org.springframework.data.jpa.repository.JpaRepository
interface LgtmRepository : JpaRepository<LgtmEntity, String>
- Jpaを継承したインターフェースの実装です
- マッピングしたいEntityを引数に渡してあげるだけであとはよしなにやってくれます
- この段階でコントローラとかで直接Repository呼んであげれば、取得はできるのですが、それはDBに依存しすぎてしまうのでレスポンスクラスを用意します
####フロントに表示させる用のDTOクラスの実装
- ここではテーブルに持ってる内容に加えて1つ変数を加えます
- そして、このクラスの中で詰め替えるメソッドも定義してしまおうと思います
LgtmTopResponse.kt
import java.util.*
/**
* フロントに返す用のDTOクラス
*/
class LgtmTopResponse {
var imageUrl = String()
var imageName = String()
var imageLgtmUrl = String()
var updateDatetime = Date()
var isNew = false
fun buildWithEntity(lgtmEntity: LgtmEntity,baseDate :Date) : LgtmTopResponse{
return LgtmTopResponse().apply {
imageUrl = lgtmEntity.imageUrl
imageName = lgtmEntity.imageName
imageLgtmUrl = lgtmEntity.imageLgtmUrl
updateDatetime = lgtmEntity.updateDatetime
isNew = lgtmEntity.updateDatetime.after(baseDate)
}
}
}
- Entityと日付を引数に受け取って、DBに持ってないものはこの中で計算してResponseに詰めて返すという単純な処理をしてるメソッドを定義しています
- あとは、コントローラでこいつを呼んであげればResponseを返せるのですが、コントローラからRepositoryを呼ぶのが個人的に好きではないので、そのためだけのサービスを作ろうと思います
####リポジトリを呼ぶサービスの実装
LgtmRepositoryAccessor.kt
@Service
class LgtmRepositoryAccessor(private val lgtmRepository: LgtmRepository ) {
/**
* 取得用のメソッド
*/
fun getImages() :List<LgtmEntity>{ return lgtmRepository.findAll()}
}
- これだけ見るとToo Muchな感じは多少否めないのですが、後々コントローラが増えたときにRepositoryが色々なところから呼ばれなくて済むというのとデータアクセス部分を隠蔽できるという観点からメリットはあるのかなーと思います(但、完全に好みですw)
- 最後にコントローラの実装を完成させてしまいましょう
- 流れとしては、全件取得→Responseに詰め替える→新しいやつに色をつける→シャッフルするってな感じで実装してら良いのではないでしょうか??
LgtmController.kt
import com.example.sample_kotlin.controller.resources.LgtmEntity
import com.example.sample_kotlin.controller.resources.LgtmTopResponse
import com.example.sample_kotlin.service.LgtmRepositoryAccessor
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import java.util.*
@Controller
@RequestMapping("/")
class LgtmTopController(private val accessor: LgtmRepositoryAccessor) {
/**
* 取得用メソッド
*/
@GetMapping
fun getLgtmImages(model: Model) : String {
// 現在時刻の取得をしておく
val now = Calendar.getInstance()
now.add(Calendar.DAY_OF_MONTH, -7)
val baseDate = now.time
// 一旦Repositoryから全部取得しておく
val responseList = accessor.getImages()
// 上の処理に対してストリームしていく
var builtList : MutableList<LgtmTopResponse> = mutableListOf()
responseList.forEach{
it -> builtList.add(LgtmTopResponse().buildWithEntity(it, baseDate))
}
// 整形済みのリストをNewフラグの是非でバラす
var topResponseList : MutableList<LgtmTopResponse> = mutableListOf()
var nonNewList : MutableList<LgtmTopResponse> = mutableListOf()
builtList.forEach{ it -> if (it.isNew) {
topResponseList.add(it)
} else {
nonNewList.add(it)
}
}
// それぞれシャッフルして合体させる
topResponseList.shuffle()
topResponseList.addAll(nonNewList.shuffled())
model.addAttribute("imageResources", topResponseList)
return "top"
}
}
- こんな感じで実装してみました
- CURLでレスポンスをみたい場合は@RestControllerをつけておけば、ここでは試せるのですが、後々画面を作ってブラウザで見ようとするとERRORになります。なので、今回は@Controllerをつけておきましょう(先に叩いて試したければ、@RestControllerつけて叩いてみてください)
- あとは、引数で受け取っているModelというやつにフロントで使いたいデータを渡してあげることで、あとはHTML側でよしなに表示させることができます
####一旦取得系はこれで動きます
###削除系の実装
- 削除は簡単です
- Entityが定義されているので、Entityに主キーの値を詰めてRepositoryに渡してあげれば削除できます
- では、さっそく
####コントローラに追加する
LgtmTopController.kt
import com.example.sample_kotlin.controller.resources.LgtmEntity
import com.example.sample_kotlin.controller.resources.LgtmTopResponse
import com.example.sample_kotlin.service.LgtmRepositoryAccessor
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import java.util.*
@Controller
@RequestMapping("/")
class LgtmTopController(private val accessor: LgtmRepositoryAccessor) {
/**
* 削除用メソッド
*/
@GetMapping("/delete")
fun deleteLgtmImage(model : Model, @RequestParam(name = "q") deleteImageUrl : String) : String{
// 受け取ったUrlをEntityに詰める
var lgtmEntity = LgtmEntity()
lgtmEntity.apply { imageUrl = deleteImageUrl }
// 実行する
try {
accessor.deleteImage(lgtmEntity)
} catch (ex : Exception) {
model.addAttribute("error", ex.message)
}
return "redirect:/"
}
}
- まず、リクエストパラメータとしてURLを受け取りました
- 受け取ったURLをEntityに詰めて、渡すだけですみます
- あとは、return "redirect:/"でホームにリダイレクトするように制御しておきましょう
####LgtmRepositoryAccessorに追加する
- このクラスを実装してない人は読み飛ばしてください
LgtmRepositoryAccessor.kt
import com.example.sample_kotlin.controller.resources.LgtmEntity
import com.example.sample_kotlin.infrastructure.LgtmRepository
import org.springframework.stereotype.Service
@Service
class LgtmRepositoryAccessor(private val lgtmRepository: LgtmRepository ) {
/**
* 削除用のメソッド
*/
fun deleteImage(lgtmEntity: LgtmEntity) { lgtmRepository.delete(lgtmEntity) }
}
- リポジトリにdeleteメソッドが用意されているので呼んであげるだけで完了です
- これで取得と削除は一旦完了したので、フロントを追加しておきます
###フロントの実装
top.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>LGTM.in</title>
<link rel="stylesheet" media="all" th:href="@{/css/style.css}"/>
<link rel="stylesheet" media="all" th:href="@{/webjars/bootstrap/4.1.3/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{js/copyToClipboard.js}"></script>
<script type="text/javascript" th:src="@{js/deleteImage.js}"></script>
</head>
<body>
<div class="h1">
<div class="text-left" style="margin-left: 80px">
<a href="/">
<span style="color: black" class="text-decoration:none">LGTM.in</span>
</a>
</div>
</div>
<div class="h3">
<div class="text-right" style="margin-right:80px">
<a href="/register">
<button class="btn btn-outline-secondary">upload</button>
</a>
</div>
</div>
<br/>
<div style="margin-left: 60px;margin-right:60px">
<div class="text-center">
<div style="float: left; margin: 5px" th:each="image, index:${imageResources}">
<p><img th:src="${image.imageUrl}" style="object-fit:contain; height:200px; width:200px;background-color:#F5F5F5"></p>
<input class="copyTarget" type="hidden" th:value="${image.imageLgtmUrl}" />
<button class="btn btn-outline-secondary" th:data-index="${index.index}" onclick="copyToClipboard(event)">コピー</button>
<a th:href="@{/delete(q=${image.imageUrl})}">
<button class="btn btn-outline-secondary" onclick="return deleteImage();">削除</button></a>
</div>
</div>
</div>
<br/>
</div>
</body>
</html>
- ただ受け取ったDTOのリストをThymeleafのfor文でバラして要素ごとに表示させただけです
- 今回フロントの部分はスコープ外なので、HTML詳しくなりたい方はGoogle先生に聞いてください
####削除とコピーのエフェクトを追加
- ここらへんは、jsの話なので触れないですが、一応ソースだけ置いておきます
copyToClipboard.js
function copyToClipboard(e) {
// コピー対象をJavaScript上で変数として定義する
var index = e.target.getAttribute("data-index");
var copyTarget = document.querySelectorAll(".copyTarget")[index];
// 仮想のdiv要素を作りそこに引数で受けた文字列を持たせる
var tempElement = document.createElement('div');
tempElement.appendChild(document.createElement('pre')).textContent = copyTarget.value;
// 仮想のdiv要素をviewに表示させないようにする
tempElement.style.position = 'fixed';
tempElement.style.left = '-100%';
document.body.appendChild(tempElement);
document.getSelection().selectAllChildren(tempElement);
document.execCommand('copy');
document.body.removeChild(tempElement);
// コピーをお知らせする
alert("コピーできました! : " + copyTarget.value);
}
deleteImage.js
function deleteImage() {
if (window.confirm('削除しますか?')) {
return true;
}
return false;
}
###登録の実装
- 脇道にそれましたが、サーバーサイドの話に戻ります
- では次に、登録画面を作っちゃいます
####実装イメージ
- イメージURLと画像の名前を登録画面で受け取る
- 受け取ったイメージURLを元にLGTM用のURLに変換してDBに登録する
- では、早速実装してみる
####登録用のDTOを作成
- 画像の名前とURLだけ定義しておきます
RegisterForm.kt
class RegisterForm {
var imageName = String()
var imageUrl = String()
}
####LgtmRepositoryAccessorに追加
- repositoryにsaveメソッドがあるので呼ぶ
LgtmRepositoryAccessor.kt
import com.example.sample_kotlin.controller.resources.LgtmEntity
import com.example.sample_kotlin.infrastructure.LgtmRepository
import org.springframework.stereotype.Service
@Service
class LgtmRepositoryAccessor(private val lgtmRepository: LgtmRepository ) {
/**
* 登録用のメソッド
*/
fun postImage(lgtmEntity: LgtmEntity) { lgtmRepository.save(lgtmEntity)}
####コントローラの実装
- あとはコントローラを実装するだけです
- 必要なことは2つあって、1つは登録ページに飛んだ時に空の登録フォームが表示されること、もう1つがPOSTの処理が実行できることです
LgtmRegisterController.kt
import com.example.sample_kotlin.controller.resources.LgtmEntity
import com.example.sample_kotlin.controller.resources.RegisterForm
import
com.example.sample_kotlin.service.LgtmRepositoryAccessor
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import java.util.*
@Controller
@RequestMapping("/register")
class LgtmRegisterController (var accessor: LgtmRepositoryAccessor){
/**
* 登録フォーム取得用
*/
@GetMapping
fun getRegisterForm(registerForm: RegisterForm, model: Model): String {
model.addAttribute(registerForm)
return "register"
}
/**
* 登録処理
*/
@PostMapping(name = "/register")
fun post(registerForm: RegisterForm,model: Model) : String{
// 登録用のインスタンスの作成
var lgtmEntity = LgtmEntity()
lgtmEntity.apply {
imageUrl = registerForm.imageUrl
imageName = registerForm.imageName
imageLgtmUrl = "[![LGTM](${registerForm.imageUrl})](${registerForm.imageUrl})"
updateDatetime = Calendar.getInstance().time
}
// 登録処理
try {
accessor.postImage(lgtmEntity)
} catch (ex : Exception) {
model.addAttribute("error", ex.message)
}
model.addAttribute(registerForm)
return "redirect:register"
}
}
- 最初にアクセスした時に空の登録フォームが表示されるようにしているのが1個目のメソッドです
- 登録の実質的な処理を担っているのが2個目のメソッドです
- コントローラの中で、Entityに詰め替えをしちゃいましたが、Factoryクラスとか定義してそこでやっても良いかもです
####フロントの実装
register.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Upload️</title>
<link rel="stylesheet" media="all" th:href="@{/css/style.css}"/>
<link rel="stylesheet" media="all" th:href="@{/webjars/bootstrap/4.1.3/css/bootstrap.min.css}"/>
</head>
<body style="background: white">
<div style="margin-left: 80px">
<div class="h1">
<div class="text-left">
<a href="/">
<span style="color: black" class="text-decoration:none">Upload️</span>
</a></div>
</div>
<div>
<div class="text-left">
<form role="form" action="/register" th:action="@{/register}" th:object="${registerForm}" method="post" autocomplete="off">
<div>Image Url</div>
<input type="text" id="inputPng" style="width:50%;" placeholder="https://abcdefghijk123.png" th:field="*{imageUrl}"/>
<br/>
<div>Image Name</div>
<input type="text" id="inputName" style="width:50%;" placeholder="ドナルドトランプLGTM" th:field="*{imageName}"/>
<br/>
<button class="btn btn-outline-secondary" name="register" type="submit">submit</button>
</form>
</div>
</div>
</div>
</body>
</html>
###最終形態
- こんな感じになりました
###まとめ
- 今回は簡単なアプリケーションだったので半日もかからずして実装できましたが、なんとなーくkotlinの基本的な書き方は理解できたのかなーと思います
- ただ、まだまだKotlinの旨味を味わえてないので要努力だとは思っています
- あと、このアプリケーションも色々な機能を追加したり、リファクタリングしてそれも記事のネタにできたらなーと思っています
###おまけ
- 一応ソース晒しておきます
- https://github.com/yuaikawa/sample-kotlin