Tech Hotoke Blog

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

さくらVPSにLinuxをインストールする

目的

学習用のLinux環境をさくらVPS上に構築すること

前提

  • さくらVPSの会員登録が完了していること
  • 基本的なネットワークの知識があること
  • 基本的なLinuxの知識があること

やること

  • サーバーのレンタル
  • レンタルしたサーバーにDebianをインストール
  • ローカル環境からSSH接続する
  • SSH接続以外は行えないようにする

VPSとは?

f:id:TechHotoke:20220305231402p:plain

  • 1台の物理的なサーバーの中に、仮想的に複数のサーバーを構築します。
  • 共用サーバーと異なり、ホストOSの土台の上にユーザーごとにゲストOSが用意されており、基本的にこのゲストOS同士は干渉することはありません。そのため、共用サーバーのように、他のユーザーの利用の影響によって障害などが発生したり動作が重くなってしまったりということは基本的にはありません。
  • メモリやCPUについても、共用サーバーと違ってそのゲストOS用の性能が確保されるため、共用サーバーより負荷のかかる利用にも適しています。* 1つのOSを共有する共用サーバーと異なりゲストOS上では自由にソフトウェアなどの運用ができるのも特徴です。
  • 専用サーバーと比べて簡単・安価に導入が可能です。

参考: https://www.kagoya.jp/howto/it-glossary/server/basic-02/

レンタル〜OSインストール

  • 下記のような内容でレンタルします。1年間契約の場合は800円くらい安くなるようなので、年間契約にしておきます。(サーバー名はAWSで構築しているEC2のサーバー名にちなんで命名。このサーバーの上にEC2で動いているアプリケーションの管理画面を乗っけたいので…)

f:id:TechHotoke:20220305010034p:plain

  • OSの再インストールを選択

  • カスタムOSを選択

  • Debian11 64amd を選択

  • VNCコンソールを開きます

    • rootユーザーのパスワードを入力します

    • 一般ユーザーを作成し、パスワードを設定します

    • partition disk でuse entire diskを選択します

    • インストールが完了したら、rootユーザーと一般ユーザーでログインできることを確認します

パーティショニングとは?

パーティショニングはインストール時に必ず必要な作業で、ハードドライブ上の利用できる領域をハードドライブ上に保存されるであろうデータとコンピュータで予定されている用途に従って分割する作業です (分割されたそれぞれの区画を「パーティション」と呼びます)。パーティショニングの段階で、使用するファイルシステムを選択します。これらすべてが、性能、データのセキュリティ、サーバの管理、に影響をおよぼします。

use entire disk(すべてのファイルを 1 つのパーティションに)は、Linux システムツリー全体 (ルート / ディレクトリ以下すべて) を単一のファイルシステムに保存します。

このパーティショニングは個人および単一ユーザシステム用途にぴったりで、実際には、システム全体用と仮想メモリ (swap) 用の2 つのパーティションが作られます。

SSH接続の設定

comming soon...

参考:

サーバ名など、たくさんの名前が必要なときに使える「命名シリーズ」まとめ - masahirorの気まま記録簿

第 4 章 インストール

【HTML】再入門

f:id:TechHotoke:20220304174154p:plain

目的

HTMLについての基本的な考え方などをおさらいすること

前提

  • HTMLを用いたコーディング経験が既にあること
  • 詳細な説明(タグなど)については行わない

HTMLとは?

  • Hypertext Markup Languageの略称。ここでいうMarkupは「目印付け」を意味しており、デザインをするためのマークアップとは別物。

  • あくまでもHTMLはwebページ上の文章の構成要素の役割を記述する、いわばページの骨格を形成するもの。なので、ブラウザで見たときに、この見出しが h1 だと大きくて嫌だから h3 にしよう、だとか、ここに隙間が欲しいから br を入れよう、ここに線を入れたいから hr を入れようみたいなことはしてはいけません。

  • 人間はHTMLが変換された結果を読み、HTMLはコンピュータが読み込む。そのため、様々な環境の差異(フォントとか)を吸収するためにHTMLという世界的に統一された規格に基づく言語による記述が必要。

