【SQL】SQLで再帰処理を実現する
with句とunion allを使用することで、SQLで再帰処理を実現することが可能になります。
再帰を用いることで、大量のダミーデータを簡単に作成したり、階層構造になっているデータの取得が簡単に実現できるようになります。
ただしこのこの構文はDBMSによっては実装されていなかったり、若干書き方が変わる可能性があるので、注意が必要です。
再帰を用いたダミーデータの作成
以下は、PostgreSQLとOracle DBで、再帰を使用してダミーデータを表示できるSQL文を示します。
-- PostgreSQLの場合 with recursive DUMMY(i) as (select 1 i union all select i+1 from DUMMY where i < 10000) select i as id, 'テスト' as name from DUMMY;
-- Oracle DBの場合 with DUMMY(i) as (select 1 i from dual union all select i+1 from DUMMY where i < 10000) select i as id, 'テスト' as name from DUMMY;
結果はどちらも以下のようになります。
id | name |
---|---|
1 | テスト |
2 | テスト |
... | テスト |
10000 | テスト |
union allの下のselect文のwhere句の数値を変えることで、表示されるレコード件数を変更することができます。
大量のテストデータを作成する場合などに使用できるテクニックです。
簡単に中身を解説すると、
union all の上に書いているselect文は、それ単体で実行できるselect文を指定します。
union all の下に書いているselect文では、with句で指定した名前をテーブル名として、union allの上で書いたselectのカラムそのテーブルのカラムとして使用することができます。
階層構造のデータを取得する
データベースで階層構造のデータを扱いたい場合があります。
階層構造とは、PCのフォルダ(ディレクトリ)のような、入れ子構造になっているデータのことです。
業務系のシステムだと「部門」「カテゴリ」「製品」などは階層構造で表現されることも多いです。
部門の場合、例えば「東京事業所」という部門があり、その中に「開発部」と「営業部」があり、それぞれに「1課」「2課」と分かれていたりします。
- 東京事業所
- 開発部
- 1課
- 2課
- 営業部
- 1課
- 2課
- 開発部
カテゴリの場合、例えば「料理」というカテゴリがあり、その中に「和食」「中華」「イタリアン」などがあり、その中にさらに細かいカテゴリがあります。
以下は、興味のある分野を洗濯する際に使用することをイメージしたカテゴリの例です。
- 料理
- 和食
- 寿司
- 天ぷら
- イタリアン
- パスタ
- ピザ
- 和食
- スポーツ
- 球技
- 野球
- サッカー
- 陸上
- 球技
- 音楽
- ヒップホップ
- J-POP
今回はこの例をテーブルで扱う方法を考えます。
まずはサンプルデータを作成します。
create table category as select 1 id, '料理' name, null parent_id union all select 2, 'スポーツ', null union all select 3, '音楽', null union all select 4, '和食', 1 union all select 5, 'イタリアン', 1 union all select 6, '球技', 2 union all select 7, '陸上', 2 union all select 8, '寿司', 4 union all select 9, '天ぷら', 4 union all select 10, 'パスタ', 5 union all select 11, '野球', 6 union all select 12, 'サッカー', 6 union all select 13, 'ヒップホップ', 3; union all select 14, 'J-POP', 3;
テーブルで見ると以下のようになる。
id | name | parent_id |
---|---|---|
1 | 料理 | null |
2 | スポーツ | null |
3 | 音楽 | null |
4 | 和食 | 1 |
5 | イタリアン | 1 |
6 | 球技 | 2 |
7 | 陸上 | 2 |
8 | 寿司 | 4 |
9 | 天ぷら | 4 |
10 | パスタ | 5 |
11 | 野球 | 6 |
12 | サッカー | 6 |
13 | ヒップホップ | 3 |
14 | J-POP | 3 |
idはテーブルのプライマリーキーで適当な連番を振っています。
nameはカテゴリの名称。parent_idは、そのカテゴリが属している親カテゴリのidが入っています。
最上位のカテゴリの場合はparent_idはnullを指定します。
この時、料理のカテゴリに含まれるサブカテゴリを全て(階層をたどって最下層のカテゴリのデータも含めて)取得する場合を考えます。
PostgreSQLの場合、以下のようなSQLで実現することができます。
with recursive ca(id, name, i) as ( select id, name, 1 i from category p where id = 1 union all select category.id, category.name, i + 1 from category join ca on parent_id = ca.id ) select id , name, i from ca
結果は以下のようになります。
id | name | i |
---|---|---|
1 | 料理 | 1 |
4 | 和食 | 2 |
5 | イタリアン | 2 |
8 | 寿司 | 3 |
9 | 天ぷら | 3 |
10 | パスタ | 3 |
料理のレコードと、そのサブカテゴリである和食とイタリアン、さらにそのサブカテゴリである寿司・天ぷら・パスタが表示されています。
階層がどこまで深くなっても、料理のカテゴリに紐づいているレコードは全て表示されます。
ここでiは、階層の深さを示しています。
union allの上のselectで指定したレコードの階層を1として、そこから階層が1つ深くなるごとに1ずつ加算されていく仕組みです。
※Oracle DBの場合は、connect by 句を使用することで、with句を使用するよもシンプルに階層構造のデータを取得することも可能です。
【Java】入れ子構造のデータの表示
例えば、メインカテゴリを一覧で表示するときに、そのメインカテゴリに対するサブカテゴリも入れ子にして一覧で表示したいような場合があります。
例えば以下のような表示をしたい時。
- メイン1
- サブ1
- サブ2
- サブ3
- メイン2
- サブ1
- サブ2
- サブ3
このような表示は多くのWebアプリケーションで見られるデータの構造です。
例えば、4択問題による学習アプリを作成する場合に、問題の一覧を表示させながら、それぞれの問題に対する選択肢を表示する、のような場合にも適用することができます。
DBから取得したデータを使ってJavaを使ったWebアプリケーションでこのような表示をしたい場合を想定します。
メインとサブは別テーブルでデータが保持されていて、外部キー参照で紐づいているイメージです。
メイン
id | name |
---|---|
1 | メイン1 |
2 | メイン2 |
サブ
id | name | parent_id |
---|---|---|
1 | サブ1 | 1 |
2 | サブ2 | 1 |
3 | サブ3 | 1 |
4 | サブ1 | 2 |
5 | サブ2 | 2 |
6 | サブ3 | 2 |
Javaを使用してこのような表示をする場合、おそらくメインカテゴリ情報を保持するクラスとサブカテゴリ情報を保持するクラス(それぞれJavaBeansのクラス)を用意することになるでしょう。
個人的に、すぐに思いつく方法としては大きく3つ。
- SQLでgroup by, max関数, case式, string_agg関数などを駆使して、メインカテゴリごとに1レコードとなるようにデータを取得する。
- メインとサブのテーブルを結合してまとめてデータを取得し、画面側(VIEW側)で条件分岐の構文を使用して制御する。
- メインのJavaBeansクラスに、サブのクラスをListなどで保持し、画面側ではシンプルな2重ループになるようにデータを作成する。
1の方法は、SQLが少し複雑になりますが、Javaのプログラムと画面側のソースは比較的シンプルになりそうです。
ただし、サブカテゴリとして表示したいものの数が決まっている場合にしか有効ではなく、上記の例で言えば、サブ4が登場した時にソースコードを修正する必要が出てきて、拡張性に乏しいです。
2の方法は、SQLもJavaのプログラムもシンプルになります。データ保持用のJavaBeansのクラスで、メインとサブ両方のデータを保持するようにフィールド(プロパティ)を保持しておく必要があります。
画面側で条件分岐を駆使して表示をコントロールすることになりますが、サブ側のループ処理で、メイン側のidとサブ側のparent_idが同じ場合のみ表示するようにすればシンプルに実装できそうです。
3の方法は、SQLも画面もシンプルに実装できますが、Javaの方でデータ保持用のインスタンスを色々と変換する作業が必要になります。
3の方法
ここでは3の方法についてのサンプルコードを示します。
話をシンプルにするために、DBへの接続は省略します。
また、表示も標準出力を利用したコンソールへの出力にしています。
import java.util.List; import java.util.ArrayList; public class Category { private int id; private String name; // サブ用 private SubCategory subCategory; private List<SubCategory> subList; public Category() { } public Category(int id, String name) { this.id = id; this.name = name; } public Category(int id, String name, int subId, String subName) { this.id = id; this.name = name; this.subCategory = new SubCategory(subId, subName, id); } public String getName() { return this.name; } public SubCategory getSubCategory() { return this.subCategory; } public List<SubCategory> getSubList() { return this.subList; } public void setSubLust(List<SubCategory> list) { this.subList = list; } public void initSubList() { this.subList = new ArrayList<>(); } public void addSubCategory(SubCategory sub) { this.subList.add(sub); } // ListのCntainsで存在チェックしたときに、idとnameが同じなら同じと見なすようにする @Override public boolean equals(Object obj) { if(obj != null && obj instanceof Category) { Category c = (Category)obj; if(this.id == c.id && (this.name != null && this.name.equals(c.getName()))) { return true; } else { return false; } } else { return false; } } }
public class SubCategory { private int id; private String name; private int parentId; public SubCategory() { } public SubCategory(int id, String name, int parentId) { this.id = id; this.name = name; this.parentId = parentId; } public String getName() { return this.name; } // その他のアクセッサは省略 }
import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { List<Category> list = new ArrayList<>(); list.add(new Category(1, "Main1", 1, "sub1")); list.add(new Category(1, "Main1", 2, "sub2")); list.add(new Category(1, "Main1", 3, "sub3")); list.add(new Category(2, "Main2", 4, "sub4")); list.add(new Category(2, "Main2", 5, "sub5")); list.add(new Category(2, "Main2", 6, "sub6")); // リストへの詰め替え。 List<Category> newList = new ArrayList<>(); for(Category c : list) { if(!newList.contains(c)) { newList.add(c); newList.get(newList.indexOf(c)).initSubList(); } newList.get(newList.indexOf(c)).addSubCategory(c.getSubCategory()); } // 出力 for(Category c : newList) { System.out.println("main : " + c.getName()); for(SubCategory s : c.getSubList()) { System.out.println("sub : " + s.getName()); } } } }
結果
main : Main1 sub : sub1 sub : sub2 sub : sub3 main : Main2 sub : sub4 sub : sub5 sub : sub6
【JavaScript】星マークでの5段階評価
商品やお店のレビューなどでよく見かけるような、星マークでの5段階評価を行うJavaScriptプログラムのサンプルです。
実際のサービスでのプログラムでどのようなプログラムになっているかは不明ですが、自分なりに作成してみたので、レビューや評価ができるWebアプリ作成の参考に。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> .star { cursor: pointer; /* クリックできるような表示にする */ font-size: 18px; } </style> <title>5段階評価</title> </head> <body> <div id="stars"> <span class="star" data-star="1">☆</span> <span class="star" data-star="2">☆</span> <span class="star" data-star="3">☆</span> <span class="star" data-star="4">☆</span> <span class="star" data-star="5">☆</span> </div> <script> const stars = document.getElementsByClassName('star'); // 星マークにマウスオーバーした時のイベント const starMouseover = (e) => { const index = Number(e.toElement.getAttribute('data-star')); for(let j=0; j < index; j++) { stars[j].textContent = '★'; } } // 星マークからマウスが離れた時のイベント const starMouseout = (e) => { for (let j=0; j < stars.length; j++) { stars[j].textContent = '☆'; } } for (let i=0; i < stars.length; i++) { stars[i].addEventListener('mouseover', starMouseover); stars[i].addEventListener('mouseout',starMouseout); // 星マークをクリックした時のイベント stars[i].addEventListener('click', e => { for (let j=0; j < stars.length; j++) { stars[j].textContent = '☆'; } const index = Number(e.toElement.getAttribute('data-star')); for(let j=0; j<index; j++) { stars[j].textContent = '★'; } // マウスオーバーとマウスアウトのイベント解除 for(let j=0; j<stars.length; j++) { stars[j].removeEventListener('mouseover', starMouseover); stars[j].removeEventListener('mouseout', starMouseout); } // // 非同期通信で情報をサーバーサイドに送信する // let data = new URLSearchParams(); // data.append('star',index); // fetch('abcde', { // method:'post', // body: data // }).then() // .catch(); }); } </script> </body> </html>
【JavaScript】オブジェクト指向
JavaScriptにおけるのオブジェクト指向の概要の説明。
ここではJavaやPHPなど、サーバーサイド用の言語のオブジェクト指向の知識がある程度備わっている前提で話を進めます。
JavaScriptは、元々オブジェクト指向を前提に作られた言語ではありませんが、歴史とともに徐々にオブジェクト指向が取り入れられてきました。
ES5(ECCMAScript 2015)からは、他の多くの言語でも採用されているクラスを用いたオブジェクト指向の書き方ができるようになり、Javaなどの言語のオブジェクト指向に似た書き方できるようになりました。
以下はES5以降でできるJavaScriptのクラス構文です。
Userクラスを定義し、そのクラスのインスタンスを作成してメソッドを実行します。
// クラスの定義 class User { // コンストラク constructor(_name, _age) { this.name = _name; this.age = _age; } // プロパティ get name() { return this._name; } set name(value) { this._name = value; } get age() { return this._age; } set age(value) { this._age = value; } // メソッド show() { console.log(`名前:${this.name} 年齢:${this.age}`); } } // オブジェクトの作成 const user = new User('Alice', 20); // メソッドの実行 user.show();
結果
名前:Alice 年齢:20
解説
- JavaScriptのクラスではいわゆるインスタンスフィールドを定義することはできません。ゲッター、セッター(get, set)によってプロパティを定義します。
- コンストラクタはconstructorにより定義します。
- JavaScriptのオブジェクト指向ではpublic, privateなどのアクセス修飾子を指定することはできません。全てがpublicとしての扱いになります。
static
メソッドにstaticをつけることで静的メソッドを定義することができます。
静的メソッドでは、オブジェクトを作成することなく、クラス名.メソッド名で呼び出すことができます。
class Calc { static triangle(base, height) { return base * height / 2; } } console.log(Calc.triangle(10, 3));
結果
15
継承
JavaScriptのオブジェクト指向では既存のクラスを継承することができます。
継承の考え方はその他のオブジェクト指向言語の継承とほとんど同じです。
既存のクラスのメソッドやプロパティを引き継ぎつつ、新たなメソッドやプロパティを追加することができます。
また、同名のメソッドを定義してメソッドを上書き(オーバーライド)することができます。
// Userクラスの継承 class Student extends User { constructor(_name, _age, _score) { super(_name, _age); this.score = _score; } get score() { return this._score; } set score(value) { this._score = value; } // オーバーライド show() { console.log(`名前:${this.name} 年齢:${this.age} スコア:${this.score}`); } } const student = new Student('Bob', 22, 80); student.show();
結果
名前:Bob 年齢:22 スコア:80
解説
- 既存のクラスを継承する場合はextendsキーワードを使用します。
オブジェクト
JavaScriptでclass構文が使用できるようになったのはES5以降。
それまではクラス構文はありませんでしたが、オブジェクトという概念自体はあります。
オブジェクトは以下のように表すことができます。
const user = { name : "satou", age : 20, show() { console.log('名前:' + this.name + ' 年齢:' + this.age); } }; // プロパティ参照方法1 console.log(user.name); // プロパティ指定方法2 console.log(user['name']); // メソッドの呼び出し user.show(); user.score1 = 45; // プロパティ追加 user.score2 = 55; console.log(user.score1); console.log(user['score2']);
functionを使ってオブジェクトを作る
functionの構文を使ってオブジェクトを作成することも可能です。
function User(name, age) { this.name = name; this.age = age; } let alice = new User('Alice', 20); console.log(alice.name); console.log(alice.age);
オブジェクトのプロトタイプ
Object のプロトタイプ - ウェブ開発を学ぶ | MDN
その他
- JavaScriptのオブジェクト指向では、他の言語のオブジェクト指向で使われている抽象クラスやインターフェースといった概念はありません。
- ただし、TypeScriptなどのaltJSを使うことで、より他のオブジェクト指向言語に近い文法でクラスを定義することが可能になります。
【JavaScript】少しリッチなカレンダーの作成
以前、シンプルなカレンダーを作成するJavaScriptのコードの記事を書きましたが、今回はボタンによって前月、次月のカレンダーを表示することができるカレンダーを作成しました。
Webアプリ、Webサイトでのカレンダー作成時の参考に。
CSSは省略しています。
<body> <div> <div class="month"> <a href="#" id="last-month"><b><</b></a> <a href="#" id="next-month"><b>></b></a> <span id="year"></span>年<span id="month"></span>月 </div> <table id="cal" class="calendar"> </table> </div> <script> 'use strict' function createCalendar(year, month) { const start = new Date(year, month, 1); // 月初 const last = new Date(year, month + 1, 0); // 月末 const startDate = start.getDate(); // 月初 const lastDate = last.getDate(); // 月末 const startDay = start.getDay(); // 月初の曜日 const lastDay = last.getDay(); // 月末の曜日 let days = []; let weekDay = []; let dayCount = 0; // 曜日カウント用 for (let i = startDate; i <= lastDate; i++) { if (i === startDate) { for (let j = 0; j < startDay; j++) { weekDay.push(''); dayCount++; } } weekDay.push(i); dayCount++; if (dayCount === 7) { days.push(weekDay); dayCount = 0; weekDay = []; } } for (let i = lastDay; i < 6; i++) { weekDay.push(''); } days.push(weekDay); let cal = `<tr> <th>日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> </tr>`; for (const week of days) { cal += '<tr>'; for (const day of week) { cal += '<td class="day">' + day + '</td>'; } cal += '</tr>'; } document.getElementById('cal').innerHTML = cal; document.getElementById('year').textContent = year; document.getElementById('month').textContent = month + 1; } document.getElementById('last-month').addEventListener('click', e => { e.preventDefault(); let year = Number(document.getElementById('year').textContent); let month = Number(document.getElementById('month').textContent); year = month === 1 ? year - 1 : year; month = month === 1 ? 12 : month - 1; createCalendar(year, month - 1); }); document.getElementById('next-month').addEventListener('click', e => { e.preventDefault(); let year = Number(document.getElementById('year').textContent); let month = Number(document.getElementById('month').textContent); year = month === 12 ? year + 1 : year; month = month === 12 ? 1 : month + 1; createCalendar(year, month - 1); }); const today = new Date(); // 現在の日時 createCalendar(today.getFullYear(), today.getMonth()); </script> </body>
【JavaScript】非同期通信
JavaScriptの非同期通信についてのまとめ
そもそも非同期とは
非同期を知るには、同時に同期を知る必要があります。
イメージとしては電話とメールの違い。
通常の会話や、電話、テレビ会議などは、同期コミュニケーションです。
1人が話しているときは、他の人は聞き手に周り、話しての話が終わったら聞き手だった人が話す。
話している人が終わるのを待ち、終わった後に次の人が話し出すのが同期コミュニケーション。
一方で、メールやチャットなどは非同期コミュニケーションです。
相手が今どういう状態かは知らなくても、一方的に連絡をすることができます。
たとえ相手からの返信がなかったとしても、次のメッセージを送ることも可能です。
これが非同期のコミュニケーション。
コンピュータにおける同期・非同期も考え方は同じです。
一つの処理が終わるのを待ってから次の処理の進むのが同期処理。
一方で、処理が終わるのを待たずに次の処理まで進むのが非同期処理です。
非同期処理
JavaScriptでは、setTimeoutやsetIntervalなどの、関数を引数とにとる関数が多数ある。
引数として渡される関数のことをコールバック関数と呼ぶ。
JavaScriptではコールバック関数の処理は非同期処理となる。
通常、if文やfor文を使って作られたプログラムは、上から順に実行されていき、1つ1つ処理が終わってから次の処理に進む。
JavaScriptでは、コールバック関数の処理は非同期となるため、setIntervalやsetTimeoutなどの関数は、コールバック関数の処理が終わっていなくても次の行の処理に進む。
このような処理がJavaScriptの非同期処理。
サンプル
// 非同期処理 let count = 0; let intervalId = setInterval(() => { count++; console.log(count); if(count >= 10) { clearInterval(intervalId); } }, 1000); let count2 = 0; let intervalId2 = setInterval(() => { count2++; console.log(count2); if(count2 >= 10) { clearInterval(intervalId2); } }, 1000);
結果は、1秒毎に1~10の数値が2個ずつ出力される。
それぞれのsetInterbal関数では、1秒毎に数値がカウントアップされて10まで出力される処理となっている。
同期処理の発想で考えると、1~10までが一つずつ出力されて、その後再び1~10までが出力されそうだが、実際には1~10がそれぞれ2個ずつ出力されるような結果になる。
それは、setIntervalの引数となるコールバック関数が終わるのを待たずに処理が進むため、結果としてコールバック関数の処理が非同期でそれぞれ独立して呼ばれているため。
setTimeoutとsetInterval
setIntervalを使うことで、指定したミリ秒毎にコールバック関数の処理を実行することができます。
一方、setTimeoutでは指定したミリ秒後に一度だけコールバック関数の処理が実行されます。
ですが、setTimeoutでも関数を再起呼び出しすることで指定したミリ秒毎に処理を実行することが可能です。
サンプル
function setNum1() { console.log('Hello'); setTimeoutId1 = setTimeout(() => { setNum1(); }, 1000); } // 関数の呼び出し setNum1(); // setTimeoutの処理の終了 // clearTimeout(setTimeoutId1);
setIntervalで処理を何度も実行する場合とsetTimeoutで処理を何度も実行する場合の違いですが、
setIntervalでは、きっかり指定したミリ秒後に処理が実行され、前回の処理が終わっていなかったとしても、次の処理が実行されます。
一方でsetTimeoutでは、処理が終了した後に指定されたミリ秒後に次の処理が実行されます。
処理に負荷をかけたくない場合などでは、setTimeoutの処理が実行されるそうです。
非同期通信
JavaScriptを使うことで、非同期でWebサーバーに対してHTTPリクエストを投げてHTTPレスポンスを受け取ることができます。
非同期通信を使うことで、HTMLのaタグやformタグを用いて画面遷移をしなくても、単一のWebページで画面遷移せずにサーバーと通信することができます。
以前はJavaScriptでの非同期通信といえばAjaxという技術を使うのが主流でしたが、現在はfetchと呼ばれる関数を使うことで以前よりも簡単に非同期通信を実現できるようになりました。
従来の同期通信による処理の場合、サーバーと通信するたびに画面遷移して画面が表示されるまで処理を待つ必要がありました。
非同期通信の場合、必要に応じてページの一部だけを書き換えることが可能です。
また、非同期通信の場合はサーバーとの通信処理の最中でもクライアントの処理を継続することができます。
結果として非同期通信を用いることによってアプリケーションのパフォーマンス向上につながります。
非同期通信の例
- Googleマップ
Googleマップでは、画面遷移することなく、マウスをドラッグするだけで新たな位置情報を取得して画面を最新に表示することができます。あの仕組みは非同期通信が使われています。 - 入力候補の表示
GoogleやYouTubeなどの検索欄では、入力した文字に応じて候補がいくつか表示されます。あの仕組みも非同期通信が使われています。 - SNSのいいねボタン
Twitterのいいねボタンなども、画面遷移することなく処理が実行されます。
fetchの例(GETの場合)
fetchを使った非同期通信の例です。
まずはGETの場合です。
ここではGETとPOSTについての細かい解説は割愛します。
fetch('sample.php?num1=10&num2=20') .then(response => response.json() .then(json => console.log(json.result)) ) .catch(error => console.log(error))
sample.php
<?php header("Content-Type: application/json; charset=utf-8"); $num1 = (int)$_GET['num1'] ?? ''; $num2 = (int)$_GET['num2'] ?? ''; $result = $num1 + $num2; $data = ['result' => $result]; echo json_encode($data);
サンプルのjavascriptの処理が実行されると、ブラウザのコンソールに30が表示されます。
サンプルではサーバーサイドの言語はPHPで書いていますが、Webサーバー上で動作するプログラムであればなんでも構いません。
解説
まずfetchを使ってsample.phpに対して、GETでHTTPリクエストを送信します。
GETでリクエストを送信する場合はURLにパラメータが付与されます。
サンプルではnum1とnum2というパラメータで数値を送信します。
PHP側ではnum1とnum2のパラメータを受け取り、数値として加算処理をして、結果をjsonデータで返します。
JavaScriptでは、responseという名前で結果を受け取り、データをjsonに変換してコンソールに表示しています。
JSON
ここではJSONについても簡単に説明しておきます。
jsonとはデータのフォーマットの一つです。
以下のようなデータフォーマットです。
{ name : 'Alice', age : 25, bloodTyoe: B, }
キー : 値 という組み合わせのデータをカンマ区切りで並べて書き、全体を{}(中括弧)で囲います。
これまで非同期通信で主流だったAjaxでは、XMLと呼ばれるデータ形式でのデータもやりとりされていました。AjaxのxはXMLのxです。
しかし、XMLはタグを用いでデータを表現するデータ形式で、各データに対して開始タグ、終了タグが必要になり、テキストの量が増えます。
また、JSONの書き方はJavaScriptのオブジェクトの書き方と同じであり、JavaScriptとの相性が良いため、近年非同期通信ではJSONを使ってデータのやり取りが主流です。
サンプルでは、PHPのプログラムによって以下のようなJSONファイルをレスポンスとして返します。
{ result : 30 }
thenとcatch
fetchではリクエストに成功した場合にはthenの処理が実行され、失敗した場合にはcatchの処理が実行されます。
ここでいう失敗は、ネットワークエラーなどでリクエストが送信できなかった場合です。
サーバー側でのプログラムのエラーなどではcatchの処理は実行されません。
サーバー側のプログラムでエラーが起きたかどうかはレスポンスのステータスコードなどを確認する必要があります。
thenとcatchの引数にはコールバック関数を指定します。
非同期通信に成功した場合にはResponseオブジェクトを得ることができるので、thenのコールバック関数ではResponseオブジェクト格納用の変数を指定します。
ここではアロー関数の形でresponseオブジェクトのjsonメソッドを実行しています。
Promise
thenとcatchはPromiseオブジェクトに大きく関係しています。
Promiseは非同期処理の完了を表すオブジェクトです。
fetchを使用した場合、Promiseが返ってきます。
Promiseで処理が完了した状態であればthen、失敗した状態であればcatchが実行されます。
また、レスポンスからJSONデータを取得するためにResponseオブジェクトのjsonメソッドを使用していますが、jsonメソッドもPromiseを返すため、さらにthenが入れ子の形になります。
fetchの例(POSTの場合)
続いてはPOSTの場合。
JavaScript
let data = new URLSearchParams(); data.append('num1', 100); data.append('num2', 200); fetch('sample.php', { method: 'POST', body: data, }) .then(response => response.json() .then(json => console.log(json.result)) ) .catch(error => console.log(error))
<?php header("Content-Type: application/json; charset=utf-8"); $num1 = (int)$_POST['num1'] ?? ''; $num2 = (int)$_POST['num2'] ?? ''; $result = $num1 + $num2; $data = ['result' => $result]; echo json_encode($data);
結果はブラウザのコンソールに300と出力されます。
解説
PHPのコードは、リクエストがPOSTになったので値の取得で扱う変数が$GETから$POSTに変わります。
JavaScriptですが、POST送信するときにはfetch関数の第二引数でオプションを指定します。
第二引数の指定は省略可能ですが、デフォルトではGET送信になるため、POSTで送信する場合は第二引数の指定が必須になります。
POSTのでパラメータを送信したい場合はオプションのbodyに指定します。
bodyからデータを送信する場合にはURLSearchParamsのインスタンスに対してappendでデータ追加します。
Promiseって結局何?
Promiseは、非同期通信の完了を表すオブジェクトとのことでしたが、結局のところなんなん?と思っている人もいるのではないでしょうか。
以下のようなプログラムを考えてみます。
let data = new URLSearchParams(); data.append('num1', 100); data.append('num2', 200); let sum = 0; // 変数の初期化 fetch('sample.php', { method: 'POST', body: data, }) .then(response => response.json() .then(json => sum = json.result) ) .catch(error => console.log(error)) console.log(sum); // コンソールへの出力
変数sumを定義します。非同期通信で結果をsumに代入し、最後にコンソールに出力します。
sample.phpの処理でリクエストで送られたパラメータの値を合計し、結果として300が返ってくるとした場合、最終的にコンソールには何が表示されるでしょうか。
非同期処理のプログラムを作成したことがない人にとっては、直感的には300が出力されそうな気もしますが、結果は0が出力されます。
これはつまり、fetchの処理が非同期であるが故に、fetchの処理が終了する前にコンソールへの出力処理が実行されていることになります。
fetchの処理が確実に完了していることを保証した上で処理をしたい場合、thenの中に書くことで非同期処理が完了した前提で処理をすることができます。
参考サイト
HTTPリクエストとHTTPレスポンスについて
fetchの詳細
Promiseについて
Responseオブジェクトについて
Content-Typeについて
おまけ
iQueryを使用する場合
iQueryの場合は、ajaxというメソッドを使うことで簡単に非同期通信が利用可能。
サンプル。
$.ajax({ url: $form.attr('action'), type: $form.attr('method'), data: $form.serialize(), dataType: 'json', beforeSend: function(xhr, settings) { // Buttonを無効にする $('.add-cart').prop('disabled', true); } }).done(function(data) { // 成功したときの処理 }).fail(function(data){ // 失敗したときの処理 }).always(function(data) { // 常に実行したときの処理 });
.doneがfetchの場合の.thenに、.failがfetchの.catchに該当するイメージ。
JavaScriptでAjaxを使用する
JavaScriptでAjaxを使用する場合は、XMLHttpRequestオブジェクトを使用します。
fetchに比べると複雑で、現在JavaScriptで非同期通信を行うのであればfetchを使うのがおそらく主流になるとは思いますが、他の人が書いたAjaxのソースを読む機会があったときのために概要を知っておくと良いかも。
サンプル
let xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { // 通信が完了した時 if(xhr.readyState === 4) { // 通信が成功した時 if(xhr.status === 200) { // console.log('通信成功'); let json = JSON.parse(xhr.response); console.log(json.result); } else { console.log('通信失敗'); } } else { // console.log('通信中'); } }; // リクエストを初期化 xhr.open('GET', 'sample.php?num1=10&num2=20', true); // リクエスト送信 xhr.send(null);
axios
fetch, ajax以外で非同期通信を実現する技術としてaxiosと呼ばれる技術があります。
axiosの概要は以下
axiosとはブラウザやnode.js上で動くPromiseベースのHTTPクライアントです。
参考サイトはこちらになります。
https://www.willstyle.co.jp/blog/2751/
以前Vue.jsを使って開発を行なったときに使用しました。
Vue.jsでは非同期通信にはaxiosを使用するのが一般的なんだそう。
とはいえ、正直なところfetchと大きな差はなさそうなので、好みの問題かもしれません。
サンプル
// POSTの場合 params = new URLSearchParams(); params.append('type', 1); axios.post('./getUseTableList', params) .then(res => this.tableList = res.data) .catch(error => console.log(error)); // GETの場合 axios.get('./getUseTableList') .then(res => this.tableList = res.data) .catch(error => console.log(error));
fetchとの違いは、axiosの場合はGET送信の場合はgetメソッドを、POST送信の場合はpostメソッドを使用します。
非同期通信の結果のデータはレスポンスのdataに格納されます。
非同期関数を作る
functionを宣言するときにasyncキーワードをつけることで、非同期関数を作ることができます。
非同期関数ではPromiseオブジェクトを返却する必要があります。
非同期関数の中では、awaitキーワードを使用することができます。
awaitを使うことで、非同期関数の処理を一時停止してPromiseの解決を待つことができます。
非同期関数では、awaitを使うことで、Promiseチェーンを書き換えることも可能です。
詳しくは下記の公式サイトを参照ください。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function
Herokuで構築したシステムをCentOS8環境に移行した時の作業ログ
無料で使えるPaasのサービスであるHerokuにPHPを使ったシステムを構築していましたが、さくらのVPSを契約してCentOS8環境に移行したので、その時の手順をまとめます。
移行前の環境
- サーバー環境:Heroku
- DB:PostgreSQL(Heroku Postgres)
- 言語:PHP
概要
システムの概要は本筋と関係ないので省略。
PHPで作成したWebアプリケーションをHerokuのフリープランの中でデプロイして稼働。
DBはHerokuのサービスの中で無料で使用することができるHeroku Postgresを使用。
移行後の環境
移行しようと思った背景
稼働時間の解決
Herokuのフリープランだと、初回アクセスに時間がかかる。
初回アクセスから30分は通常通りに動作するが、また30分たつと初回アクセスが遅くなる。
dynoというHerokuのコンテナが、起動するときに多少時間がかかるのだが、フリープランだと、基本的にはdynoは起動しておらず、アクセスされて初めて起動。
その後アクセスし続ければ遅くなることはないが、30分アクセスがなければ自動でまた停止してしまう。
解決するためには定期的にcurlコマンドなどでリクエストを投げれば解決できる。
cronとかタスクスケジュールでローカルのPC定期的から定期的にリクエスト投げれば良いが、ローカルのPCの電源が付いている必要がある。
また、Herokuのフリープランではトータル稼働時間に制限がある。(確か600時間くらい)
クレジットカードを登録することで無料のままでもトータル稼働時間は伸ばせる。 ただ、1つのシステムだけ稼働していれば十分対応できるが、システムを2つ以上稼働させようと思うとフリープランでは厳しい。ドメインの取得
自分でサーバーを構築、ドメインの取得をすれば、サブドメインとバーチャルホストの機能を使うことで、1つのサーバーで複数のシステムを構築することができる。DBの統一
HerokuだとアプリケーションごとにDBを作成する必要があるし、レコード件数に縛りがある。
自分でサーバー構築したら、複数のシステムがあってもDBを1つにできるしレコード数の制限もない。httpsにしたかった
個人情報の入力やログイン処理などがあるが、Herokuではスキームはhttpになっている。
Herokuでもhttpsにすることはできるみたいだが、聞いた話少々面倒らしいので、自分でサーバーを構築した方が手っ取り早そうだった。Linux上で色々遊ぶことができる
CentOS8上で構築できるので、システムの動作環境以外でも色々Linux絡みの勉強やお試しができる。
移行時に注意するべきこと
- DB環境
Herokuの環境はPostgreSQLで、CentOSはMySQLなので、DBの違いに注意。
CentOSでもPostgreSQLをインストールすることもできるが、さくらのVPSだと起動時のスクリプトでLAMPのインストールがある。
PHPのシステムなのでLAMPで楽したかったのでMySQLにした。
ただ、ローカルの開発環境はXAMPPとかMAMPを使ってたので、MySQLでもPostgreSQLでも適応できるSQLで開発してたので問題なし。
いざ実行
前置きはこれくらいにして実際の作業手順に付いて。
1. Herokuをメンテナンスモードに
まずは移行元となっているアプリケーションを使用できない状態にする必要がある。
Herokuにはメンテナンスモードというのが用意されており、コマンドでサクッと切り替えられたので、それを使用。
heroku maintenance:on --app アプリ名
これでメンテナンスモードになり、システムが使用不可になる。
2. DBのバックアップの取得
続いてはheroku postgresからDBのバックアップの取得をする。
まずはconfigの情報からデータベースの情報を取得。
heroku config:get DATABASE_URL --app アプリ名
結果、こんな感じのが返ってくる。
postgres://pxxsmjadcejqnv:8a68cb4f3e311442d688439ea25d780f7e580d0263da70afe65181f20c1a705b@ec2-174-129-255-15.compute-1.amazonaws.com:5432/dd2fk9n2pggjn7
これは
postgres://ユーザー名:パスワード@ホスト名:ポート/DB名
となっている。
それを踏まえてpg_dumpコマンドを使ってバックアップを取得する。
※ローカル環境にpostgresqlがインストールされている前提です。じゃないとpg_dumpコマンドが使えない。
Heroku上でもDBのバックアップは取得可能。
ただし、バイナリ形式になっている。
MySQLに移行するためにはinsert文でデータが欲しいので、pg_dumpを使って取得する。
pg_dump -d DB名 -h ホスト名 -U ユーザー名 --column-inserts -W > database.sql
--column-insertのオプションをつけないと、インサート文の形式ではなくなる。
insert文の形式にすると処理に時間がかかるらしいので、postgresqlに対しての移行ならオプションはつけない方が良いでしょう。
今回は移行先のDBが異なるのでinsert文にした。
3. SQL文を実行する
テーブルを作成するSQL文に関しては、開発時点で作成済みなので、移行先の環境にも既にスキーマやテーブル、インデックスなどは作成済み。
なのでインサート文のみ実行。
注意点としては、各テーブルの主キーはidで自動採番にしていたので、その点の考慮が必要。
PostgreSQLだとserial、MySQLだとauto_increment
ただ、最近はMySQLでもserialが使える(内部的にはauto_incrementとして処理しているだけらしいが)
ので、テーブル作成のcreate文はserialにしてればそのまま活用可能。
ただインサート後にauto_incrementの数値を更新するSQLを作成する必要がある。
postgreSQLのバックアップにserialのデータの更新があったので、その数値を参考にauto_incrementの更新を作成して実行。
ALTER TABLE xxxx AUTO_INCREMENT=100;
インサート文は特にエラーも起きず、問題点はなかった。
4. プログラムの移行
プログラムのソースコードはGitHubで管理していたので、移行で特に困ることはなかった。
さくらのサーバーにあらかじめGitがインストールされていたので、それを使ってClone及びプルする感じ。
DBの接続先をMySQLにして、サーバーのMySQLのパスワードに変更。
デフォルトだとrootユーザーしかなく、パスワードが設定されていないので、必要に応じてユーザーを作成してパスワードを設定して、接続URLを合わせてソースコードを修正。
また、作成したデータベースに対して接続するユーザーからのアクセス権限をつけないと、プログラムからアクセスできないので注意。
grant all on opencourt.* to root@'localhost';
プログラムでエラーが出た時には
/avr/log/php-fpm/www-error.log
の中にあるので、動作確認でエラーが出る場合はこちらを参照。
なんか、json_encodeの関数でエラーが出たりする現象があった。
jsonのライブラリがインストールされてなかったみたい。
https://zafiel.wingall.com/archives/10521
ここを参考にインストールした。
5.サブドメインとバーチャルホストの設定
サーバーを移行した一番の目的がこれ。
ドメインを取得して、サブドメインとバーチャルホストを使うことで、一つのドメインで複数のシステムを同時に公開することができる。
サブドメインの設定方法は下記のサイトを参考にした。
http://monopocket.jp/blog/it_others/2457/
送信ボタンを押すまで反映されないことに注意。
Apacheのバーチャルホスト用の設定ファイルが必要。
/etc/httpd/conf.d/
の中に、なんでもいいので拡張子が.confのファイルを作成する。
vhost.confとかvirtualhost.confとかにするのが一般的っぽい。
書き方は適当にネットで検索してそれっぽいのを探す。
書き方が間違っていなければ、apacheの再起動でうまくいく。
6. SSLの設定
Herokuの時にはhttpsにしてなかったので、移行ついでにSSLの設定も実施した。
最初は、一番安い有料のSSL証明書を購入して
https://qiita.com/yoshizaki_kkgk/items/e6f39a5bfb99900b44b2
ここを参考にやってみた。
※vhost.confの中に「Listen 443」の行があるが、これがあるとサーバー起動でエラーになるので注意。
また、認証ファイルの配置方法についてはメールの案内に従った方が確実。
手順通りでSSLにすることはできたが、どうやらサブドメインに対応してなかった。。。
よくよく調べてみると、サブドメインに対応するにはワイルドカード型という証明書である必要があるらしい。。。
ただ、ワイルドカード型は値段が高価。
ということで断念。
色々調べていると、無料で使えるSSL証明書で「Lets Encrypt」というのがあるらしい。
https://knowledge.sakura.ad.jp/10534/
それを使うことにした。
上記の情報は色々バージョンが古かったので、CentOS8で新しいバージョンに対応しているもの
https://www.server-world.info/query?os=CentOS_8&p=ssl&f=2
ここを参考にした。
最終的にはこんな感じにコマンドで、サブドメインに対してSSLを対応させることができた。
certbot-auto -w /var/www/html -w /var/www/html/aaaa -w /var/www/html/bbbb -d eventmanc.com -d aaaa.eventmanc.com -d bbbb.eventmanc.com
ただ、SSLの有効期限はデフォルトでは3ヶ月らしい。
有効期限が1ヶ月を切ったタイミングで
certbot-auto renew
コマンドを入力すると期限をリセットできるらしい。
しかし手動で打つのは面倒なので、cron使って毎日夜中に自動でコマンドが入力されるように更新する。
crontab -e # 中身 0 3 * * * root /usr/bin/certbot-auto renew
7. Heroku環境のプログラムの修正
システム移行後は移行前のシステムでデータを登録されても困るので、使えないようにする。
.htaccessを設定して、index.php(index.html)にしかアクセスされないようにする。
そのページでは、新しいシステムのURLの案内を書いておく。
内容に問題なければ、メンテナンスモードを解除する。
heroku maintenance:off --app アプリ名
まとめ
手順として1〜7まで書きましたが、実際には4, 5, 6のプログラムの移行、サブドメインとバーチャルホストの設定、SSLの設定は移行する前に事前に実施しておく。
そしてサーバー移行のタイミングで
- Herokuをメンテナンスモードにする
- データベースのバックアップをとってデータ移行
- 移行先環境で軽く動作確認
- 移行元のプログラムの修正と反映
- メンテナンスモードの解除
という感じです。