Tech Hotoke Blog

IT観音とは私のことです。

Vue×SpringでSPA作成14【Create・Update・Delete処理】

f:id:TechHotoke:20220218004406p:plain

まえがき

こちらの記事の続編です。

techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。 備忘録のため、詳細な説明を省略している部分があります。

前提

  • 基本的なJavaの知識やSpringの知識があること
  • 基本的なJavaScriptの知識やVueの知識があること

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1
  • Vue2.6
  • IDE:InteliJ
  • AWS(EC2,RDS,ELBなど)

やること

  • 一覧画面の作成
  • DBの情報を一覧表示する

完成画面

f:id:TechHotoke:20220227000447p:plain

画面遷移

f:id:TechHotoke:20220227004726p:plain

新規作成仕様

  • 画面右上ボタン押下時にポップアップする
  • ポップアップ画面は編集画面と項目が共通しているため共通化
  • ポップアップ時のタイトル表示のみ、editIndexの値に応じて変更する
  • 保存ボタン押下時にsaveメソッドが走る
  • 取り消しボタン押下時にポップアップを閉じる

編集仕様

  • テーブルActionsカラムのペンシルアイコン押下時にポップアップする
  • ポップアップ画面は編集画面と項目が共通しているため共通化
  • ポップアップ時のタイトル表示のみ、editIndexの値に応じて変更する
  • 保存ボタン押下時にupdateメソッドが走る
  • 更新はdelete/insertとする
  • 取り消しボタン押下時にポップアップを閉じる

削除仕様

  • テーブルActionsカラムのバケツアイコン押下時にポップアップする
  • 削除ボタン押下時にdeleteメソッドが走る
  • 削除は論理削除とする
  • 取り消しボタン押下時にポップアップを閉じる

実装~フロントエンド~

  • 今回はフロントエンドから作っていこうと思います。

  • まず、DankaList.vueに必要な処理やと要素など色々詰め込みます。

<template>
  <div>
      <template>
        <v-data-table
          :headers="headers"
          :items="dankaList"
          :page.sync="page"
          :items-per-page="itemsPerPage"
          sort-by="calories"
          class="elevation-1"
          @page-count="pageCount = $event"
          hide-default-footer
        >
          <template v-slot:top>
            <v-toolbar flat>
              <v-toolbar-title>檀家一覧</v-toolbar-title>
              <v-spacer></v-spacer>
              <v-dialog v-model="dialog" max-width="500px">
                <template v-slot:activator="{ on, attrs }">
                  <v-btn
                    color="primary"
                    dark
                    class="mb-2"
                    v-bind="attrs"
                    v-on="on"
                  >
                    新規作成
                  </v-btn>
                </template>
                <v-card>
                  <v-card-title>
                    <span class="text-h5">{{ formTitle }}</span>
                  </v-card-title>

                  <v-card-text>
                    <v-container>
                      <v-row>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkSeshuName"
                            label="施主名"
                          ></v-text-field>
                        </v-col>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkKoshuName"
                            label="戸主名"
                          ></v-text-field>
                        </v-col>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkKoshuName2"
                            label="戸主名2"
                          ></v-text-field>
                        </v-col>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkPhonenumber1"
                            label="電話番号"
                          ></v-text-field>
                        </v-col>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkEmail1"
                            label="メールアドレス"
                          ></v-text-field>
                        </v-col>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkAddress1"
                            label="住所"
                          ></v-text-field>
                        </v-col>
                        <v-col cols="12" sm="6" md="4">
                          <v-text-field
                            v-model="editedItem.dnkBikou1"
                            label="備考"
                          ></v-text-field>
                        </v-col>
                      </v-row>
                    </v-container>
                  </v-card-text>

                  <v-card-actions>
                    <v-spacer></v-spacer>
                    <v-btn color="blue darken-1" text @click="close">
                      取消
                    </v-btn>
                    <v-btn color="blue darken-1" text @click="save()">
                      保存
                    </v-btn>
                  </v-card-actions>
                </v-card>
              </v-dialog>
              <v-dialog v-model="dialogDelete" max-width="500px">
                <v-card>
                  <v-card-title class="text-h5"
                    >削除してもよろしいですか?</v-card-title
                  >
                  <v-card-actions>
                    <v-spacer></v-spacer>
                    <v-btn color="blue darken-1" text @click="closeDelete"
                      >取消</v-btn
                    >
                    <v-btn color="red darken-1" text @click="deleteItemConfirm"
                      >削除</v-btn
                    >
                    <v-spacer></v-spacer>
                  </v-card-actions>
                </v-card>
              </v-dialog>
            </v-toolbar>
          </template>
          <template v-slot:[`item.actions`]="{ item }">
            <v-icon  color="green" class="mr-6" @click="editItem(item)">
              mdi-pencil
            </v-icon>
            <v-icon color="red" @click="deleteItem(item)"> mdi-delete </v-icon>
          </template>
          <template v-slot:no-data>
            <v-btn color="primary" @click="getDanakList()"> Reset </v-btn>
          </template>
        </v-data-table>
        <div class="text-right pt-2">
          <v-pagination v-model="page" :length="pageCount"></v-pagination>
          <v-text-field
            :value="itemsPerPage"
            label="Items per page"
            type="number"
            min="-1"
            max="15"
            @input="itemsPerPage = parseInt($event, 10)"
          ></v-text-field>
        </div>
      </template>
  </div>