公式ページはこちら↓2WHATWG(Web Hypertext Application Technology Working Group)が定めています(少し前までW3Cと規格がごちゃごちゃだったものがWHATWGに統一された)

html.spec.whatwg.org

こちらは発見したHTML要素のチートシート。このようなものを無料で公開いただいている方がいることに頭があがりません。。。

htmlls.docs-share.com

HTMLで記述するにあたっての心得

  • 人間が理解しやすい文章構成になっていること
    • 論理構造が明確
    • 適切なハイライト
    • 主題を表すタイトル
    • 作者・更新情報
  • それぞれのタグは意味を持っているため、適切なメタデータ(headタグの情報など)やh1、dfnタグなどを用いることで索引や目次の自動生成などの処理を実現できるため、曖昧なタグの仕様や情報の過不足には注意を払う必要がある
  • 要素を選ぶときはセマンティクス(コードの断片の意味を指します。たとえば、「JavaScript でその行を実行すると、どのような効果があるのか?」、「その HTML 要素には、どのような目的や役割があるのか?」 (「どのように見えるのか?」ではなく)。)を基準にする。見た目はCSSで調整すること。

参考:

Dublin Core(ダブリン・コア): ウェブ資源メタデータの共通語彙

HTML は既定の表示スタイルに基づくのではなく、入力されるデータを表現するためにコーディングするべきです。プレゼンテーション (どのように見えるか) は、完全に CSS の責任です。

セマンティックなマークアップを書くことの利点の中には、次のようなものがあります。 検索エンジンが内容を重要なキーワードとして理解し、ページの検索ランキングを上げる (SEO を参照) 読み上げソフトが、目の不自由なユーザーがページを操作するのを補助するための標識として利用することができる 意味のあるコードのブロックを見つけることは、セマンティックまたは名前空間のついたクラスがあるにしろないにしろ、延々と div の中を検索するより明らかに簡単である 開発者に目立たせるデータの種類を提案することができる セマンティックな名前は、正しいカスタム要素やコンポーネントの名前を反映する どのマークアップを使用するか迷ったときは、自分自身に「どの要素が掲載しようとしているデータを最もよく説明または表現しているか」と自問してみてください。例えば、これはデータのリストか?順序付きか順序なしか?これは節と関連情報の補足から成る記事か?定義の一覧になっているか?キャプションを必要としている図または画像か?サイト内でグローバルのヘッダーおよびフッターに加えてヘッダーやフッターを持つべきか?などです。

引用:

Semantics (セマンティクス) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

アウトラインとは

  • アウトラインとは、 生物 哺乳類 猫 アメリカンショートヘアー うちで飼ってるタマ 犬 ブルドック うちで飼ってるイギー このように、情報の分類と階層になります。

階層に関しては、「の」を付けて矛盾をしない関係になるもの、と考えるとわかりやすいです。

生物の哺乳類 哺乳類の猫 猫のアメリカンショートヘアー 「の」を付けて矛盾が起きません。

もし、以下のようなアウトラインだった場合、

カレーのレシピ

レシピの作者 材料 ( 8皿分 ) 調味料 作り方 ポイント このレシピの生い立ち コメント レシピの作者の材料 ( 8皿分 )

レシピの作者の調味料

レシピの作者の作り方

レシピの作者のポイント

レシピの作者のこのレシピの生い立ち

レシピの作者のコメント

は、おかしいです。

材料は、レシピの作者の材料ではなく、カレーの材料になります。 なので、レシピの作者 と 材料 ( 8皿分 ) は親子関係を築けません。 ほかも同様です。

例えば、このようなアウトラインにすると、

カレーのレシピ レシピの作者 材料 ( 8皿分 ) 調味料 作り方 ポイント このレシピの生い立ち コメント 「の」を付けると、

カレーのレシピの(レシピの)作者 カレー(のレシピ)の材料 ( 8皿分 ) カレー(のレシピ)の調味料 (この)カレーのレシピ(による)作り方 カレーのレシピのポイント カレーのレシピの(このレシピの)生い立ち カレーのレシピのコメント と、矛盾が起きません。

