20210315のMySQLに関する記事は14件です。

Laravel+Guzzleを使用してAPIトークンを取得する方法

こんにちは、くりぱんです。

この記事で実現できること

  • Guzzleのインストール
  • Laravel+GuzzleでAPIトークンの取得

説明

今回はPHPのHTTPクライアントであるGuzzleを使用して、とあるAPIのトークンを取得していきたいと思います。

開発環境

  • Laravel: 6.2
  • Guzzule: 7.2

実装の流れ

  1. Laravelプロジェクトの作成
  2. Guzzleのインストール
  3. 日本時間にする
  4. データベース設定
  5. Route
  6. Controller
  7. View

実装

1. Laravelのプロジェクトの作成

$ composer create-project --prefer-dist laravel/laravel testAPI "6.*"

testAPIというLaravel6のプロジェクトが作成されます。

2. Guzzleのインストール

$ composer require guzzlehttp/guzzle

composer.jsonで確認していきます。

composer.json
{

    ーー省略ーー

    "license": "MIT",
    "require": {
        "php": "^7.2.5|^8.0",
        "fideloper/proxy": "^4.4",
        "guzzlehttp/guzzle": "^7.2", // ここチェック
        "laravel/framework": "^6.20",
        "laravel/tinker": "^2.5"
    },

    ーー省略ーー

}

私の場合composer.jsonには"guzzlehttp/guzzle": "^7.2"となっていますが、バージョンが多少異なっていても特に問題はありません。

3. 日本時間にする

APIトークンの有効期限も画面に出したいので、一応日本時間にしておきます。
config/app.phpを編集します。

app.php
return [

    ーー省略ーー

    'timezone' => 'Asia/Tokyo',

    ーー省略ーー

]

4. データベース設定

今回はMySQLを使用していきます。
mysql -u root -pなどでMySQLに入り、test_apiというデータベースを作成してください。

mysql> CREATE DATABASE test_api;

.envに作成したデータベースの設定をしていきます。

.env
// データベース関連のところを抜粋
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=test_api // 今回作成したデータベース名
DB_USERNAME=ユーザー名
DB_PASSWORD=パスワード

5. Route

routes/web.phpでルートの設定を行っていきます。

web.php
<?php

Route::get('/', 'TestApiController@index'); // ここを追記

http://127.0.0.1:8000/にアクセスするとこれから作成するTestApiControllerindexメソッドが走るようになりました。

6. Controller

$ php artisan make:controller TestApiController

このコマンドで、以下のようなapp/Http/Controllers/testApiController.phpが作成されます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestApiController extends Controller
{
    //
}

今回は例として下記の情報で接続したと過程します。

  • ベースURL: openapi.test.api.jp
  • HTTP リクエスト: POST openapi.test.api.jp/v2/test
  • API_KEY: hjfdksaJhjfkdw134fjdklsaKJ93JKL
  • user_id: user_id
  • password: password

先程コマンドで作成したapp/Http/Controllers/testApiController.phpを編集していきます。

TestApiController.php
<?php

namespace App\Http\Controllers;

use Facade\Ignition\QueryRecorder\Query;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Header;

class TestApiController extends Controller
{
    public function index()
    {
        // ベースURL
        $base_url = 'openapi.test.api.jp';

        // インスタンス作成
        $client = new Client([
            'base_url' => $base_url,
        ]);

        // API_KEY
        $api_key = 'hjfdksaJhjfkdw134fjdklsaKJ93JKL';

        // オプション
        $options = [
            // デバック(デバックしたい時は記述)
            'debug' => true,

            // 
            // パラメーター(Header)
            'headers' => [
                'api-key' => $api_key,
            ],

            // パラメーター(Query)
            'query' => [
                'user_id' => 'user_id',
            ],

            // パラメーター(Body)
            'json' => [
                'password' => 'password',
            ],
        ];

        // パス
        $path = '/v2/test';

        // リクエストするURL(openapi.test.api.jp/v2/test)
        $send_url = $base_url . $path;

        $response = $client->request('POST', $send_url, $options);

        // JSONデータとして取得
        $json = $response->getBody();

        // JSONデータを連想配列にする
        $api_token = json_decode($json, true);

        return view('welcome', compact('api_token'));
    }
}

passwordはTypeがBodyだったのですが、json指定だとうまい感じで変換してくれます。

7. View

JSONを連想配列で取得しているので、有効期限とAPIトークンをそれぞれ取得していきます。
resources/views/welcome.blade.phpを下記の通り編集してください。

welcome.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

    </head>
    <body>
        <p>有効期限:{{ date('Y年m月d日 H時i分s秒', $api_token['expires_at']) }}</p>
        <p>APIトークン:{{ $api_token['token'] }}</p>
    </body>
</html>

有効期限はミリ秒で取得されるので、date関数でフォーマットを整えています。
これで、php artisan serveしてからブラウザで確かめると、無事有効期限とAPIトークンを取得できるかと思います。

最後に

APIはまだまだわからないことばかりなので、ぜひご指摘等ありましたらコメント欄にお願いいたします。

少しでも役に立った!という時は、LGTMをポチッと、、、笑
1つでもLGTMが付くとその日がハッピーになるんです!
役に立たなかった時は、怒らないでコメント頂けると幸いです笑

Twitterもやってます!
プログラミングや金融知識についてやエンジニアの現実についてつぶやいています!
よかったら見てみてくださいね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ridgepoleでfulltext indexを貼る方法

MySQLの全文検索インデックス

MySQL5.6から、fulltext インデックスを貼ることができるようになった。(pluginによるparserの変更は5.7から?)

ridgepoleでfulltext indexを貼ると発生する問題

create_table :fuga do
  ...
  t.text :hoge
  ...
end

execute("CREATE FULLTEXT INDEX fk_hoge ON fuga(hoge) WITH PARSER ngram")

この書き方だと、冪等性がなく、
2回目以降に実行すると、一度、 remote_index :hoge が行われたあとに、再度、 executeCREATE FULLTEXT INDEX が呼ばれてしまう。create_table 句で name に対するindexを貼っていないための模様

この状態だと、マイグレーションを走らせるたびに、重いINDEXを貼る作業が毎回行われるので、回避するべき事象となる。

対処方法

create_table :fuga do
  ...
  t.text :hoge
  t.index :hoge, type: :fulltext, ignore: true # これを追加
  ...
end

execute("CREATE FULLTEXT INDEX fk_hoge ON fuga(hoge) WITH PARSER ngram") do |c|
  # INDEXが登録済みの場合は再実行しない
  rows = c.raw_connection.query("SHOW INDEX FROM fuga")
  rows.none? { _1[2] == 'fk_hoge' }
end

ignore を含んだ、TableDefinitionを追加し、 execute にINDEXの存在を確認するブロックを渡すことで、再実行してもOKになる。
超参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP/MySQLで作ったポートフォリオの振り返り

初めに

開発環境はmacOSで
PHPのバージョンは7.3を使用しています。
あくまで、アウトプットを目的として書いた記事なので、理解しにくい点、間違った内容があるかもしれません。ご容赦ください。

ポートフォリオについて

名前は『BookList』というアプリで、本のECサイトという設定で開発しました。
ECサイトは、CRUD機能だけでなく、より便利に理想的なものを作ろうとすると、
たくさんの機能が追加でき、すでに世の中にお手本となるフリマサイト、ECサイトがたくさんあるので自分もECサイトの開発に挑戦してみたいと思いました。

アプリの機能一覧

No 機能 機能について
1 新規登録機能
2 ログイン機能
3 ゲストログイン機能 新規登録しなくても、予め用意されているユーザー
4 パスワードのハッシュ化 この機能は記事にまとめました
5 商品の出品機能 商品の状態、種類、画像、タイトル、値段、著者などを登録
6 商品の編集 出品した、個数、値段、PR文、ステータスなどを編集
7 商品の削除 出品者は自分の商品を削除
8 コメント機能 商品の購入前に、出品者に対してコメントできる
9 コメント削除機能
10 商品の検索機能 商品の状態、種類で商品を検索できる
11 ページネーション 商品一覧ページ、商品別コメントページにそれぞれ
12 カートに追加 商品をカートに追加
13 カート削除 カートに追加した商品を削除
14 商品購入機能 商品を購入する