</template>

<script>
export default {
  name: "DankaList",
  data: () => ({
    dankaList: [],
    page: 1,
    pageCount: 0,
    itemsPerPage: 10,
    dialog: false,
    dialogDelete: false,
    headers: [
      {
        text: "施主名",
        align: "start",
        value: "dnkSeshuName",
      },
      { text: "戸主名", value: "dnkKoshuName" },
      { text: "戸主名2", value: "dnkKoshuName2" },
      { text: "住所", value: "dnkAddress1" },
      { text: "電話番号", value: "dnkPhonenumber1" },
      { text: "メールアドレス", value: "dnkEmail1" },
      { text: "備考", value: "dnkBikou1" },
      { text: "Actions", value: "actions", sortable: false },
    ],
    editedIndex: -1,
    editedItem: {
      dnkSeshuName: "",
      dnkKoshuName: "",
      dnkKoshuName2: "",
      dnkAddress1: "",
      dnkPhonenumber1: "",
      dnkEmail1: "",
      dnkBikou1: ""
    },
    defaultItem: {
      dnkSeshuName: "",
      dnkKoshuName: "",
      dnkKoshuName2: "",
      dnkAddress1: "",
      dnkPhonenumber1: "",
      dnkEmail1: "",
      dnkBikou1: ""
    },
  }),
  computed: {
    formTitle() {
      return this.editedIndex === -1 ? "新規登録" : "編集";
    },
  },
  watch: {
    dialog(val) {
      val || this.close();
    },
    dialogDelete(val) {
      val || this.closeDelete();
    },
  },
  created() {
    this.getDanakList();
  },
  methods: {
    getDanakList() {
      const _this = this;
      this.$axios.get("/danka").then(response => {
        console.log(response.data);
        _this.dankaList = response.data;
      });
    },
    editItem(item) {
      this.editedIndex = this.dankaList.indexOf(item);
      this.editedItem = Object.assign({}, item);
      this.dialog = true;
    },
    deleteItem(item) {
      this.editedIndex = this.dankaList.indexOf(item);
      this.editedItem = Object.assign({}, item);
      this.dialogDelete = true;
    },
    deleteItemConfirm() {
      this.dankaList.splice(this.editedIndex, 1);
      this.delete();
      this.closeDelete();
    },
    close() {
      this.dialog = false;
      this.$nextTick(() => {
        this.editedItem = Object.assign({}, this.defaultItem);
        this.editedIndex = -1;
      });
    },
    closeDelete() {
      this.dialogDelete = false;
      this.$nextTick(() => {
        this.editedItem = Object.assign({}, this.defaultItem);
        this.editedIndex = -1;
      });
    },
    delete(){
      const dankaId = this.editedIndex + 1;
      this.$axios.post(`/danka/delete/${dankaId}`).then(() => {
        console.log("削除に成功しました");
      })
    },
    save() {
      // 編集の場合はif句の処理、新規登録の場合はelse句の処理
      if (this.editedIndex > -1) {
        const dankaId = this.editedIndex + 1;
        
        Object.assign(this.dankaList[this.editedIndex], this.editedItem);
        
        this.$axios.post(`/danka/update/${dankaId}`, {
        seshuName: this.editedItem.seshuName,
        koshuName: this.editedItem.koshuName,
        koshuName2: this.editedItem.koshuName2,
        address: this.editedItem.address,
        phonenumber: this.editedItem.phonenumber,
        mailAddress: this.editedItem.mailAddress,
        remark: this.editedItem.remark
        }).then(() => {
          console.log("更新に成功しました");
        })
      } else {
        // const _this = this;
        this.$axios.post(`/danka/save`, {
          seshuName: this.editedItem.seshuName,
          koshuName: this.editedItem.koshuName,
          koshuName2: this.editedItem.koshuName2,
          address: this.editedItem.address,
          phonenumber: this.editedItem.phonenumber,
          mailAddress: this.editedItem.mailAddress,
          remark: this.editedItem.remark
        }).then(() => {
          console.log("保存に成功しました");
        })
      }
      this.close();
    },
  },
};
</script>

