はじめに
前回はサーバの構築をしてインフラ周りについての記事を書きましたが、今回はアプリケーション側であるバックエンドとフロントエンドについて開発したものと進捗をまとめていこうと思います!!
制作動機
- このあっつい中で冷房ガンガンにして寝て目が覚めると、、「いや、今日暑いんか?寒いんか?湿度が高いだけなんか??」ってことがあって数値で測って知りたいと思ったから
- 天気痛持ちだから家の中での気圧の変動記録しておきたいから
実現方法
- 不快指数なるものを参照して記録・通知しよう!
- どこでも見れるようにwebサーバへのリクエストで気圧の変動グラフを表示しよう!
不快指数ってなんだ?
不快指数は、「日中の蒸し暑さ」を表し、数字が大きいほど蒸し暑く不快であると言えます。「70未満・70~74・75~79・80~84・85以上」の5レベルで、80以上はほとんどの人が不快に感じる暑さです。
参考サイト:tenki.jp
日本人では、不快指数75で約9%の人が、77で約65%の人が不快に感じるようです。
不快指数 DI (Tは乾球気温℃、Hは湿度%)
DI=0.81T+0.01H×(0.99T−14.3)+46.3
参考サイト:casio
まとめると、気温と湿度から算出される値で78前後で大体イラッとする。
乾球気温とは・・・乾球温度計は気温を示し,湿球温度計は水でぬらしたガーゼの温度を示しています。
参考サイト:Benesse
実現するための設計・仕様をまとめる
Spring設計
- localhost:8080で表とグラフが表示される
- Mybatisから取得したデータをマッピングする
- 実行するSQL文はxmlファイルに記述してJavaインターフェースで宣言する
Ras Pi設計
- 気温の許容誤差は1℃とする
- 湿度、気圧に関してはセンサ以外で測るものがないため許容誤差は定義しない
- センサからデータベースへの操作以外は役割を持たない
データベース設計
- devデータベースを作成
- weatherテーブルを作りセンサからの情報をまとめる
- deleteは削除フラグで操作を行う
- deal_flgは今後の機能追加で利用予定
- dateはRas Pi本体のタイムゾーンを参照するためデータベースではタイムゾーンの設定はしない
- weatherテーブルの内容は以下の通りにする
Table "public.weather"
Column | Type | Collation | Nullable | Default
----------+-----------------------------+-----------+----------+-------------------------------------
id | integer | | not null | nextval('weather_id_seq'::regclass)
temp | integer | | |
humid | integer | | |
pressure | integer | | |
comfort | integer | | |
deal_flg | boolean | | |
del_flg | boolean | | |
date | timestamp without time zone | | |
Indexes:
"weather_pkey" PRIMARY KEY, btree (id)
今回行うこと
- Spring Boot実行環境をDocker上に構築
- Springアプリケーションへのリクエスト処理を設定(HTML形式で返す)
- Ras Piにセンサを搭載して、取得データをPostgreに保存
- 動作確認
実装環境
ソフトウェア側実装環境
- Java11
- Spring Boot 2.7.15
- PostgreSQL
- Docker
- Python3.10
ハードウェア側実装環境
- Raspberry Pi
- BME280(温湿度、気圧取得センサ)
- mac m1(今後AWSにデプロイ予定)
Spring Boot実行環境をDocker上に構築する
AWS EC2へのデプロイとローカル環境でJavaを複数バージョンで環境構築するとバージョン変更が大変になりそうなのでDockerを利用する
Docker上で構築するもの
- postgres
- Java11
- adminer(ブラウザ上からDBの管理を行うためのもの)
ディレクトリ構造
今回はComfortDemoディレクトリに構築していきます。
ComfortDemo % tree -L 1
.
├── docker-compose.yml
├── docker.sh
├── fordocker
└── server
docker.shでコマンドをまとめてますが、作成途中なので触れません。
1. 作成するコンテナの情報をdocker-compose.ymlにまとめる
docker-compose.yml
version: '3.8'
services:
app:
image: openjdk:11
container_name: app
ports:
- 8080:8080
tty: true
volumes:
- ./server:/srv:cached
working_dir: /srv
depends_on:
- db
adminer:
image: adminer:4.7.8
container_name: adminer
ports:
- "9000:8080"
depends_on:
- db
db:
image: postgres:13.1
container_name: db
environment:
POSTGRES_USER: "root"
POSTGRES_PASSWORD: "root"
POSTGRES_DB: "dev"
ports:
- "5432:5432"
volumes:
- dbvol:/var/lib/postgresql/data
- ./fordocker/db/initdb:/docker-entrypoint-initdb.d
volumes:
dbvol:
volumesでコンテナ停止時にデータが揮発しないように設定する&コンテナ作成時に実行するSQLファイルのパスをまとめる
2. コンテナ作成時に実行されるSQLをファイルにまとめる
ComfortDemo % ls
docker-compose.yml fordocker
docker.sh server
ComfortDemo % cd fordocker
fordocker % tree -L 3
.
└── db
└── initdb
├── 1_create_table.sql
├── 2_insert_testinfo.sql
└── 3_create_role_appuser.sql
現在、postgre上にrootでログインして作業を行なっているため、3_create_role_appuser.sql
以外について記載します。
1_create_table.sql
CREATE TABLE weather (
id SERIAL NOT NULL,
temp integer,
humid integer,
pressure integer,
comfort integer,
deal_flg boolean,
del_flg boolean,
date timestamp,
PRIMARY KEY (id)
);
2_insert_testinfo.sql
テストデータを追加
INSERT INTO weather (
temp,
humid,
pressure,
comfort,
deal_flg,
del_flg,
date
) values (
0,
0,
0,
0,
'false',
'True',
'2023-03-01'
);
3. serverディレクトリにspringの初期設定を行う
Spring Initializrでspringの初期設定zipファイルを作成する
設定した項目
- Project:Gradle-Groovy
- Language:Java
- Spring Boot:2.7.15
- Project Metadate
- Artifact:app
- Packaging:jar
- Java:11
- Dependencies
- spring devtool
- Lombok
- spring web
- Thymeleaf
- Mybatis Framework
- PostgreSQL Driver
Generateで生成したzipを展開してappディレクトリ配下を全てserverディレクトリに移動する
サーバサイド
Springアプリケーションへのリクエスト処理を設定(HTML形式で返す)
現時点ではCRUDのC(create),R(read)の部分を実装する
1. postgreへの接続設定を行う
ComfortDemo/server/src/main/resources/application.propertiesに接続内容を記入
spring.datasource.url=jdbc:postgresql://db:5432/dev
spring.datasource.username=root
spring.datasource.password=PASSWORD
spring.datasource.driver-class-name=org.postgresql.Driver
#xmlファイルがどこにあってもパスが通るようにする(規定通りに配置しますが、一応、、)
mybatis.mapper-locations:classpath*:xml/*.xml
##ホットリロード設定
spring.devtools.remote.restart.enabled=true
spring.devtools.livereload.enabled=true
ホットリロード設定を追記すると毎回ビルドせずともjavaプログラムの変更内容が反映されるらしいですが、、docker上だからなの上手くホットリロードができず、、、なぜなのか不明。
2. xmlファイルの作成とInterfaceクラスの作成
ここからは実行するSQL文をまとめたxmlファイルとxmlから実行するためのJavaインターフェースの作成をします。mybatis generatorから自動生成もできますが、今回は学習のための手書きでいきます。
ComfortDemo/server/src/main以下を操作します
% cd ComfortDemo/server/src/main
まずは、SQLの結果をマッピング(javaのオブジェクトとして一時保管)するためのjavaクラスを作成します。implementを使ってinterfaceとの結び付けも出来るようですが、、それは後々、、、
./java/com/example/app/entity/Weather.java
package com.example.app.entity;
import java.sql.Timestamp;
import lombok.Data;
@Data
public class Weather {
private int id;
private int temp;
private int humid;
private int pressure;
private int comfort;
private boolean deal_flg;
private boolean del_flg;
private Timestamp date;
}
- データベースで登録した型と値を揃えることが必要
- lombokを依存させることによる
@Data
で付与することで本来privateの変数を他のクラスの渡す、もしくは上書きする際には別のメソッドを作成する(getter,setterと呼ばれるもの)必要になるが、ビルド時に自動生成を行ってくれるため記述しない
./java/com/example/app/mapper/WeatherMapper.javaでインターフェースを作成
package com.example.app.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.app.entity.Weather;
@Mapper
public interface WeatherMapper {
public List<Weather> selectAll();
}
xmlファイルをresourceディレクトリ配下にインターフェースと同じパスで作成する。「application.properties」の「mybatis.mapper-locations:classpath*:xml/*.xml」の部分でどこにxmlファイルを置いてもパスが通るようになるが規定通りの方が可読性が高かったり、分かりやすいから)
揃えるもの
- インターフェースとxmlファイルのパス
- 両方のファイル名
- インターフェースのメソッド名とxmlファイル上のSQLのid
./resources/com/example/app/mapper/WeatherMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.app.mapper.WeatherMapper">
<select id="selectAll" resultType="com.example.app.entity.Weather">
select * from weather where del_flg = false
</select>
</mapper>
ここまでの1と2で、selectAll()が呼び出された時にxmlファイルに記述されたid=selectAllのSQL文の実行と実行された結果をweather classにマッピングすることができる。
3. リクエスト処理の設定をする
javaへの理解を深める(ラムダ式&streamAPIを使ってみる)ことを念頭に置きながらリクエスト処理についてjava classを作成していきます。
ComfortDemo/server/src/main/java/com/example/app/controller/ServiceController.java
package com.example.app.controller;
import java.util.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.app.entity.Weather;
import com.example.app.mapper.WeatherMapper;
@Controller
public class ServiceController {
@Autowired
WeatherMapper weatherMapper;
@RequestMapping(value="/")
public String index(Model model) {
// 横軸
//横軸に日付を入れるリストを作成
List<String> date = new ArrayList<>();
// 縦軸
//気温格納用リストを作成
List<Integer> temp = new ArrayList<>();
//湿度格納用リストを作成
List<Integer> humi = new ArrayList<>();
//気圧格納用リストを作成
List<Integer> press = new ArrayList<>();
//SQL実行結果をリストに格納
List<Weather> list = weatherMapper.selectAll();
//気温、湿度、気圧リストにstreamAPIで値を追加
list.stream()
.forEach(t -> {
date.add(t.getDate().toString());
temp.add(t.getTemp());
humi.add(t.getHumid());
press.add(t.getPressure());
});
//気温、湿度、気圧リストからstreamAPIで配列に変換
String[] label = date.stream()
.toArray(String[]::new);
int[] tempArr = temp.stream()
.mapToInt(Integer::intValue)
.toArray();
int[] humiArr = humi.stream()
.mapToInt(Integer::intValue)
.toArray();
int[] pressArr = press.stream()
.mapToInt(Integer::intValue)
.toArray();
model.addAttribute("label",label);
model.addAttribute("tempArr",tempArr);
model.addAttribute("humiArr",humiArr);
model.addAttribute("pressArr",pressArr);
model.addAttribute("info", list);
System.out.println(list);
return "index";
}
}
全体像:
- ブラウザからのアクセス
- SQL実行結果をlistに格納
- htmlファイルに値を受け渡す処理
- 戻り値でhtmlファイルを指定
Spring部分
-
@Controller
でリクエストの処理をするための依存性を持たせる -
@Autowired
でimportした他のクラスを内部で自動的に結び付ける -
@RequestMapping(value="/")
でブラウザからのアクセスパラメータの設定- 戻り値でComfortDemo/server/src/main/resources/templates配下のファイルを指定することでブラウザに指定されたファイルを表示する
-
model.addAttribute("label",label)
でhtmlファイルのEL式に値を受け渡す
(EL式については後で、、)
Javaコーディング部分
htmlファイルのEL式に値を受け渡してグラフを表示するための必要なデータをまとめる処理について記述していきます
streamAPIの基本の使い方について
参考サイト:Java8 Streamざっくりまとめ
- Collectionやlistのstreamを作成する(streamはイテレーションの拡張API)
- Javaでのイテレーション:要素の集まりに対する繰り返し処理のこと
- 上記の操作をオブジェクトにしたものがイテレータ
- 中間操作を行う(streamの内部を都合よく変換する)
- 特性変更操作、ステートレス操作、ステートフル操作の3種類があるらしいが後々やって行こう、、、
- 終端操作を行う(streamの内容に対して処理を行い、処理後はそのstreamは利用不可になる)
- 今回は終端操作として
forEach()
とtoArray()
を利用する
- 今回は終端操作として
ラムダ式について(ラムダ式については細く説明は記載しません)
参考サイト:知っといてムダにならない、Java SE 8の肝となるラムダ式の基本文法
forEach()
の引数部分について説明していきます
-
(t -> ~~)
の部分-
t
がlistのstreamを指定 -
~~
が処理をまとめる
-
weatherクラスの性質を持ったList型listでstreamAPIを利用する
list.stream()
.forEach(t -> {
date.add(t.getDate().toString());
temp.add(t.getTemp());
humi.add(t.getHumid());
press.add(t.getPressure());
});
処理内容について
- 上記のコードでは終端操作のみ
- listはweatherクラスの性質を持つためgetterメソッドが利用できる
- getterメソッドはlombok.Dataにより自動生成
- 全てのJavaクラスはlang.Objectを継承しているためObjectクラスが持つ
toString()
も特記なしで今回は使うことができる
- weatherのdata,temp,humidはString,Integer,Integerの各参照型になりそれぞれのList型に追加される
ここで、もう一度ラムダ式とstreamAPIに戻ります
int[] tempArr = temp.stream()
.mapToInt(Integer::intValue)
.toArray();
上の部分でIntergerの要素を持つList型tempに要素が格納されたのでそのリストにstreamを利用する
- tempのstreamを作成
- 中間操作
mapToInt()
- 引数について
-
(Integer::intValue)
はラムダ式の一つで標準的に書くとInteger.intvalue()
でInteger型の値をint型で返す
- 終端操作
toArray()
- streamで処理を行った各要素を配列に変換し、int型配列tempArrに格納
フロントエンド
HTMLファイルの作成をする
コピペで使えるChart.js v3.7.0 スクリプト【二軸グラフ】
1. Thymeleafを利用してEL式を使えるようにする
ComfortDemo/server/src/main/resources/templates/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>WeatherList</title>
</head>
<body>
<h1>WeatherList</h1>
<form method="post" th:each="Weather : ${info}">
<table border="1">
<tr>
<th>気温[℃]</th>
<th>湿度[%]</th>
<th>気圧[hPa]</th>
<th>不快指数</th>
<th>日付</th>
</tr>
<tr th:if="${!#lists.isEmpty(info)}">
<th><p th:text="${Weather.temp}"/></th>
<th><p th:text="${Weather.humid}"/></th>
<th><p th:text="${Weather.pressure}"/></th>
<th><p th:text="${Weather.comfort}"/></th>
<th><p th:text="${Weather.date}"/></th>
</tr>
</table>
</form>
</body>
</html>
Thymeleafが使えるように<html>
タグを<html xmlns:th="http://www.thymeleaf.org">
とすることでThymeleafが導入されEL式が使えるようになる
EL式とは、文書中に一つの式として表された短いプログラムコードを記述し、処理を実行したり評価結果を埋め込んで表示する技術
参考サイト:IT用語辞典
ComfortDemo/server/src/main/java/com/example/app/controller/ServiceController.java
model.addAttribute("info", list);
上記のコードでhtml内部でinfo
を指定、利用できるようになった
-
th:each="Weather : ${info}"
で受け取ったinfo
をWeather
で使える -
th:text="${Weather.temp}"
でJavaでのWeatherクラスのtempを表示させることができる
2. Chart.jsを使ってグラフを作成する
先ほどのコードに書き加えていきます
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
<title>WeatherList</title>
</head>
<body>
<h1>WeatherList</h1>
<form method="post" th:action="@{/update}" th:each="Weather : ${info}">
<table border="1">
<tr>
<th>気温[℃]</th>
<th>湿度[%]</th>
<th>気圧[hPa]</th>
<th>不快指数</th>
<th>日付</th>
</tr>
<tr th:if="${!#lists.isEmpty(info)}">
<th><p th:text="${Weather.temp}"/></th>
<th><p th:text="${Weather.humid}"/></th>
<th><p th:text="${Weather.pressure}"/></th>
<th><p th:text="${Weather.comfort}"/></th>
<th><p th:text="${Weather.date}"/></th>
</tr>
</table>
</form>
<h2>気温、湿度、気圧のグラフ</h2>
<canvas id="WeatherInfo"></canvas>
<script type="text/javascript" th:inline="javascript">
/*<![CDATA[*/
var ctx = document.getElementById("WeatherInfo").getContext('2d');
var WeatherInfo = new Chart(ctx, {
type: 'line',
data: {
// コントローラーで格納したlabelを変数式で参照
labels: /*[[ ${label} ]]*/,
datasets: [
{
label: "気温[℃]",
borderColor: 'rgb(255, 0, 0)',
lineTension: 0,
fill: false,
data: /*[[ ${tempArr} ]]*/,
},
{
label: "湿度[%]",
borderColor: 'rgb(0, 255, 0)',
lineTension: 0,
fill: false,
data: /*[[ ${humiArr} ]]*/,
},
{
label: "気圧[hPa]",
borderColor: 'rgb(0, 0, 255)',
lineTension: 0,
fill: false,
data: /*[[ ${pressArr} ]]*/,
},
]
},
options: {
responsive: true,
}
});
/*]]>*/
</script>
</body>
</html>
<meta>
タグの下に<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
でChart.jsを埋め込むことで使用可能になる
その後は
- グラフを描画する
- フレームの設定
- グラフ設定
- グラフの種類の設定(今回は折れ線を指定)
- 色彩設定
- EL式での値の受け渡し
- 受け渡しを行う際には
/*[[]]*/
で変数をくくる
- 受け渡しを行う際には
Dockerからのビルドと起動
docker-compose 下で Java + Spring Boot + PostgreSQL (Spring Data JPA編)を参考に行う
一部抜粋
docker-compose up -d
# DB が立ち上がって初期化されるまでちょっとかかるのでちょっと待つ
docker-compose exec app bash
bash-4.4# sh gradlew build
...
BUILD SUCCESSFUL in 8m 41s
5 actionable tasks: 5 executed
# できてるのを確認
bash-4.4# ls build/libs/
app-0.0.1-SNAPSHOT.jar
bash-4.4# java -jar build/libs/app-0.0.1-SNAPSHOT.jar
このまんま操作を行った後にlocalhost:8080にアクセス
無事に表示された
さらにlocalhost:9000にアクセスしてログインでデータ確認
dockerで立てたSpringとAdminerへの接続確認ができた!!
今回はここまで!!
次回はRas Piの方をやっていこう!!!
第2弾(RasPi編)はこちら
第3弾(AWS編)はこちら
問題解決編(デーモン化)はこちら
おまけや今後の展望
- Postgreを選んだのは、MySQLだとMybatisからDate型を正しく引っ張ることが出来なかったため。(Mybatis GeneratorでもMySQLはサポート外らしく、手打ちで試したが出来なかったためPostgreを利用した)
- mybatisの設定が結構難しかった(xmlファイル初見&仕組みの理解に手間がかかった)
- dockerでの環境構築にインターンなどの経験が活かせたのが良かった(ymlファイルの作成やリクエスト処理部分など)
- Springのホットリロード設定が出来なかったのはなぜなのだろうか、、Docker上だと環境構築を変えないといけないのか?ホットリロードできるようにしたい
- 今回は一つのテーブルで管理しているが、複数のテーブルで管理する場合どうなるのかやってみたい
- streamAPIの中間操作についての理解を深めたい(今回はstreamAPIの概要を理解して実装しただけなのでより深くまで理解したい)
- 閾値設定して超えたらslackとかで通知とかやってみたい