厳密にはアウトラインの親子関係は、セクションに内包される情報を見て親子関係であるかを判断するのであって、一つの簡単なヒントを得るために、「の」の関係を見る、ことを提案しましたが、実際の見出しに使われている言葉が「の」の関係を正しい日本語の形で接続しないことが通常です。

なので、「の」関係に矛盾がないかを調べる際は上記のように、見出しの言葉をアレンジする必要がありますが、その点は本質ではなく、セクションに内包される情報を見て親子関係であるかを判断する、ということが重要です。

また、以下のようなアウトラインの設計をした方も結構います。

例)

カレーのレシピ レシピの作者 材料 ( 8皿分 ) 調味料 作り方 ポイント このレシピの生い立ち コメント 例)

カレーのレシピの(レシピの)作者 カレー(のレシピ)の材料 ( 8皿分 ) 材料 ( 8皿分 )の調味料 (この)カレーのレシピ(による)作り方 作り方のポイント カレーのレシピの(このレシピの)生い立ち カレーのレシピのコメント これも矛盾がないですね。 どれが正しいかは、文章を読んだ上で、その文章の情報から

どのような設計が読み手に対して理解しやすいか 作者はどのような意思を持ってこの文書を書いたか を考え、マークアップします。 つまり、明らかな間違いはありますが、人の解釈によって設計は異なってくるので、唯一の正解はない、ということになります。

これが、アウトラインになります。

アウトラインに合わせた h タグをふることも重要です。

以下のアウトラインだった場合、

カレーのレシピ レシピの作者 材料 ( 8皿分 ) 調味料 作り方 ポイント このレシピの生い立ち コメント section の入れ子構造でアウトラインを表現すれば、hタグは何にしてブラウザの解釈は変わりません。

全てをh1にしても、

カレーのレシピ

レシピの作者

材料 ( 8皿分 )

調味料

作り方

ポイント

このレシピの生い立ち

コメント

以下のように解釈されます。

カレーのレシピ レシピの作者 材料 ( 8皿分 ) 調味料 作り方 ポイント このレシピの生い立ち コメント ただ、コードとしては見にくいです。以下のように、アウトラインの階層に合わせてhタグの数字(レベル)をふると、その数字を見ただけで階層がすぐにわかり、コードとして読みやすくなります。

アウトラインの階層に合わせると、以下の様になります。

カレーのレシピ

レシピの作者

材料 ( 8皿分 )

調味料

作り方

ポイント

このレシピの生い立ち

コメント

ここで重要なのが、hタグのレベルとコンテンツの重要さは関係ないことです。これは大事ではないから、h5を使う、というのは間違いです。

実際に書いてみた

HTMLの練習(このレシピにマークアップをしてみましょう)

修正

dl は説明リストを表すタグなのですが、dl の中にある dt dd が一つ一つの項目なります。今回のようなマークアップだと、dl が一つの項目ごとに区切られてしまっているので、リストにならなくなってしまいます。また、dl には、dt (説明をするもの)、dd(説明)がセットである必要があります。dl のよく使われるシチュエーションとして、商品説明やブログ記事などのmetaデータに使います。 例えば小説の通販サイトでは、こういうのをよく書きます。

上記ご指摘頂いたため、olリストに修正しました(20220305)

参考

HTML5マークアップの心得と作法

文章のアウトラインを表す主な要素 < HTML | HTML+CSSまとめページ

https://developer.mozilla.org/ja/docs/Web/HTML/Element/dl

今回はここまでになります! お付き合いいただきありがとうございました!

【Ruby】InteliJでRubyの開発環境をセットアップする

f:id:TechHotoke:20220304005050p:plain

目的

Rauby/Railsを使用することになり、InteliJのUltimateライセンスを登録していたので、せっかく&勿体無いのでInteliJをRubymineっぽく使ってみてVSCodeとどっちがいいか決める(前書いた記事ではVSCode使ってました)