ECサイトで必須となる、出品->カートに追加->購入機能を中心に、本を扱うアプリなので、本の状態や、著者、コメント機能なども、追加していきました。

DB設計

booklist ER図.png

カラムはidしか表示させていませんが、DB設計を通じて、主キーや外部キーの繋がりを理解し、論理的に開発することができました。

セキュリティ、脆弱性

このポートフォリオは、フレームワークを使わずに、生のPHPとMysqlのみで作ったので、セキュリティ、脆弱性についてはしっかりと対策しました。

クロスサイトスクリプティング(XSS)

//xss対策
function h($str){
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

上記の関数を作り、画面上にecho printを使って、値を出力する時は必ずh(hogehoge)とエスケープ処理をしました。

SQLインジェクション

これに関しては、formからpostされてきた値をそのまま、保存するのではなく、
変数に格納し、PDOのprepareメソッドを利用して作成したステートメントに値をバインドさせ対策しました。

クロスサイトリクエストフォージェリ (CSRF)

主な脆弱性の一つのcsrfに対しては、post送信を行う全てのformに対して、ランダムな文字列を生成し、post送信の受け取って操作をする前に、そのトークンの照合行い、異なる文字列の場合には、ログインページに飛ばすように処理しました。
トークンの文字列が盗まれ、再利用されないように、トークンの破棄も忘れず、実装しています。

工夫した点

1.MVCを意識する

フレームワークを使っていなくてもmodelviewcontrollerとそれぞれのフォルダを作り、データの流れを意識しながら、開発を進めました。

2.高品質なコーディングを心がける

なるべくわかりやすい関数名、変数名を命名し、require,includeなどの関数を用いて、templeteファイルなどで共通化を取り入れました。さらにコメントを積極的に、コード内に書き込み可読性の高いコーディングに仕上げました。

参考 github

デプロイについて

conohaのvpsを使って、デプロイしました。
ssh接続し、本番環境にファイル、データベースの設置が完了したら、
セキュリティを高めるために、rootログインを禁止にし公開鍵認証の設置、filewallの設定でポート番号を変更しました。
そして、ドメインを購入して、conohaのIPアドレスに割り当てました。

BookList

課題

デザイン面

バックエンドエンジニアを目指しているので、最低限のデザインしかしていない

DBの発行のしすぎ

重複したDBの値などもあり、処理が増えてしまっている

まとめ

・フレームワークを使っていないので、多機能なアプリとは言えないが、あえてPHPのみでの開発でプログラミングの楽しさや、エラーが発生した時の自走力が身につきました。

・セキュリティに関する知識、対策、SQLの理解なども深めることができました。フレームワークを利用すると、あまり直接SQL文を書くことがないのでとても勉強になったし、汎用性のきく知識だと感じています。

最後まで、読んでいただきありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker,PHP,MySQLで構築した環境におけるDBホスト名の調べ方

以下の記事を参考に、Docker,PHP,MySQLの開発環境を構築した。

記事の手順にしたがって開発環境は構築できたのだが、mysqliでDBに接続する際に、ホスト名が分からない事態に遭遇。
ホスト名は、DBに接続して以下のコマンドを入力する事で確認できる。

mysql> show variables like 'hostname';

今回はDocker環境なので、DBコンテナに入ってから上記コマンドを入力する必要がある。
入力するコマンドは以下の通り。

// DBコンテナに接続
docker-compose exec db bash

// DBに接続
root@2918fd6815ab:/# mysql -u root -p
→ パスワードを入力

// ホスト名を表示
mysql> show variables like 'hostname';

以上のコマンドで、ホスト名を確認できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MAMPでEC-CUBE2系インストールに失敗した

MAMPで、EC-CUBE2系(2.13.5)をLocalにインストールした際、DB情報入力画面の次で処理が止まるエラーが発現。
何度もインストールしてたのに、解消まで手間取ったのでログを残す。

ログを確認すると、

[15-Mar-2021 16:53:16 Asia/Tokyo] PHP Fatal error: DB処理でエラーが発生しました。
SQL: [SET SESSION storage_engine = InnoDB]
MDB2 Error: unknown error
_doQuery: [Error message: Could not execute statement]
[Last executed query: PREPARE mdb2_statement_mysql_12a941c4181cfb45bc8b89a459f857c1ba510fac2 FROM 'SET SESSION storage_engine = InnoDB']
[Native code: 1193]
[Native message: Unknown system variable 'storage_engine']
in /Applications/MAMP/htdocs/eccube-2.13.5/data/class/SC_Query.php on line 1095

とのこと。storage engineのエラーとのことなので、MySQLそのものが壊れちゃったのかと疑った。
しかし、結論からいうと、下記の記事が解決法になりました。

前のMAMPではMySQLのバージョンが5.7よりも低かったのでしょうね。おそらく。
今回はEC-CUBEのソース側を変更することでインストール処理を進めました。
終わり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringBoot + MySQL でTodoリストを作る!CRUDの実装(Read編)

はじめに

今回は、「SpringBootでMySQLからデータを取得して、Thymeleafを用いて画面に表示させる」をゴールとしています。

SpringBootを学び始めた方、復習をしたい方に向けて学んだ事を共有します。

開発環境

OS: macOS Mojave(バージョン 10.14.6)
$ java -version
openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)
$ mvn -version
Maven home: /usr/local/Cellar/maven/3.6.3_1/libexec
Java version: 15.0.2, vendor: N/A, runtime: /usr/local/Cellar/openjdk/15.0.2/libexec/openjdk.jdk/Contents/Home
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.14.6", arch: "x86_64", family: "mac"
$ spring --version
Spring CLI v2.3.1.RELEASE
$ mysql --version
mysql  Ver 8.0.23 for osx10.14 on x86_64 (Homebrew)

プロジェクトの作成(pom.xmlの編集)

Spring initializr でサクッとプロジェクトを作成します。

依存関係は以下の通りです。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>todolist</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>todolist</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>

        <!-- テンプレートエンジン -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- RESTfulを含んだWebアプリケーションライブラリの利用 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- ファイルの変更を検知すると自動で再起動する -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- @Dataアノテーションを付与する事で,getter,setter等を自動生成してくれる -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- テスト用 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- DBとJavaオブジェクトやり取りを行う。 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

MySQLと接続

いきなりターミナルでmvn spring-boot:runを打ち込んで起動しようとしても出来ません。

terminal
$ mvn spring-boot:run

~以下略~

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

1.DBを作成、テストデータの作成

まずMySQLでデータベースを作成します。

データベース作成
CREATE DATABASE spring_todolist_qiita;
mysql
mysql> show databases;
+-----------------------+
| Database              |
+-----------------------+
| information_schema    |
| mysql                 |
| performance_schema    |
| spring_todolist_qiita | -- ←作成完了
| sys                   |
+-----------------------+

テーブルを作成してデータを追加します。

テーブル作成
mysql> use spring_todolist_qiita;

mysql> create table if not exists todolist (
  id integer not null auto_increment,
  content text not null,
  done boolean not null default 0,
  primary key (id)
);

データを何個か作っておきます。

データの追加
mysql> insert into todolist (content) values ("卵を買う");
--以下略--

2.SpringBootにMySQLの情報を定義する

application.propertiesにMySQLに接続するため記述します。

application.properties
# 接続先のDB名を設定
spring.datasource.url=jdbc:mysql://localhost/spring_todolist_qiita

# 接続しているDBの username と passwordを設定
# 〇〇と△△はご自身のusername,passwordを記載してください
spring.datasource.username=〇〇
spring.datasource.password=△△

# MySQL用JDBCドライバの設定
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver

再度ターミナルでmvn spring-boot:runを打ち込むと、起動はするようになります。

