SpringBootでWebアプリ開発チャレンジ(3日目)

データベースとテーブルの準備

まずはMySQLにあらかじめデータベースとテーブルを用意しておく

  • ルートユーザー権限でMySQLにログイン
    • ターミナルの表示がmysql>となっていたらログイン成功
    • 設定によってはパスワード(-pオプション)が必要なことも。詳しくは「mysql コマンド」で検索を…
$ mysql -u root
  • 家計簿アプリ用のデータベースを作成
> create database kakeibo;

Query OK, 1 row affected (0.11 sec)

  • データベースができているかはshow databases;で確認できる

  • テーブルを作るためにkakeiboデータベースへ接続

    • Database changed と表示されたら接続OK
> use kakeibo;
  • income_outgoテーブルとcategoryテーブルを作成する
    • idは自動採番
    • date0000-00-00の形式で年月日が入る
    • 主キーはid
> create table income_outgo(
    -> `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    -> `cost` int(10) NOT NULL,
    -> `date` DATE NOT NULL,
    -> `type` varchar(64) NOT NULL,
    -> `memo` varchar(64) NOT NULL,
    -> PRIMARY KEY(`id`)
    -> );
Query OK, 0 rows affected (0.17 sec)
  • そういえばint(10)の(10)とかvarchar(64)の(64)って何を指すのだろう…?桁数じゃないよね…??

  • categoryテーブルを作成する

> create table category( 
    -> `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    -> `name` varchar(64) NOT NULL,
    -> `type` varchar(64) NOT NULL, PRIMARY KEY(`id`)
    -> );
Query OK, 0 rows affected (0.04 sec)
  • income_outgoテーブルにカテゴリID忘れてたので追加
> alter table income_outgo add category_id varchar(64) NOT NULL;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0
  • カラムが作成されたかを確認
    • show columns [table_name]を使う
> show columns from income_outgo;
+-------------+------------------+------+-----+---------+----------------+
| Field       | Type             | Null | Key | Default | Extra          |
+-------------+------------------+------+-----+---------+----------------+
| id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| cost        | int(10)          | NO   |     | NULL    |                |
| date        | date             | NO   |     | NULL    |                |
| type        | varchar(64)      | NO   |     | NULL    |                |
| memo        | varchar(64)      | NO   |     | NULL    |                |
| category_id | varchar(64)      | NO   |     | NULL    |                |
+-------------+------------------+------+-----+---------+----------------+

と、テーブルへのカラム挿入までしてしまったのですが…

実は必要ありませんでした…

この後SpringBoot側で実装するEntityクラスによって、カラムが自動生成されるからです。
なのでデータベースまで用意しておけばOK。

ユーザー作成

  • kakeiboテーブルアクセス専用のユーザーを作成する
    • ユーザー名はkakeibodev
    • [password]には任意のパスワードを設定する
> create user kakeibodev identified by '[password]'
  • kakeibodevにkakeiboテーブルに対する全ての権限を付与
> grant all privileges on kakeibo.* to kakeibodev;
  • ルートユーザーからログアウトし、kakeibodevでログイン
$ mysql -u kakeibodev -p
Enter password: //設定したパスワードを入力
  • kakeibodevユーザーでデータベース操作ができるか確認
    • これらのコマンドが全部成功したらひとまずOK
//kakeiboデータベースを使用
> use kakeibo;

// categoryテーブルにデータを挿入(idは自動採番)
> insert into `category`(
    -> `name`, `type`)
    -> values('食費', 'outgo');

//categoryテーブルの中身を表示(挿入したデータが表示されるはず)
> select * from category;

//categoryテーブル id=1のnameを更新
> update `category` set `name` = '日用品' where id = 1;

// categoryテーブル id=1のデータを削除
> delete from `category` where id = 1;