前提

  • InteliJ Ultimateライセンスに登録していること
  • Rubyがインストールされていること
  • Railsがインストールされていること
  • rbenvがインストールされていること

環境

導入

  • Ruby/Railsの導入はこちらを参考にしてください

techhotoke.hatenablog.com

  • InteliJのプラグインRubyをインストール f:id:TechHotoke:20220303225942p:plain

  • InteliJの再起動

  • 新規プロジェクト作成欄にRubyRailsプロジェクトが選択できることを確認 f:id:TechHotoke:20220303230124p:plain

プロジェクトを作ってみる

  • Railsプロジェクトを作成します

  • 実行してみます

  • こんなエラーが出たので対処します。

/Users/yudainoda/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/msgpack-1.4.5/lib/msgpack.rb:7:in `require': dlopen(/Users/yudainoda/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/msgpack-1.4.5/lib/msgpack/msgpack.bundle, 9): no suitable image found.  Did find: (LoadError)
  • 表示されているパスを確認したところ、該当するgemファイル存在するようで、M1 Macだとこのようなエラーが報告されている的な記事を見つけたので、一度gem uninstall msgpackを実行

  • 複数バージョンが存在していたようなので、それらを一括削除してgem install msgpackを実行

  • アプリケーションを起動すると、今度はwebpacker.ymlが無いから作れと怒られました。

/Users/yudainoda/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/webpacker-5.4.3/lib/webpacker/configuration.rb:103:in `rescue in load': Webpacker configuration file not found /Users/yudainoda/Documents/workspace-STS/sample_IJ_app/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/yudainoda/Documents/workspace-STS/sample_IJ_app/config/webpacker.yml (RuntimeError)
  • config直下にwebpacker.ymlを作成しました
# Note: You must restart bin/webpack-dev-server for changes to take effect

default: &default
  source_path: app/javascript
  source_entry_path: packs
  public_root_path: public
  public_output_path: packs
  cache_path: tmp/cache/webpacker
  webpack_compile_output: true

  # Additional paths webpack should lookup modules
  # ['app/assets', 'engine/foo/app/assets']
  additional_paths: []

  # Reload manifest.json on all requests so we reload latest compiled packs
  cache_manifest: false

  # Extract and emit a css file
  extract_css: false

  static_assets_extensions:
    - .jpg
    - .jpeg
    - .png
    - .gif
    - .tiff
    - .ico
    - .svg
    - .eot
    - .otf
    - .ttf
    - .woff
    - .woff2

  extensions:
    - .mjs
    - .js
    - .sass
    - .scss
    - .css
    - .module.sass
    - .module.scss
    - .module.css
    - .png
    - .svg
    - .gif
    - .jpeg
    - .jpg

development:
  <<: *default
  compile: true

  # Reference: https://webpack.js.org/configuration/dev-server/
  dev_server:
    https: false
    host: localhost
    port: 3035
    public: localhost:3035
    hmr: false
    # Inline should be set to true if using HMR
    inline: true
    overlay: true
    compress: true
    disable_host_check: true
    use_local_ip: false
    quiet: false
    pretty: false
    headers:
      'Access-Control-Allow-Origin': '*'
    watch_options:
      ignored: '**/node_modules/**'


test:
  <<: *default
  compile: true

  # Compile test packs to a separate directory
  public_output_path: packs-test

production:
  <<: *default

  # Production depends on precompilation of packs prior to booting for performance.
  compile: false

  # Extract and emit a css file
  extract_css: true

  # Cache manifest.json for performance
  cache_manifest: true
  • 再びアプリケーションを起動すると今度はイケてるっぽいので、http://0.0.0.0:3000/にアクセスしてみるとwelcomeページが表示されました。

f:id:TechHotoke:20220304004728p:plain

  • VScodeで環境を作ったときに比べて、はまりどころが多い印象でしたが、IDEに起因しているか微妙なエラーが多発していたため、今後も同じような現象が発生して、それに対する処置が設定な度でカバーできない場合はVSCodeで開発した方がストレスフリーに行ける気がします。または、環境構築はVSCodeで行って開発はInteliJで行うかですかね。。。