SpringBootとMySQL間のやり取りの実装

TodolistApplication.javaと同じ階層に各フォルダを作成して、それぞれ中身を実装していきます。

スクリーンショット 2021-03-13 10.53.56.png

1.エンティティモデルの作成

@Entityでエンティティクラスである事を宣言し、データベースのテーブルとマッピング。

@Table(name = "todolist")は、エンティティクラスとマッピングされるDBのテーブル名を指定する。

@Idは、主キーである事を、@GeneratedValue(strategy = GenerationType.IDENTITY)は、主キーが自動採番されるよう宣言。

Todo.java
package com.example.todolist.Model;

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

import lombok.Data;

// データを格納するクラス(DBへ登録、更新時の入れ物)
@Entity
@Data
@Table(name = "todolist")
public class Todo {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  private String content;

  private boolean done;
}

2.DBにアクセスするRepositoryを作成

データベースから値の取得、更新などのメソッドが定義されているJpaRepositoryインターフェースを継承したインターフェースを用意する。

@RepositoryでDBアクセスのクラスであると宣言、DIコンテナに登録される。

TodoRepository.java
package com.example.todolist.Repository;

import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.todolist.Model.Todo;

// DBにアクセスするためのインターフェース
@Repository
public interface TodoRepository extends JpaRepository<Todo, Integer> {
}

3.具体的な処理を行うServiceを作成

@Serviceでサービスクラスである事を宣言、DIコンテナに登録される。

@Autowiredで、DIコンテナに登録されているTodoRepositoryのインスタンスを生成して返してくれている。

TodoService.java
package com.example.todolist.Service;

import java.util.List;

import com.example.todolist.Model.Todo;
import com.example.todolist.Repository.TodoRepository;

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

// DBとの具体的な処理(データの取得、新規作成など)を記述するクラス
@Service
public class TodoService {

  @Autowired
  TodoRepository todoRepository;

  // todolistを全件取得
  public List<Todo> searchAll() {
    return todoRepository.findAll();
  }
}

取得したデータを画面に表示させる

Controllerを用意してDBからデータを取得し、Viewに送って画面に表示させます。

1.Controllerを作成

@Controllerでコントローラークラスである事を宣言。

@Autowiredで、DIコンテナに登録されているTodoServiceのインスタンスを生成して返してくれている。

TodoController.java
package com.example.todolist.Controller;

import java.util.List;

import com.example.todolist.Model.Todo;
import com.example.todolist.Service.TodoService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class TodoController {

  @Autowired
  TodoService todoService;

  @GetMapping("/")
  public String home(Model model) {
    List<Todo> allTodo = todoService.searchAll();
    model.addAttribute("allTodo", allTodo);
    return "home";
  }
}

@GetMapping("/")でlocalhost:8080にアクセスすると、homeメソッドが呼び出される。

todoServiceのserarchAllメソッドで、DBからデータを全件取得してmodel.addAttributeでViewにJavaオブジェクトを渡している。

2.HTMLの作成

上記Controllerでreturn "home"としているので、home.htmlを作成する。

home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>SpringTodoList</title>
</head>
<body>
  <div th:each="todo : ${allTodo}">
    <p th:text="${todo.content}"></p>
  </div>
</body>

</html>

Controllerから渡されたallTodoはList型なので、
th:each="todo : ${allTodo}"のループを回してtodoに一つずつデータを格納している。

todo.contentでcontentを描画している。

SpringBootの起動

MySQLのデータが画面に表示されているか確認します。

terminal
$ mvn spring-boot:run

localhost:8080にアクセスしてみると...

スクリーンショット 2021-03-13 11.27.00.png

無事表示されました。

終わりに

CSSの記述もしていないので味気ないページですが、SpringBootとMySQLを接続してデータを表示させる事が出来ました。

ご自身の環境でも試してみてください。

次は新規登録の実装をしてみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

<Golang+Nginx+MySQL>Macローカル上にDocker Composeでアプリを立ち上げてみた

Dockerアプリケーションのアーキテクチャー

スクリーンショット 2021-03-15 10.32.57.png

ざっくりとやること・できることの説明

  • docker-composeでNginxとGo言語のAPI, DBとしてMySQLのコンテナを3つ立ち上げる
  • Nginxのindex.htmlからGo言語で書いたREST API(go-ginのフレームワーク使用)を経由して、DBに問い合わせ
  • DBにCRUD(gormのフレームワーク使用)ができることを確認
  • ※DockerとDocker Composeはインストールされているものとする。

バージョン確認

確認してみたら、以下のバージョンでした。

Docker version 20.10.2
docker-compose version 1.27.4
golang:1.14.7-alpine3.12
mysql:5.7
nginx:1.17.3-alpine

※厳密にはバージョンの依存関係とかも考えないといけないが、一旦ローカルで動かすので、スルーしますm(__)m

フォルダ構成

docker-local
├── .env
├── api
│   └── src
│       ├── main.go
│       ├── model  ── model.go
│       └── config ── config.go
├── db
│   ├── conf ── my.cnf
│   └── db-data
├── docker
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.yml
└── web
    ├── env
    │   └── nginx ── site.conf
    └ src ── index.html

ディレクトリとファイルの用途解説

名前 種類 説明
docker-local フォルダ dockerローカルアプリケーションの親フォルダ
.env ファイル docker-composeで使う環境変数のファイル
api フォルダ APIのフォルダ
src フォルダ ソースフォルダ
main.go ファイル goを起動するときのファイル
model/model.go ファイル DBとやりとりする時のファイル
struct, dao系のメソッドを記載
config/config.go ファイル DBの接続情報等を記載
db フォルダ DBに関するフォルダ
conf/my.cnf ファイル 任意のDB設定を記載。マウント用
db-data フォルダ DBのデータをsyncするマウント用
docker フォルダ Dockerfileたちを格納するフォルダ
nginx フォルダ NginxのimageをビルドするDockerfileを格納
Dockerfile ファイル nginxのimage
nginx.conf ファイル nginxの設定ファイル
docker-compose.yml ファイル docker containerを一括でコントロールするyamlファイル
web フォルダ フロントのファイルを格納
env フォルダ 環境設定ファイルを格納
nginx/site.conf ファイル nginxのserver設定ファイル
IPアドレス、ポート、ルーティングなど設定
src/index.html ファイル 画面表示用ファイル

Docker Composeの中身

version: "3.8"

services:
  nginx:
    image: infra-challenge:nginx-20210312
    # command: 
    build: 
      context: ./docker/nginx
      dockerfile: Dockerfile
    container_name: nginx
    ports:
      - 80:80
    volumes:
      - ./web/env/nginx:/etc/nginx/conf.d
      - ./web/src:/var/www/html
    depends_on:
      - api
    logging:
      driver: "none"
    restart: always
    networks:
      - services

  api:
    image: golang:1.14.7-alpine3.12
    command: go run main.go --host 0.0.0.0 --port 8080
    container_name: api
    ports:
      - 8080:8080
    volumes:
      - ./api:/go/src/github.com/infra-challenge/api
      - /Users/username/go:/go
    working_dir: /go/src/github.com/infra-challenge/api/src
    environment:
      - APP_STAGE=local
      - LOG_LEVEL=debug
      - MYSQL_CONNECTION
    depends_on:
      - db
    restart: always
    networks:
      - services

  db:
    image: mysql:5.7
    # build: ./docker/mysql
    container_name: db
    ports:
      - 3306:3306
    volumes:
      - ./db/conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/db-data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD
      - MYSQL_DATABASE
      - MYSQL_USER
      - MYSQL_PASSWORD
    healthcheck:
      test: ["CMD-SHELL", "mysqlshow -u ${MYSQL_USER} -p${MYSQL_PASSWORD}"]
      interval: "5s"
      retries: 10
    logging:
      driver: "none"
    restart: always
    networks:
      - services

networks:
  services:
    external:
      name: local_infra_challenge_networks