SpringBootのデータベース接続設定

  • src/main/resources/application.propertiesを作成し、設定を記述
    • usernamepasswordにkakeiboデータベースアクセス用ユーザーのユーザー名とパスワードを記述
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/kakeibo
spring.datasource.username=kakeibodev
spring.datasource.password=[password]
# 中略

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.springframework.boot:spring-boot-devtools")
    // JPA Data (We are going to use Repositories, Entities, Hibernate, etc...)
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    // Use MySQL Connector-J
    compile 'mysql:mysql-connector-java'
    testCompile("junit:junit")
}

Entityクラスの実装

いよいよクラスの実装だーー!
以前調査で読んでいたこちらのチュートリアルを参考に

incomeOutgo.java

  • inteliJには自動でGetterとSetterを作ってくれる
    • カラムを宣言した後に右クリック→「Generate」を選択
    • 出てきたポップアップから「Getter and Setter」を選択
    • 全てのカラムを選択して「OK」をクリック
  • このクラスで宣言されているメソッドは?
    • getXX()メソッド:XXカラムの値を取得
    • setXX()メソッド:引数の値をカラムに代入
  • この@で宣言されてるやつなんだっけか…
    • 「アノテーション」と言います
    • 日本語訳すると「注釈」
    • なるだけコードに各アノテーションの意味書いていきます
package income_outgo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;

@Entity //「このクラスからテーブルを作るよ!」を示す
public class IncomeOutgo {
    @Id //主キーはidで
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer id;
    private Integer cost;
    private Date date;
    private String memo;
    private String type;
    private Integer category_id;

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getCost() {
        return cost;
    }

    public void setCost(Integer cost) {
        this.cost = cost;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Integer getCategory_id() {
        return category_id;
    }

    public void setCategory_id(Integer category_id) {
        this.category_id = category_id;
    }

    public Integer getId(){

    }

}

Category.java