今回はここまでとなります! お付き合い頂きありがとうございます!

【Rails】gemってなに?美味しいの?

f:id:TechHotoke:20220304002744p:plain

目的

gemについての理解を深めること

gem is なに?

  • 外部ライブラリのフォルダやファイルを1つのgem形式のファイルにパッケージングされたもの

f:id:TechHotoke:20220304000718p:plain

  • Rubyにおけるgemは以下の2つの意味を指すことがあるそうです

    • RubyGemsが公開しているRubyのパッケージのこと
    • それらのパッケージを管理するパッケージ管理システムの名前

後者の意味でのgemは使用される機会が少なく、現在は前者の意味合いで使用されることが多いらしいです。

後者の意味で使用されるのは「bundler」になるそうです。

参考:

【Ruby】 gemの仕組みを図解形式で学ぼう | Pikawaka

ライブラリ

ruby gems is なに?

Ruby 1.9 以降からの標準ライブラリ

bundler is なに?

bundlerとはgemを管理するためのツールで、bundler自体もgemの一種です。bundlerを使うことで、複数のgemの依存関係を保ちながらgemの管理ができます。

参考:

Bundler: The best way to manage a Ruby application's gems

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>

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

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

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

Vue×SpringでSPA作成13【MyBatisの導入、Read処理~フロントエンド偏~】

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の情報を一覧表示する

実装

  • まず最初に npm install axios --save コマンドなどでaxiosをインストールします

  • main.jsに依存関係を追加します

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/'
import axios from 'axios'
import vuetify from '@/plugins/vuetify'

Vue.config.productionTip = false

axios.defaults.baseURL = '/api';

// Cookieを有効にする。
axios.defaults.withCredentials = true;
Vue.prototype.$axios = axios;

Vue.use(vuetify)

new Vue({
  router,
  store,
  axios,
  vuetify,
  render: h => h(App)
}).$mount('#app')
  • src/frontend/views配下にDankaList.vueを作成します。