主な属性の解説

  • docker composeの文法バージョンは3.8だよ
  • services配下に、コンテナの定義を書いていく。今回はnginx, api, dbの3つ
  • nginxDockerfileを使うから、image名は任意で、buildの属性を指定。
    • contextはDockerfileのある相対パス、dockerfileはDockerfile名
    • ※ファイル名がDockerfileとなっていたら、build: ./docker/nginxとしてもOK
  • container_nameを指定すると、docker stopとかするときに、idじゃなくて名前を指定すれば制御できるので便利
  • ports<ホスト(Macローカル)>:<ゲスト(Dockerコンテナ)>でポートを接続
  • volumes<ホスト(Macローカル)>:<ゲスト(Dockerコンテナ)>でマウントフォルダを指定
    • マウントとは、フォルダ同士を共有してsync(同期)すること。
  • depends_onは他のコンテナが起動成功でき次第〜みたいなやつ
  • apiとdbは、自分でDockerfileは用意せずに、Docker-Hub公式のimageを使っている
  • environmentに環境変数を指定する。
    • docker-compose.ymlと同階層に.envファイルを置き指定
  • DBではhealthcheckを行い、起動しているか随時確認
  • ネットワークを組むことで、コンテナ間通信が可能となり、APIからDBに問い合わせができるようになった

ネットワークについて

作ったネットワークの情報を確認してみると、下記のようなObjectが返ってきますね〜
"Subnet": "172.19.0.0/16",の中で、Containers(nginx, api, db)がIPアドレスを振られて存在しているのがわかります。
ちなみに、networksだけでなくlinksという属性もあり、linksを使うと、一方的にコンテナ間通信ができるようになる設定もあるので、TPOによって使い分ける◎
公式docker:links