  • incomeOutgo.javaと同じように作成
package income_outgo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Category {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Integer id;
    private String name;
    private String type;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

Repositoryクラスの実装

IncomeOutgoRepository

  • なんとコードはこれだけ…
  • IncomeOutgoRepositoryが呼び出されるとCRUDが自動で実装されるんだって…!
    • importしているCrudRepositoryがよしなにやってくれるのか…
package income_outgo;

import org.springframework.data.jpa.repository.JpaRepository;


public interface IncomeOutgoRepository extends JpaRepository<IncomeOutgo, Integer> {

}

Serviceクラスの実装

  • Repositoryクラスで定義されたCRUDを呼び出すクラス
    • CrudRepositoryだとfindAll()メソッドでエラー→JpaRepositoryに置換
    • findOneメソッドとdeleteメソッドが赤字になった
      • Repositoryクラス側でメソッドを宣言
public interface IncomeOutgoRepository extends JpaRepository<IncomeOutgo, Integer> {
    IncomeOutgo findOne(Integer id);
    void delete(Integer id);
}

→しかしこの後アプリを実行してみると、deleteメソッドが機能していませんよというエラーが出てしまう

Error creating bean with name 'incomeOutgoRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract void income_outgo.IncomeOutgoRepository.delete(java.lang.Integer)! No property delete found for type IncomeOutgo!

登録(Create)と照会(Read)あたりが実装できてないと始まらんので、各クラスに宣言したdeleteメソッドは一旦コメントアウト…

ちなみにServiceクラスはこのようになった(現時点で使ってないメソッドはコメントアウト中)

package income_outgo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class IncomeOutgoService {
    @Autowired
    private IncomeOutgoRepository incomeOutgoRepository;

    public List<IncomeOutgo> findAll() {
        return incomeOutgoRepository.findAll();
    }

//    public IncomeOutgo findOne(Integer id){
//        return incomeOutgoRepository.findOne(id);
//    }
//
    public IncomeOutgo save(IncomeOutgo incomeOutgo){
        return incomeOutgoRepository.save(incomeOutgo);
    }
//
//    public void delete(Integer id){
//        incomeOutgoRepository.delete(id);
//    }
}

こんがらがってきたので整理

この辺りから、何から実装すればいいか頭がこんがらがってきた…
いっぺんに実装しようとすると、何をしているか見失いがち。
なので、一旦実装すべき機能を整理してみた。(カッコ内は関連するview部分)

  • Create
    • 収入/支出の登録(new)
    • カテゴリの登録(setting)
  • Read
    • 月別一覧(month)
    • 年間一覧(year)
    • 登録済みカテゴリの表示(setting)
  • Update
    • 収入/支出の編集(month→new)
    • カテゴリの編集(setting)
  • Delete
    • 収入/支出の削除(month)
    • カテゴリの削除(setting)

とにかく実装すべきはCRUD!そうCRUD!
というわけで、Create→Read→Update→Deleteの順に実装していくことに。

そして、Controllerクラスの実装に関しては何をどう書けばいいのか見当付かなかったため、
こちらの記事をもろに参考にさせていただきました…!!(土下座)

収入/支出の登録

new.html

  • formタグでアクションとPOSTメソッドを指定
  • inputタブのname属性で入力値をどのカラムに送信するか指定
    • typeは隠し入力欄(type=”hidden”)で値を送りたい
//ヘッダー・左サイドバーは省略
<!-- outgo -->
<div class="input-form" id="outgo" role="tabpanel" aria-labelledby="outgo-tab">
    <h4>【支出】</h4>
    <form th:action="@{/new}" th:method="post">
        <div class="form-group row">
            <label class="col-sm-2 col-form-label">日付</label>
            <div class="col-sm-6">
                <input type="date" class="form-control" name="date">
            </div>
        </div>
        <div class="form-group row">
            <label class="col-sm-2 col-form-label">支出</label>
            <div class="col-sm-6">
                <input type="number" class="form-control" name="cost">
            </div>
        </div>
        <div class="form-group row">
            <label class="col-sm-2 col-form-label">カテゴリ</label>
            <div class="col-sm-6">
                <select class="custom-select" name="category_id">
                    <option selected disabled>選択して下さい</option>
                    <option value="1">食費</option>
                    <option value="2">日用品</option>
                    <option value="3">交通費</option>
                </select>
            </div>
        </div>
        <div class="form-group row">
            <label class="col-sm-2 col-form-label">MEMO</label>
            <div class="col-sm-6">
                <input type="text" class="form-control" name="memo">
            </div>
        </div>
        <input type="hidden" name="type" value="outgo">
        <button class="btn btn-primary" type="submit">入力する</button>
    </form>
</div>

IncomeOutgoController.java

  • /new のパスに対しGET, POST, PUTメソッドを定義
//import部分は省略

@Controller
@RequestMapping("/")
public class IncomeOutgoController {
    @Autowired
    private IncomeOutgoService incomeOutgoService;

    @GetMapping("new")
    public String newIncomeOutgo(Model model){
        return "/new";
    }

    @GetMapping("month")
    public String month(Model model){
        List<IncomeOutgo> month = incomeOutgoService.findAll();
        model.addAttribute("/", month);
        return "/month";
    }

    @PostMapping("new")
    public String create(@ModelAttribute IncomeOutgo incomeOutgo){
        incomeOutgoService.save(incomeOutgo);
        return "redirect:/new";
    }

    @PutMapping("{id}")
    public String update(@PathVariable Integer id, @ModelAttribute IncomeOutgo incomeOutgo){
        incomeOutgo.setId(id);
        incomeOutgoService.save(incomeOutgo);
        return "redirect:/new";
    }
}
  • モデルクラスでのdateが受け付けずエラーに…
Field error in object 'incomeOutgo' on field 'date': rejected value [2018-12-06]; codes [typeMismatch.incomeOutgo.date,typeMismatch.date,typeMismatch.java.util.Date,typeMismatch]; arguments ...
  • Date型は 2018-12-06の形式を受け付けないようだ
    • →dateカラムをStringで宣言
      • IncomeOutgo.javaの private Date date;private String date; に書き換え
    • データ操作するときにDate型変換が必要なら変換する

これで収入/支出の登録はできた!

カテゴリの登録

実装方針としては収入/支出と同じ

CategoryController.java

  • /setting のパスに対しGET, POST, PUTメソッドを定義
//import部分は省略

@Controller
@RequestMapping("/setting")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    @GetMapping
    public String newcCategory(Model model){
        return "/setting";
    }

    @PostMapping
    public String create(@ModelAttribute Category category){
        categoryService.save(category);
        return "redirect:/setting";
    }

    @PutMapping("{id}")
    public String update(@PathVariable Integer id, @ModelAttribute Category category){
        category.setId(id);
        categoryService.save(category);
        return "redirect:/setting";
    }
}

setting.html

//ヘッダー・左サイドバーは省略
//支出(outgo)のhtmlのみ記載

<div class="col">
<div class="setting-header">
    <h2>設定</h2>
</div>

<!-- Category Setting (outgo)-->
<div class="outgo-setting">
    <p>カテゴリ(支出)</p>
    <form th:action="@{/setting}" th:method="post">
        <div class="form-group row">
            <div class="col-sm-4">
                <input type="text" class="form-control" name="name">
            </div>
            <div class="col-sm-2">
                <button class="btn btn-outline-secondary">追加</button>
            </div>
        </div>
        <input type="hidden" name="type" value="outgo">
    </form>