実装~バックエンド~

  • まず、画面からの入力値の箱に当たるFormクラスをdomain/form配下に作成します
    private Integer dnkId = 0;

    private String seshuName = "";

    private String koshuName = "";

    private String koshuName2 = "";

    private String address = "";

    private String phonenumber = "";

    private String mailAddress = "";

    private String remark = "";

// setter/getter省略
  • 次にControllerクラスに必要なメソッドを定義していきます
    @PostMapping("/danka/save")
    @ResponseBody
    public void saveDanka(@RequestBody DankaForm dankaSaveForm) {
        TblDankaEntity tblDanka;

        tblDanka = setDanakFormToTblDanka(dankaSaveForm);

        dankaService.saveTblDanka(tblDanka);
    }

    @PostMapping("/danka/update/{dankaId}")
    @ResponseBody
    public void updateDanka(@PathVariable("dankaId") String dankaId,
                            @RequestBody DankaForm dankaSaveForm) {
        TblDankaEntity tblDanka;

        tblDanka = setDanakFormToTblDanka(dankaSaveForm);
        //formの値をsetしてからIDをsetすること
        tblDanka.setDnkId(Integer.parseInt(dankaId));

        dankaService.updateTblDanka(tblDanka);
    }

   @PostMapping("/danka/delete/{dankaId}")
    @ResponseBody
    public void deleteUpdateDanka(@PathVariable("dankaId") String dankaId) {
        dankaService.deleteUpdateTblDanka(Integer.parseInt(dankaId));
    }

    /*
     * 檀家Formの入力値を檀家Entityにセットするメソッド
     * @param dankaSaveForm
     * @return tblDanka
     */
    private TblDankaEntity setDanakFormToTblDanka(DankaForm dankaForm) {
        TblDankaEntity tblDanka = new TblDankaEntity();
        String now = getNowDateTime();

        tblDanka.setDnkId(dankaForm.getDnkId());
        tblDanka.setCreatedAt(now);
        // TODO 登録者のユーザーIDを登録できるように変更すること
        tblDanka.setCreatedBy("rinsyou@gmail.com");
        tblDanka.setDnkSeshuName(dankaForm.getSeshuName());
        tblDanka.setDnkKoshuName(dankaForm.getKoshuName());
        tblDanka.setDnkKoshuName2(dankaForm.getKoshuName2());
        tblDanka.setDnkPhonenumber1(dankaForm.getPhonenumber());
        tblDanka.setDnkEmail1(dankaForm.getMailAddress());
        tblDanka.setDnkAddress1(dankaForm.getAddress());
        tblDanka.setDnkBikou1(dankaForm.getRemark());
        tblDanka.setIsDeleted("0");

        return  tblDanka;
    }

    /**
     * 現在日時をyyyy/MM/dd HH:mm:ss形式で取得する.<br>
     */
    public static String getNowDateTime(){
        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        final Date date = new Date(System.currentTimeMillis());
        return df.format(date);
    }