$ docker network inspect local_infra_challenge_networks
[
    {
        "Name": "local_infra_challenge_networks",
        "Id": "f6a978e6f3aa803be061418c1c7081d132c9538c207ea08b1d732fc55ba2b917",
        "Created": "2021-03-12T03:57:20.0697997Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.19.0.0/16",
                    "Gateway": "172.19.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "7a81d8c6f965a07ce446aa0983180bbb4775187ed6873a9e26dbc9540f623392": {
                "Name": "nginx",
                "EndpointID": "09336c1acc58f9b75ea3ced604a7d82b168a5f0cd132ae4f2fffd267a416b022",
                "MacAddress": "02:42:ac:13:00:04",
                "IPv4Address": "172.19.0.4/16", #<-これ!!
                "IPv6Address": ""
            },
            "842244244bf00593e25317dd1cafb35ae03cfbc5246a87c666c6434a6ad7540d": {
                "Name": "db",
                "EndpointID": "3810c5b1b5f87b3ba4331320fcb2106696f3dcd157a771f9de63aeffc3a14720",
                "MacAddress": "02:42:ac:13:00:02",
                "IPv4Address": "172.19.0.2/16", #<-これ!!
                "IPv6Address": ""
            },
            "c0906b3fc86d92f4b89474b7647e85a36e489469f83905c94785f2f278807532": {
                "Name": "api",
                "EndpointID": "e291176e7bd08040c2486d0409dab009a724fb7e3f0da3a5a9585cb23344a999",
                "MacAddress": "02:42:ac:13:00:03",
                "IPv4Address": "172.19.0.3/16", #<-これ!!
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

コンテナ間通信をするときは、コンテナ名でエンドポイントを指定することができるから便利

# db:3306 の部分 <コンテナ名:ポート>
MYSQL_CONNECTION=local:local@tcp(db:3306)/infra-challenge 

マウントについて

nginx

    volumes:
      - ./web/env/nginx:/etc/nginx/conf.d
      - ./web/src:/var/www/html
  • nginxにはdefault.confというものがあり、それを自分の設定に書き換えたいので、./web/env/nginx で書き換えてる
  • /var/www/html以下がdockerコンテナ内のnginxフォルダ構成になる(する)ので、index.htmlがあるディレクトリを同期

api

    volumes:
      - ./api:/go/src/github.com/infra-challenge/api
      - /Users/username/go:/go
  • とりあえずGo Pathが通っているところをdockerコンテナ側にマウントしてる。
  • APIのファイル等も同期

db

    volumes:
      - ./db/conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/db-data:/var/lib/mysql
  • MySQLのデータの格納先が/var/lib/mysqlになるので、ホストの./db/db-dataに同期して、コンテナを除去してもデータが消えないようにする
  • 自分のDB設定、例えば、character-setとかcollationとかをマウントして上書き
# my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[mysql]
default-character-set=utf8mb4

いざ、Docker Composeで起動してみる!!

コマンド

$ cd /Users/username/src/github.com/infra-challege/docker-local
$ docker network create --driver bridge local_infra_challenge_networks
[~省略~]
$ docker-compose build --no-cache
[~省略~]
$ docker-compose up -d --remove-orphans
Creating db ... done
Creating api ... done
Creating nginx ... done

$ docker ps -a
CONTAINER ID   IMAGE                                 COMMAND                  CREATED         STATUS                            PORTS                               NAMES
8fxxxb102ed4   infra-challenge:nginx-20210312        "nginx -g 'daemon of…"   4 seconds ago   Up 3 seconds                      0.0.0.0:80->80/tcp                  nginx
8fxxx25640e6   golang:1.14.7-alpine3.12              "go run main.go --ho…"   6 seconds ago   Up 4 seconds                      0.0.0.0:8080->8080/tcp              api
e5xxxe97320e   mysql:5.7                             "docker-entrypoint.s…"   6 seconds ago   Up 5 seconds (health: starting)   0.0.0.0:3306->3306/tcp, 33060/tcp   db

  • docker-composeファイルがある階層に移動
  • networks属性を指定したので、そのネットワークを作成する。
  • nginxはDockerfileをつかってimageを作成するので、ビルドする必要がある。
    • --no-cacheオプション:構築時にイメージのキャッシュを使わない。
  • docker-compose upで起動
    • -dオプション:バックグラウンド実行
    • --remove-orphansオプション:Composeファイルで定義されていないサービス用のコンテナを削除

Webページにアクセスできるか確認

(超しょぼいですが...w)
アクセスできた!
スクリーンショット 2021-03-15 11.54.07.png

user1を作成してみよう。
スクリーンショット 2021-03-15 11.57.28.png
作成できたことを確認◎
スクリーンショット 2021-03-15 11.59.15.png

コンテナ終了コマンド

$ docker-compose down
Stopping nginx ... done
Stopping api   ... done
Stopping db    ... done
Removing nginx ... done
Removing api   ... done
Removing db    ... done

止まりました。

ハマったポイント

  • go run main.goした後に、起動はしていたがポート設定がミスっていて、APIにアクセスできなかった。
    • 「なんかうまくいかないな〜〜汗」ってなったので、VSCodeから一旦APIサーバーを止めて再起動して、通信がうまくいったけど、それはコンテナ間通信ではなかった、、、
      • コンテナ上ではなくローカルでAPIサーバーが立ち上がってしまい、通信できていると勘違いしてしまった。
  • index.htmlの画面表示するポートとAPIを司るポートそれぞれが別だったので、その調整にハマった。
  • nginxには、サーバー設定が必要だった。
    • nginx.confdefault.confというファイルが存在しこれのIPアドレス、ポート等を設定しないといけないので、前もってnginxの知識をいれておかないと簡単には動かなかった
# site.conf (default.confの上書き)
server {
    listen      80;
    listen      8080; # APIで8080ポートも使ってる関係で、これも必要だった
    server_name localhost;
    root        /var/www/html;

    location / {
        index  index.html index.htm;
        root   /var/www/html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /var/www/html/error;
    }
}

まとめ

docker-compose.ymlでコンテナを起動して、Dockerアプリケーションを作る感覚がつかめたので、楽しかったですわい!

コンテナのコントロールってすごい簡単で、サクサク動くから、せっかちな自分でも全然苦じゃない。
マスターしていきたいなああああ

以上、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】Express.jsからMySQLのデータ加工、ejsへの受け渡し

目的

現在、MySQLの店舗データを編集できるアプリケーションを作成しています。Node.jsのexpress.jsを使用し、viewにはejsを利用しています。この記事では、初学者の私がつまずいた、パラメータの受け渡しについて記述します。

ずばり「企業一覧画面から、企業に属した店舗の一覧を表示させる」処理についてです。

一連の処理は、こんな感じです

企業一覧画面(ejsで表示)
→ 店舗名のリンクをクリック
→ express.js(app.js)でデータを加工
→ 加工したデータをejsに渡す
→ ejsで表示させる

参照

Express.js(node.js)からMySQLへの接続とCRUD操作
[Node.js][Express]リクエストからパラメータを取得する・POSTされたデータを取得する
for 文と push メソッドを使って配列要素を複数生成

環境と周辺構造

・local (mac Big Sur)
・AWS MySQL

基本となるデータベースとの接続やCRUD処理の書き方等については、リンク先の記事で作成しました。そちらを参考にしてください!
Express.js(node.js)からMySQLへの接続とCRUD操作

テーブル構造

テーブルの相関関係はこのような形です。
account_masterが企業、shop_masterが企業が運営する店舗を示しています。
スクリーンショット 2021-03-13 16.43.05.png
テーブルの関係性を簡単に表すと、
account_master.account_id = shop_master.shop_account_idです。
企業側のaccount_idは店舗側のshop_account_idと同じことを意味しています。

画面イメージ

少し雑ですが、画面のイメージは以下の通りです。
・企業 一覧画面(account_master_index.ejs)
スクリーンショット 2021-03-13 16.55.27.png
・店舗一覧画面(shop_index)
スクリーンショット 2021-03-13 16.57.24.png
企業名リンクをクリックすると、店舗一覧画面に遷移します。

ディレクトリ構造

※現在、未完成のため一部のみ記載

account_app/
├── node_modules
├── views
│   ├── account_master_index.ejs
│   ├── edit.ejs
│   └── shop_index.ejs
├── .env
├── .gitignore
├── app.js
├── package.json
└── README.md

前提

参考サイトでは、簡易なテーブルのデータで作成されていたため、企業の一覧や、店舗の一覧は簡単に表示まで出来ました。しかし、企業に基づく店舗など関係性がちょっと複雑な場合、どう表示させればよいかが全く分かりませんでした。

先に企業一覧について、次に店舗一覧について記述します。

企業一覧画面

app.js

企業一覧画面は参考サイトを元に、テーブル名、select文を変えるだけでOKでした。※記述外の設定は参考サイトを参照ください。

//app.jsの企業一覧に関する部分
app.get('/', (req, res) => {
    const sql = "select * from account_master where is_deleted = 0";
    con.query(sql, function (err, result, fields) {
    if (err) throw err;
    res.render('account_master_index',{account_master : result});
    });
});

app.getは express.js によるアプリケーションのルート( 今回は、localhost:3000 )へのGETメソッドに対応します。con.queryでデータベースから取得します。取得するデータはconst sqlで表記した select文(解約していない企業の情報すべて)です。

例外処理を記述した後、res.renderの第一引数にデータを表示させたいテンプレートを設定し、第二引数に .ejs に渡す名前を指定します。この命名により、ejs側ではresultではなくaccount_masterを使って取得したデータを利用できます。

account_master_indx.ejs

一部抜粋した記述です。

//account_master_indx.ejsの中身
    <% account_master.forEach(function (value) { %>
    <tr>
      <td><%= account_id %></td>
  ★   <td><a href="/shop_index/<%= value.account_id %>"><%= value.account_name %></a></td>
      <td><%= value.account_email.split('*') %></td>
      <td><%= value.mail_requested %></td>
      <td><%= value.is_deleted %></td>
      <td><%= value.updated_at %></td>
      <td><%= value.account_form_url %></td>
      <td><a href="/edit/<%= value.ue_account_id %>">up</a></td>
    </tr>
    <% }); %>

参考サイトはforEachを使っていたため、そのまま使っています。<% %>や、<%= %>で処理を書くのか、 HTML として表示させるかを書きます。

★ここで4行目に注目
<a href="/shop_index/<%= value.account_id %>"><%= value.account_name %></a>で企業 id のパラメータをURLに指定しています。後にこれと同じURLにする処理を app.js 内に if 文で記述します。

ここからが今回の記事の本題です

ポイントとしては以下のとおりです。
・企業に基づく店舗一覧をいきなり呼び出すのではなく、まず店舗の全データを出す
・配列を新たに作成する
・for文、if文で条件指定する
・加工した店舗一覧のデータをejs側で呼び出す

app.jsでデータを加工する

失敗した考え方
店舗一覧画面を出そうした際に、まず企業一覧画面と企業の編集画面を参考にしました。というのも、編集画面は企業のaccount_idを自然に渡せていたからです。そのため、店舗一覧画面も企業一覧と編集画面のようにaccount_idを受け渡せないかと考えました。

そもそも、この↑部分の認識が間違っており、正しくはexpress.js側で処理したデータをejsで表示させる。つまり、ejsからgetメソッドで表示させる場合はejsかた値を受け渡すなどは行わないです。

成功した考え方
いきなり企業に紐づく店舗一覧を、expressで記述しそれをejs側で表示させるのではなく、まずapp.js内で先に店舗の全データを出します。そのデータを使って、必要な情報だけを載せた配列を新たに作成します。作成した配列をejs側で呼び出し、表示させます。

店舗一覧画面

店舗一覧画面に関する記述は以下のとおりです。

app.js

//上部に宣言を記載
var shopDat;

//app.jsの上部で、店舗の全データを取得
con.query('select * from shop_master where is_deleted = 0;', function (error, results, fields) {
  if (error) throw error;
  shopDat = results; //shopDatに代入
});

~~(中略)~~

//企業ごとの店舗一覧を取得しshop_index.ejsで表示させるためのデータ加工
app.get('/shop_index/:shop_account_id', (req, res) => {
    let shops = []; //新たな配列を作成
    for(let i = 0; i < shopDat.length; i++){ //※shopDat.lendgthは要変更
      if (req.params.shop_account_id == shopDat[i].shop_account_id) { //URLパラメータの値を条件付け
        var target_shop = { //配列に入れるオブジェクトデータを定義
            "shop_id": shopDat[i].shop_id,
            "shop_name_jp": shopDat[i].shop_name_jp,
            "shop_name_en": shopDat[i].shop_name_en,
            "shop_account_id": shopDat[i].shop_account_id,
            "is_deleted": shopDat[i].is_deleted,
            "updated_at": shopDat[i].updated_at
        };
            shops.push(target_shop) //空の配列shopsに.pushで追加
        }
    };
    res.render('shop_index',{shop_data :shops}); //for文作成した配列をshop_dataと命名
});

先に shopDat を宣言し、そこにMySQLから店舗の全データを代入します。店舗一覧画面を表示させる箇所に、 shops という新たな空の配列を作成します。更にi番目の shopDat のデータを保持させる target_shop というオブジェクトを作成します。shops.push(target_shop)を shops という空の配列に、 for 文で作成されたオブジェクトを追加して、加工データを作成します。

この時req.params.shop_account_id == shopDat[i].shop_account_idではURLに渡すパラメータと企業に紐づく店舗の情報を一致させるために記述しています。※shop_account_idは企業のアカウント id と同じです。

req.params はリクエストされたパスからパラメータを取得するに使う文字列です。
[Node.js][Express]リクエストからパラメータを取得する・POSTされたデータを取得する

shop_index.ejs

//店舗一覧画面に関係する部分
    <% for (let i = 0; i < shop_data.length; i++) { %>
      <tr>
        <td><%= shop_data[i].shop_id %></td>
        <td><%= shop_data[i].shop_name_jp %></td>
        <td><%= shop_data[i].shop_name_en %></td>
        <td><%= shop_data[i].shop_account_id %></td>
        <td><%= shop_data[i].updated_at %></td>
        <td><a href="/edit/<%= shop_data[i].shop_id %>">更新する</a></td>
      </tr>
      <% }; %>

企業一覧画面ではforEachを使いましたが、こちらは for 文で表記しました。これで、企業に属する店舗の一覧を表示させることができました。

小技とつまずいたポイント

・出力したいデータが正しいかどうかをHTML(.ejs)でみたい
<%- JSON.stringify(shop_data) %>と記述することで見られます!

・JavaScriptのデータは配列[ ]の中にオブジェクト{ }をもつことができる。
→ MySQLに格納されているデータはオブジェクトだったので、連想配列かと思ってしまいましたが、JavaScriptは [{ name:aaa, email:xxx@yyyy}, { name:bbb, email:yyy@xxxx}, { }....]とできるようです。(要勉強)

・forとif文をに記述するのではなく、一つづつ書くこと
→ 処理を一気に書こうとして、ほしいデータをなかなか出すことが出来ませんでした。落ち着いて出力されたデータを見ながら、一つづつ解決するほうが結果早いですね。

・app.jsにて、if文のshopDatに[ i ]をもたせること
→ iをつけることに、なかなか気がつけませんでした。

まとめ

JavaScriptを勉強しはじめて3週間ほどですが、MySQLのデータを加工を実施しました!空の配列を作って、ほしいデータを作成をすることは初めての作業でしたが、なんとかうまく出来たので良かったです。途中で、配列なのか連想配列なのか迷ったりしたため中々答えにたどり着けませんでした。これが初学者の方のためになればと思います。

また、一部未完成・不十分な記述がありますので、ご教示いただけると幸いです!

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPのPDOでMySQLに接続する

環境

・macOS BigSur (11.2.2)
・PHP (7.4.12)
・MAMP (6.3)
・MySQL (5.7.32)

前提条件

・MAMPのphpMyAdminでデータベース・テーブルを作成してあること
・データベースに接続するためのユーザーを作成してあること

PDOとは

PHPからMySQLに接続するために必要なオブジェクトのこと
※PDOクラスのインスタンスを生成することにより、データベースサーバーとの接続が確率される

DB接続手順

//事前準備
$dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
$user = 'ユーザー名';
$pass = 'パスワード';

変数に対象の「データベース情報」「DB接続するユーザー情報」を設定

//DB接続
$dbh = new PDO($dsn,$user,$pass);

設定した変数を引数に入れて、PDOのオブジェクト生成

データを取得する

指定した列名のデータを全て取り出す例

※上記で生成したPDOオブジェクトを使用する

//SELECT文
$select_sql = 'SELECT * FROM テーブル名';
$stmt = $dbh->query($select_sql);
foreach($stmt as $value){
  echo $value['カラム名'];
}

データを登録する

入力フォームなどで入力された値を、プリペアドステートメントを使用してDBに登録する例

※プリペアドステートメントとは、SQL文を最初に用意しておいて、その後はクエリ内のパラメータの値だけを変更してクエリを実行できる機能のこと

//INSERT文
$insert_sql = 'INSERT INTO テーブル名(カラム名) VALUES(:name)';
$stmt = $dbh->prepare($insert_sql);
$params = array(':name'=>$_POST['フォームに入力された値']);
$stmt->execute($params);

カラム名にデータ登録したいカラム名を指定する
VALUESを「:name」として抽象化し、後に$_POSTで値を設定する(※nameの部分は任意の文字で良い)
prepareメソッドでSQL文を実行する準備をする
executeメソッドでプリペアドステーメントを実行する

※正式な値が入るまで一時的に場所を確保しておくために入れておく値のことをプレースホルダという
(今回だと「:name」の部分がプレースホルダ)
※プレースホルダに値を割り当てることをバインドという

データを削除する

指定された番号のIDを持つDBの値を削除する例

//DELETE文
$delete_sql = 'DELETE FROM テーブル名 WHERE idが入ってるカラム名 = :id';
$stmt = $dbh->prepare($delete_sql);
$params = array('id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

データを変更する

指定された番号のIDを持つデータを対象に、指定されたカラム名の値を更新する例

//UPDATE文
$update_sql = 'UPDATE テーブル名 SET 更新するデータのカラム名 = :name WHERE 更新対象とするidが入ってるカラム名 = :id';
$stmt = $dbh->prepare($update_sql);
$params = array('name'=>$_POST['更新するデータ'],'id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

try~catchを使ったエラーハンドリング(例外処理)

DB接続の成功/失敗を出力する例

try{
  $dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
 $user = 'ユーザー名';
  $pass = 'パスワード';
  $dbh = new PDO($dsn,$user,$pass);
  echo 'DB接続成功'
}catch(PDOException $e) {
  echo 'DB接続エラー';
}

(例外処理についての補足)
プログラム実行中に発生したエラーのことを例外という
例外が発生すると、プログラムが異常終了する
システム的に異常終了はさせたくないから、例外を検知して、例外が発生した場合に行う処理を用意する
この一連の処理のことを例外処理という
結果、エラーは発生しているが、プログラム自体は正常に動作したという状況を作ることができる
DBの接続が失敗したとき、プログラムが異常終了する
システム的に異常終了はさせたくないから、 DBの接続処理に対してエラーハンドリングする必要がある

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP×MAMPでDB接続を行う

環境

・macOS BigSur (11.2.2)
・PHP (7.4.12)
・MAMP (6.3)
・MySQL (5.7.32)

前提条件

・MAMPのphpMyAdminでデータベース・テーブルを作成してあること
・データベースに接続するためのユーザーを作成してあること

PDOでDB接続

※PDOとは、MAMPでPHPからMySQLに接続するために必要なオブジェクトのこと

//事前準備
$dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
$user = 'ユーザー名';
$pass = 'パスワード';

変数に対象の「データベース情報」「DB接続するユーザー情報」を設定

//DB接続
$dbh = new PDO($dsn,$user,$pass);

設定した変数を引数に入れて、PDOのオブジェクト生成

データを取得する

生成したオブジェクトを使用してDBからデータを取り出す

※指定した列名のデータを全て取り出す例

//SELECT文
$select_sql = 'SELECT * FROM テーブル名';
$stmt = $dbh->query($select_sql);
foreach($stmt as $value){
  echo $value['列名'];
}

データを登録する

入力フォームなどで入力された値を、プリペアドステートメントを使用してDBに登録する例

※プリペアドステートメントとは、SQL文の値をいつでも変更できるように、変更する部分のみ変数のようにした命令文を作る仕組みのこと

//INSERT文
$insert_sql = 'INSERT INTO テーブル名(列名A) VALUES(:name)';
$stmt = $dbh->prepare($insert_sql);
$params = array(':name'=>$_POST['フォームに入力された値']);
$stmt->execute($params);

列名Aにデータ登録したい列名を指定する
VALUESを「:name」として抽象化し、後に$_POSTで値を設定する(※nameの部分は任意の文字で良い)
prepareメソッドでSQL文を実行する準備をする
executeメソッドでプリペアドステーメントを実行する

※「:name」と変化する値を確保することをプレースホルダという
※「:name」の部分に値を割り当てることをバインドという

データを削除する

指定された番号のIDを持つDBの値を削除する例

//DELETE文
$delete_sql = 'DELETE FROM テーブル名 WHERE idが入ってる列名 = :id';
$stmt = $dbh->prepare($delete_sql);
$params = array('id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

データを変更する

指定された番号のIDを持つデータを対象に、指定された列名の値を更新する

//UPDATE文
$update_sql = 'UPDATE テーブル名 SET 更新するデータの列名 = :name WHERE 更新対象とするidが入ってる列名 = :id';
$stmt = $dbh->prepare($update_sql);
$params = array('name'=>$_POST['更新するデータ'],'id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

try~catchを使ったエラーハンドリング(例外処理)

DB接続の成功/失敗を出力する例

※例外処理とは、エラーが発生した際の処理を設定しておくこと
(try~catchの間でエラーが発生した時、catchの中の処理が行われる)

try{
  $dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
 $user = 'ユーザー名';
  $pass = 'パスワード';
  $dbh = new PDO($dsn,$user,$pass);
  echo 'DB接続成功'
}catch(PDOException $e) {
  echo 'DB接続エラー';
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP×MAMPでDB操作する

環境

・macOS BigSur (11.2.2)
・PHP (7.4.12)
・MAMP (6.3)
・MySQL (5.7.32)

前提条件

・MAMPのphpMyAdminでデータベース・テーブルを作成してあること
・データベースに接続するためのユーザーを作成してあること

PDOでDB接続

※PDOとは、MAMPでPHPからMySQLに接続するために必要なオブジェクトのこと

//事前準備
$dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
$user = 'ユーザー名';
$pass = 'パスワード';

変数に対象の「データベース情報」「DB接続するユーザー情報」を設定

//DB接続
$dbh = new PDO($dsn,$user,$pass);

設定した変数を引数に入れて、PDOのオブジェクト生成

データを取得する

指定した列名のデータを全て取り出す例
※上記で生成したオブジェクトを使用する

//SELECT文
$select_sql = 'SELECT * FROM テーブル名';
$stmt = $dbh->query($select_sql);
foreach($stmt as $value){
  echo $value['列名'];
}

データを登録する

入力フォームなどで入力された値を、プリペアドステートメントを使用してDBに登録する例

※プリペアドステートメントとは、SQL文の値をいつでも変更できるように、変更する部分のみ変数のようにした命令文を作る仕組みのこと

//INSERT文
$insert_sql = 'INSERT INTO テーブル名(列名A) VALUES(:name)';
$stmt = $dbh->prepare($insert_sql);
$params = array(':name'=>$_POST['フォームに入力された値']);
$stmt->execute($params);

列名Aにデータ登録したい列名を指定する
VALUESを「:name」として抽象化し、後に$_POSTで値を設定する(※nameの部分は任意の文字で良い)
prepareメソッドでSQL文を実行する準備をする
executeメソッドでプリペアドステーメントを実行する

※「:name」と変化する値を確保することをプレースホルダという
※「:name」の部分に値を割り当てることをバインドという

データを削除する

指定された番号のIDを持つDBの値を削除する例

//DELETE文
$delete_sql = 'DELETE FROM テーブル名 WHERE idが入ってる列名 = :id';
$stmt = $dbh->prepare($delete_sql);
$params = array('id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

データを変更する

指定された番号のIDを持つデータを対象に、指定された列名の値を更新する

//UPDATE文
$update_sql = 'UPDATE テーブル名 SET 更新するデータの列名 = :name WHERE 更新対象とするidが入ってる列名 = :id';
$stmt = $dbh->prepare($update_sql);
$params = array('name'=>$_POST['更新するデータ'],'id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

try~catchを使ったエラーハンドリング(例外処理)

DB接続の成功/失敗を出力する例

※例外処理とは、エラーが発生した際の処理を設定しておくこと
(try~catchの間でエラーが発生した時、catchの中の処理が行われる)

try{
  $dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
 $user = 'ユーザー名';
  $pass = 'パスワード';
  $dbh = new PDO($dsn,$user,$pass);
  echo 'DB接続成功'
}catch(PDOException $e) {
  echo 'DB接続エラー';
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel8 ReactHooks & MySQL Create処理

やりたいこと

フロントはReactHooksを使用して、LaravelはAPIサーバーでバックエンド処理をおこなう。
バックエンド処理はaxiosを使用。

完成図

e147bdcac8ccf1b01931c53e89227bdd.png

開発環境

  • Laravel 8.0
  • php 7.3
  • MySQL 8

State

  • const [ name, setName ] = useState(null);

入力値name(名前)のState

  • const [ email, setEmail ] = useState(null);

入力値email(メールアドレス)のState

  • const [ city, setCity ] = useState(null);

入力値city(住所)のState

  • const [ address, setAddress ] = useState(null);

入力値address(市区町村)のState

  • const [ phone, setPhone ] = useState(null);

入力値phone(電話番号)のState

入力フォーム

map処理はkeyを明示する必要があるので注意

keyの明示についてはVirtualDOMのdiffから実際のDOMに反映させるときに最小限の変更にするために使われる。

form
import React, { useEffect, useState  } from 'react';

import employeeServices from "../services/Employee"

function Form(){

  const [ name, setName ] = useState(null);
  const [ email, setEmail ] = useState(null);
  const [ city, setCity ] = useState(null);
  const [ address, setAddress ] = useState(null);
  const [ phone, setPhone ] = useState(null);
  const [ rol, setRol ] = useState(null);
  const [ listRol, setListRol ] = useState([]);

  useEffect(() => {
    async function fetchDataRol() {
      // load data from API
      const res = await employeeServices.list();
      setListRol(res.data)
    }
    fetchDataRol();
  },[]);

  const saveEmployee = async () => {

    const data = {
      name, email, city, address, phone, rol
    }
    const res = await employeeServices.save(data);

    if (res.success) {
      alert(res.message)
    }
    else {
      alert(res.message)
    }
  }

  return(
    <div>
      <div className="row">
        <div className="col-md-6 mb-3">
          <label htmlFor="firstName">Name employee </label>
          <input type="text" className="form-control" placeholder="Name"
            onChange={(event)=>setName(event.target.value)} />
        </div>
      </div>

      <div className="row">
        <div className="col-md-6 mb-3">
          <label htmlFor="email">Email</label>
          <input type="email" className="form-control" placeholder="you@example.com"
            onChange={(event)=>setEmail(event.target.value)} />
        </div>
      </div>

      <div className="row">
        <div className="col-md-6 mb-3">
          <label htmlFor="phone">City </label>
          <select id="inputState" className="form-control" onChange={(event)=> setCity(event.target.value)}>
             <option selected>Choose...</option>
             <option value="London">London</option>
             <option value="Madrid">Madrid</option>
             <option value="New York">New York</option>
          </select>
        </div>
      </div>

      <div className="row">
        <div className="col-md-6 mb-3">
          <label htmlFor="address">Address</label>
          <input type="text" className="form-control" placeholder="1234 Main St"
            onChange={(event)=>setAddress(event.target.value)} />
        </div>
      </div>

      <div className="row">
        <div className="col-md-6 mb-3">
          <label htmlFor="phone">Phone </label>
          <input type="text" className="form-control" placeholder="123467890"
              onChange={(event)=>setPhone(event.target.value)}  />
        </div>
      </div>

      <div className="row">
        <div className="col-md-6 mb-3">
                    <label htmlFor="phone">Rol </label>
                    <select id="inputState" className="form-control" onChange={(event)=> setRol(event.target.value)}>
             <option selected>Choose...</option>
             {
               listRol.map((item)=>{
                 return(
                   <option key={item.rol_id} value={item.rol_id}>{item.rol_name}</option>
                 )
               })
             }
          </select>
        </div>
      </div>

      <div className="row">
        <div className="col-md-6 mb-3">
          <button className="btn btn-primary btn-block" type="submit"
          onClick={()=>saveEmployee()}>Save</button>
        </div>
      </div>
    </div>
  )
}

export default Form;

saveEmployee押化後、axiosのemployee.saveが呼び出されてlocalhost:8000/api/employee/createにPOSTする。

axios通信

axios
const baseUrl = "http://localhost:8000/api/employee"
import axios from "axios";
const employee = {};

employee.save = async (data) => {
  const urlSave= baseUrl+"/create"
  const res = await axios.post(urlSave,data)
  .then(response=> {return response.data })
  .catch(error=>{ return error; })
  return res;
}

export default employee
api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('/employee/create', 'App\Http\Controllers\API\EmployeeController@create');

Laravel側のControllerが呼び出され、実際に処理をするcreateアクションを呼び出します。

保存処理の最後にはresponseを返してあげる。今回は動作確認のため、失敗時にエラーメッセージのみを返しています。

コントローラー

EmployeeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Employee;

class EmployeeController extends Controller
{
    public function index(){
        return view("employee");
      }

      public function create(Request $request){

        try {

          $insert['name_lastname'] = $request['name'];
          $insert['email'] = $request['email'];
          $insert['city'] = $request['city'];
          $insert['direction'] = $request['address'];
          $insert['phone'] = $request['phone'];

          Employee::insert($insert);

          $response['message'] = "Save succesful";
          $response['succes'] = true;

        } catch (\Exception $e) {
          $response['message'] = $e->getMessage();
          $response['error'] = true;
        }

        return $response;
      }
}

createアクションはこのような流れで実装できます

指摘などがございましたらコメントで教えていただけると幸いです^_^

おわり

次はレコードをmapで繰り返し表示する方法など記事にしていこうと思います〜!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel8+ReactHooks+MySQL Create処理

やりたいこと

フロントはReactHooksを使用して、LaravelはAPIサーバーでバックエンド処理をおこなう。
バックエンド処理はaxiosを使用。

開発環境

  • Laravel 8.0
  • php 7.3
  • MySQL 8

ModelはItem カラムはtext/commentと仮定しています。

State

  • const [ text, setText ] = useState("");

入力値textのState

  • const [ comment, setComment] = useState("");

入力値commentのState

入力フォーム

入力値をSetStatで更新。submitでsaveEmployeeを呼び出す。

form
return (
      <form className="add-form">
        <div className="add-text">
          <label>内容</label>
          <input type="text" value={text} onChange={(event)=>setText(event.target.value)}/>
        </div>
        <div className="add-comment">
          <label>コメント</label>
          <input type="number" value={amount} onChange={(event)=>setComment(event.target.value)}}/>
        </div>

        <div className="form-row">
        <color="primary" aria-label="add"
          type="submit"
          onClick={()=>saveEmployee()}>追加</div>
    </form> 
    )

呼び出されたsaveEmployeeはdataに定義された値を保持してemployeeServices.saveにアクセス。
この際、dataに定義されているtextとcommentはuseStateを参照しています。

axios
const saveEmployee = async () => {

const data = {
  text, 
  comment, 
};

  const res = await employeeServices.save(data);
  setItemlist(res.data)

}

saveEmployeeから渡された値(data)を引数に、LaravelのAPIサーバーにPOSTする。

employee
let url = location.href;

const baseUrl = url + 'api/employee';

const employee = {};

employee.save = async (data) => {
  const urlSave = baseUrl+"/save"
  const res = await axios.post(urlSave,data)
  .then(response=>{ return response.data; })
  .catch(error=>{ return error; })
  return res;
}

Laravel側のapi.phpが呼び出され、実際に処理をするcreateアクションを呼び出します。

api.php
use App\Http\Controllers\EmployeeController;

Route::match(['get', 'post'],'/employee/itemcreate', [EmployeeController::class, 'itemcreate']);

itemcreateメソッド内で保存処理を行い、responseに$itemsを渡してreturnしてあげる。

ItemController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Item;

class EmployeeController extends Controller
{
      public function itemcreate(Request $request){

        try {
            $items = new Item();
            $items->text = $request->text;
            $items->amount = $request->comment;
            $items->save();

            $response['data'] = $items;
            $response['message'] = '成功';
            $response['success'] = true;

          } catch (\Exception $e) {
            $response['message'] = $e->getMessage();
            $response['success'] = false;
          }
          return $response;
      }

}

createアクションはこのような流れで実装できます

指摘などがございましたらコメントで教えていただけると幸いです^_^

次はレコードをmapで繰り返し表示する方法など記事にしていこうと思います〜!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPをherokuでデプロイする!(3)データベースのインポート

はじめに

PHPをherokuでデプロイする方法、今回はローカル環境で作ったデータベースの引継ぎとしてインポートする方法を書いていきます!

データベースをインポートするための準備

まずはローカル環境で作ったデータベースを引き継ぐ形でデータベースを作成していきます。前回でcleardbのアドオンを用いてMySQLが利用できるようになっていると思います。PHPではPHPMyAdminでデータベースの作成や編集、データ内容の確認ができますが、あくまでこれはローカル環境のみとなっています。herokuでデータベースをインポートするために便利なのが、「MySQL Workbench」です。

MySQL Workbenchのインストール

MySQL WorkbenchはMySQLが公式に提供しているアプリで、GUIで操作しやすいのが特徴です。ダウンロードはこちらです。
https://www.mysql.com/jp/products/workbench/
「ダウンロードはこちら」ボタンを押し自分のOSを選んでダウンロードし、インストールを進めましょう。ダウンロードする前にユーザー登録を求められますが、「No thanks, just start my download.」が下に表示されているのでそれを押すとユーザー登録なしにダウンロードできます。

なお、最新バージョンだとインストールしたあとmacでは動作しないことがあったようです。その場合はArchivesボタンを押して、8.0.19をダウンロード、インストールすると確実に動作します。インストール後表示にしたがってアイコンをドラッグするとmacであればApplicationsフォルダーにコピーされます。

MySQL Workbenchの設定

起動させると
Work1.png
こんな画面になります。MySQLConnectionsの隣にある+のボタンを押すとデータベースの作成ができます。

work2.png
ここで入力していくのはherokuのデータベースの情報です。(1)の中でも紹介した、herokuのデータベース情報を元に入力しましょう。herokuのデータベース情報はターミナルでデプロイしたいアプリにアクセスして以下を入力してください。

heroku config | grep CLEARDB_DATABASE_URL

ここで表示されるのは、CLEARDB_DATABASE_URL: mysql://ユーザー名:パスワード@ホスト名/データベース名?reconnect=trueとなります。
これに基づいて先ほどの画像の内容を入れていきましょう。

まず、「connection name」ですがこれはこのデータベースを管理する上での名前なので、好きに名付けて構いません。「conecction Method」はそのままの設定で進めましょう。
Hostnameは、先ほどherokuconfigで確認したホスト名を、Usernameも確認したユーザー名を入れましょう。Portはそのままで構いません。
そしてパスワードは「Store in Key chain」ボタンをクリックすると入力できます。入力内容が確認できないので間違いないように気をつけてください。
「Default Schema」にはデータベース名を入れましょう。

全て入れたらTest conecctionボタンを押してみましょう。そうすると問題なければ「Successfully made the MySQL connection」と表示されます。もし問題がある場合は入力内容に間違いがないかみていきましょう。これで接続できていれば、データベースをインポートする準備ができています!

データベースのエクスポート

次に、インポートするためにPHPMyAdminからデータをエクスポートします。
PHPMyAdminにアクセスし、エクスポートボタンを押して、エクスポート方法で詳細を押すと、データベースの選択ができるので、エクスポートしたいデータベースを選びましょう。
出力は必ず「出力をファイルに保存する」を選びましょう。これでないとインポートできません。その他はそのままで問題ないので、実行を押すとファイルがダウンロードされます。

データベースのインポート

それでは、いよいよインポートの作業に入ります!MySQL workbenchを再び開くと、先ほど追加したデータベースが表示されていると思います。ボタンを押すと少し接続の待ち時間があり、その後画面が開きます。

Work3.png
このような画面になったら、Data Import/Restoreを開きましょう。

work4.png
そうするとこんな感じの画面になるので、「Import from Self-Contained File」を選択し、先ほどエクスポートしたファイルを選びましょう。また「Default Target Schema」ではherokuのデータベース名を選択しましょう。
そして「Import progress」を選択し、「Start Import」を押せばインポート完了です!

もしうまくいかない時は・・・

ちなみに、もしインポートできない場合は、データベース名をコメントアウトし、テーブル名をのみを残すような形で編集するとうまくいくことがあります!ちなみにエクスポートしたデータベースは右クリックで「このアプリケーションから開く」を選択し、VScodeなどプログラミング用のアプリケーションを開けば内容を見ることができます。

次回はいよいよ最終章として、デプロイに必要なcomposerのインストール、そしてデプロイの方法について書いていきます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む