    <table class="table">
        <thead>
        <tr>
            <th scope="col">カテゴリ</th>
            <th scope="col">色</th>
            <th scope="col"></th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td></td>
            <td>橙</td>
            <td>
                <button class="btn btn-outline-secondary edit">編集</button>
                <button class="btn btn-outline-secondary delete">削除</button>
            </td>
        </tr>
        </tbody>
    </table>
</div>

これでカテゴリも登録だけはできた!
CRUDのC部分はなんとかクリアか…?(バリデーションないけど)

カテゴリの表示

  • 支出(type=”outgo”)のカテゴリ
  • 収入(type=”income”)のカテゴリ

/settingに別々に一覧表示したい

CategoryRepository.java

typeカラムでデータを取得するメソッドを宣言

//import部分は省略
public interface CategoryRepository extends JpaRepository<Category, Integer> {
    List<Category> findByType(String type);
}

CategoryService.java

  • CategoryRepositoryで定義したメソッドを呼び出し
//import部分は省略
@Service
public class CategoryService {
//略
    public List<Category> findByType(String type){
        return categoryRepository.findByType(type);
    }
//略
}

CategoryController.java

  • GetMapping部分でCategoryServiceで定義したメソッドを呼び出し
@GetMapping
    public String newcCategory(Model model){
        //支出
        List<Category> categories_outgo = categoryService.findByType("outgo");
        model.addAttribute("/setting", categories_outgo);
        //収入
        List<Category> categories_income = categoryService.findByType("income");
        model.addAttribute("/setting", categories_income);
        return "/setting";
    }

setting.html

  • テーブル部分で取得した値を表示させる
  • Thymeleafのドキュメントにも具体例がありました
<table class="table">
        <thead>
        <tr>
            <th scope="col">カテゴリ</th>
            <th scope="col">色</th>
            <th scope="col"></th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="category:${categories_outgo}" th:object="${category}">
            <td th:text="*{name}"></td>
            <td>橙</td>
            <td>
                <button class="btn btn-outline-secondary edit">編集</button>
                <button class="btn btn-outline-secondary delete">削除</button>
            </td>
        </tr>
        </tbody>
</table>

しかしビューに表示されず…

CRUDのR, U, Dについてはまた明日…

SNSでもご購読できます。

コメントを残す

*