<template>
  <div>
      <template>
        <v-data-table
          :headers="headers"
          :items="dankaList"
          :page.sync="page"
          :items-per-page="itemsPerPage"
          class="elevation-1"
          @page-count="pageCount = $event"
          hide-default-footer
        >
          <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: {
      seshuName: "",
      koshuName: "",
      koshuName2: "",
      address: "",
      phonenumber: "",
      mailAddress: "",
      remark: ""
    },
    defaultItem: {
      seshuName: "",
      koshuName: "",
      koshuName2: "",
      address: "",
      phonenumber: "",
      mailAddress: "",
      remark: ""
    },
  }),
  computed: {
    formTitle() {
      return this.editedIndex === -1 ? "新規登録" : "編集";
    },
  },
  created() {
    this.getDanakList();
  },
  methods: {
    getDanakList() {
      const _this = this;
      this.$axios.get("/danka").then(response => {
        _this.dankaList = response.data;
      });
    },
  • 細かい説明は省きますが、一覧表示・ページネーション、編集・削除ボタンの作成を行っています。上記の実装で下記のような画面が表示されればOKです。

f:id:TechHotoke:20220227000911p:plain

ポイントとなるのはこの部分で、

  created() {
    this.getDanakList();
  },
  methods: {
    getDanakList() {
      const _this = this;
      this.$axios.get("/danka").then(response => {
        _this.dankaList = response.data;
      });
    },
  1. createdメソッドがDOMの生成前に走る
  2. getDankaListメソッドが走る
  3. /dankaというパスでaxios経由でバックエンドから一覧データを取得しdankaList変数に格納する(前回のDankaControllerに作成したgetAllDankaメソッドが呼ばれています)
  4. dankaList変数の各プロパティをテーブル上に表示する

参考: vue.jsのcreatedとmountedの違いを目で見て理解 | アールエフェクト

今回はここまでとなります! お付き合い頂きありがとうございます!

Vue×SpringでSPA作成 11 - 2【セキュアな通信で安心したい】

f:id:TechHotoke:20220224185505p:plain

まえがき

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

techhotoke.hatenablog.com

目的

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

前提

  • 基本的なJavaの知識やSpring、Vueの知識があること
  • AWSに関する基本的な知識があること
  • ネットワークに関する基本的な知識があること
  • ドメインが取得されていること

環境

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

やること

  • クライアントからの通信のSSL
  • ALB-EC2間の通信のSSL

SSL(Secure Socket Layer)とは

ウェブブラウザとウェブサーバ間でのデータの通信を暗号化し、送受信させる仕組み。 http通信のままだと、パスワードなどの認証情報が平文のままネットワーク上を流れてしまうため、情報漏洩のリスクが常に付きまといます。 ただし、SSL化していても、プロキシサーバーなどを用いた中間者攻撃では盗聴・改ざんが出来るため注意は必要です。。。

クライアントーALB間のSSL

  • AWS Certificate Managerから証明書>証明書をリクエストを選択
  • ドメイン名を入力しリクエストを作成
  • 発行された証明書のリンクを押下
  • Route53でレコードを作成を押下
  • ロードバランサーのリスナーを選択
  • プロトコルがHTTP通信の項目を選択
  • プロトコルHTTPSに変更し(ポートは443のまま)で、画面下部の項目を添付画像のように変更します

f:id:TechHotoke:20220222173113p:plain

  • ELBに割り当てているセキュリティグループのインバウンドルールを80(HTTPのポート)から443(HTTPSのポート)に変更します

  • https通信でURLにアクセスし、南京錠のアイコンが表示されていればOKです(一応HTTP通信を試してみるとポートを開いていないのでアクセスできません)

f:id:TechHotoke:20220222181848p:plain

ALB-EC2間のSSL

つづいて、ALBとEC2間の通信もHTTPS化していきます。

  • EC2インスタンスに割り当てられているセキュリティグループのインバウンドルールをHTTPS443ポートに変更

  • ターゲットグループのリスナープロトコルは443、ルーティングプロトコルは8080に設定します

f:id:TechHotoke:20220222191122p:plain

  • statusがhealthyであることを確認し、再度URLにアクセスしてみます

  • 502エラーが出ているので対処します

  • リスナーに80番ポートを追加して、ターゲットグループに先ほどまでHTTP通信をおこなっていたインスタンスを選択。

  • EC2インスタンスに80番ポートを開けて、アクセスしてみます。

  • 502エラーは返されず、画面が表示されました。なのでHTTPS通信固有の問題のようです。

  • ALBのモニタリングタブを確認すると、ログにエラーが出力されているため、ALBがエラーを返却していることが分かります。

f:id:TechHotoke:20220222191453p:plain

  • 下記記事によると、ELB-EC2インスタンス間でHTTPS通信を行う場合、第三者制の証明書が必要になるようなので、ここが原因のような雰囲気が漂っていますね。。。

EC2 インスタンスとロードバランサー間の SSL 接続を有効にする

三者SSL証明書の取得

  • 今回はLet'sEncriptから取得したいと思います。(無料ですが、90日で期限が切れるので、自動更新設定などは別途行う必要があります)

はじめる - Let's Encrypt - フリーな SSL/TLS 証明書

  • シェルの管理者権限がある場合は、Certbot という ACME クライアントを使うのが推奨されているので、インストールします

f:id:TechHotoke:20220222191805p:plain

チュートリアル: Amazon Linux 2 に SSL/TLS を設定する - Amazon Elastic Compute Cloud

<ここの続きの部分ですが、EC2-ELB間の通信はプライベートIPアドレスにて行われるという点から通信をSSL化する必要性がそもそもあまりないということから実装は行わないこととしました。EC2-ELB間の通信をSSL化するためには第三者制の証明書が必要だいう学びなどがあったのでそのまま記事としては残しますが、悪しからず。。。>

参考

AWSでSSL化する方法を伝授!!! - Qiita

Application Load Balancerで設定する4種類のポート番号の意味を理解しよう | DevelopersIO