一旦ユーザー情報を固定値で入れて動作確認優先で実装を進めようと思います(エラーの要因を挟みたくない)

  • 次に、Controllerに定義したメソッドに対応するものをServiceクラスに実装していきます
public void saveTblDanka(TblDankaEntity tblDanka) {
        dankaMapper.save(tblDanka);
    }

    @Transactional
    /*
     * delete/insert
     */
    public void updateTblDanka(TblDankaEntity tblDanka) {
        Integer dankaId = tblDanka.getDnkId();
        //TODO 自作例外を作成して画面にはエラーメッセージを表示させる
        if(!isExistTargetById(dankaId)) {return;}

        dankaMapper.deleteByDankaId(dankaId);
        dankaMapper.insertDanka(tblDanka);
    }

    /**
     * Logical delete
     * @param dankaId
     */
    public void deleteUpdateTblDanka(Integer dankaId) {

        dankaMapper.deleteUpdateById(dankaId);

    }

    /**
     * 対象のデータが存在するかチェックするメソッド
     * @param dankaId
     * @return true/false
     */
    //TODO 汎用的なメソッドとして切り出す
    private boolean isExistTargetById(Integer dankaId) {
        boolean result = false;

        Optional<TblDankaEntity> target = dankaMapper.findOneDankaById(dankaId);

        if(target.isPresent()) {
            result = true;
        } else {
            return result;
        }
        return result;
    }
  • 続いてServiceクラスに定義したメソッドに対応するものをMapperクラスに定義します
void deleteByDankaId(Integer dankaId);

    Optional<TblDankaEntity> findOneDankaById(Integer dankaId);

    void saveDanka(TblDankaEntity tblDanka);

    void deleteUpdateById(Integer dankaId);
  • 次にMapperクラスに対応するSQLを記述していきます。また、論理削除を採用したため、全件取得メソッドにも条件を追加します。
<mapper namespace="jp.co.tms.domain.mapper.DankaMapper">
    <select id="findAllDanka" resultType="jp.co.tms.domain.entity.TblDankaEntity">
        SELECT *
        FROM tbl_danka
        WHERE is_deleted = 0;
    </select>
    <select id="findOneDankaById" resultType="jp.co.temple.domain.entity.TblDanka">
        SELECT *
        FROM tbl_danka
        WHERE dnk_id = #{dankaId}
    </select>

    <delete id="deleteByDankaId" timeout="20">
        DELETE
        FROM tbl_danka
        WHERE dnk_id = #{dankaId}
    </delete>

    <insert id="saveDanka">
        INSERT INTO tbl_danka
           (`dnk_id`,
            `dnk_seshu_name`,
            `dnk_koshu_name`,
            `dnk_koshu_name2`,
            `dnk_address1`,
            `dnk_bikou1`,
            `dnk_phonenumber1`,
            `dnk_email1`,
            `created_at`,
            `created_by`,
            `updated_by`,
            `is_deleted`,
            `updated_at`)
        VALUES
           (#{dnkId},
            #{dnkSeshuName},
            #{dnkKoshuName},
            #{dnkKoshuName2},
            #{dnkAddress1},
            #{dnkBikou1},
            #{dnkPhonenumber1},
            #{dnkEmail1},
            #{createdAt},
            #{createdBy},
            #{updatedBy},
            #{isDeleted},
            #{updatedAt})
    </insert>

   <update id="deleteUpdateById">
        UPDATE
            tbl_danka
        SET
            is_deleted = 1
        WHERE
            dnk_id = #{dankaId}
    </update>

これで実際に画面を動かしてみると、奇跡的に全て正常に動きました・・・笑

今回はここまでです! コードの羅列みたいになってしまっていますが、、、ご容赦を、、、 内容を逐一解説する元気がなかったため、気になる点がありましたらコメントなどいただけますと幸いです。

お付き合い頂きありがとうございました!