Quantcast
Channel: 初心者タグが付けられた新着記事 - Qiita
Viewing all 21081 articles
Browse latest View live

Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編

$
0
0

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の2本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

前回まで

前回は、環境構築と必要なパッケージのインストールを行いました。

http://localhost:8000
でLaravelのウェルカムページが表示される状態で
次に進んでください。

コンポーネントの構成

本記事では、この全体図の青色部分、
Vue.jsによるフロントエンド実装のみを行います。

Untitled Diagram.png

作るページ(コンポーネント)は全部で4つです。

  • タスク一覧
  • タスク詳細
  • タスク登録
  • タスク編集

最初に各ページの完成状態の画像を確認します。

  • タスク一覧
    list.PNG

  • タスク詳細
    show.PNG

  • タスク登録
    create.PNG

  • タスク編集
    edit.PNG

前にインストールしたlaravel/ui vueに
デフォルトで組み込まれているbootstrapを使って
最低限のシンプルなUIにしています。
※今回はbootstrapの使い方には言及しません

各ページ上部にある黒い背景色の部分はヘッダーナビで、
全ページ固定で表示されるコンポーネントです。

ヘッダーナビより下の
一覧テーブルや入力フォーム部分が
URLごとに切り替わるメインのコンポーネントになります。

それでは、各ページのメインコンポーネントに加えて
ヘッダーーコンポーネントの
計5つを実装していきます。

ベースbladeとベースルーティングを追加

このアプリでは、
初回アクセス時のみLaravel側でリクエストを受けて
ページを表示し、
それ以降はフロント側のVue Routerによってルーティングが行われます。

その最初のリクエストを受け取る
Laravel側のルーティングとbladeファイルを追加します。

routes/web.php
- Route::get('/', function () {
-     return view('welcome');
- });
+ Route::get('/{any}', function() {
+     return view('app');
+ })->where('any', '.*');
resouces/views/app.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">
+ 
+     <!-- CSRF Token -->
+     <meta name="csrf-token" content="{{ csrf_token() }}">
+ 
+     <title>{{ config('app.name', 'Vue Laravel SPA') }}</title>
+ 
+     <!-- Styles -->
+     <link href="{{ mix('/css/app.css') }}" rel="stylesheet">
+ </head>
+ <body>
+ <div id="app">
+ 
+ </div>
+ <!-- Scripts -->
+ <script src="{{ mix('/js/app.js') }}" defer></script>
+ </body>
+ </html>

commit:ベースのbladeとルーティング追加

これで、どのURLでアクセスしても
このapp.blade.phpが表示されるようになりました。

また、前回の記事でインストールした
Vue.jsやbootstrapも
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<script src="{{ mix('/js/app.js') }}" defer></script>
このjs、cssファイルで読み込まれているため
利用できる状態です。

試しにデフォルトで用意されている
ExampleComponentを表示してみてください。

resouces/views/app.blade.php
<div id="app">
+ <example-component></example-component>
</div>

これで
http://localhost:8000
にアクセスすると、
このようにExampleComponentが表示されると思います。

example.PNG

これが正しく表示されていれば、
Vue.js、bootstrapがちゃんと使えている状態です。
(このExampleComponentはbootstrapが使われています)

ヘッダーコンポーネント実装

ベースのbladeが配置できたので、
次に全ページ共通で固定表示する
ヘッダーコンポーネントを実装します。

HeaderComponentの追加

resources/js/components/HeaderComponent.vue
+ <template>
+     <div class="container-fluid bg-dark mb-3">
+         <div class="container">
+             <nav class="navbar navbar-dark">
+                 <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span>
+                 <div>
+                     <button class="btn btn-success">List</button>
+                     <button class="btn btn-success">ADD</button>
+                 </div>
+             </nav>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

classがいろいろとたくさん設定されていますが、
全部bootstrapのclassで見た目を整えているだけなので、
あまり気にしなくてOKです。
 

そのコンポーネントをVueインスタンスに登録

resources/js/app.js
+ import HeaderComponent from "./components/HeaderComponent";
//↑ファイル先頭

  Vue.component('example-component', require('./components/ExampleComponent.vue').default);
+ Vue.component('header-component', HeaderComponent);

 
 

登録したコンポーネントをベースbladeに追加

resources/views/app.blade.php
<div id="app">
+     <header-component></header-component>
</div>

commit:ヘッダーコンポーネント実装

この状態でページを表示してみます。
npm run devまたは npm run watchでソースをビルドするのを忘れないようにしましょう

ページ上部に黒いヘッダーナビが表示されていると思います。

まだボタンのリンク先は設定されていませんが、
この後ページを追加した際にこのボタンのリンクも設定します。

タスク一覧コンポーネント実装

まずタスク一覧コンポーネントを追加します。

resources/js/components/TaskListComponent.vue
+ <template>
+     <div class="container">
+         <table class="table table-hover">
+             <thead class="thead-light">
+             <tr>
+                 <th scope="col">#</th>
+                 <th scope="col">Title</th>
+                 <th scope="col">Content</th>
+                 <th scope="col">Person In Charge</th>
+                 <th scope="col">Show</th>
+                 <th scope="col">Edit</th>
+                 <th scope="col">Delete</th>
+             </tr>
+             </thead>
+             <tbody>
+             <tr>
+                 <th scope="row">1</th>
+                 <td>Title1</td>
+                 <td>Content1</td>
+                 <td>Ichiro</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             <tr>
+                 <th scope="row">2</th>
+                 <td>Title2</td>
+                 <td>Content2</td>
+                 <td>Jiro</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             <tr>
+                 <th scope="row">3</th>
+                 <td>Title3</td>
+                 <td>Content3</td>
+                 <td>Saburo</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             </tbody>
+         </table>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

ID、Title、Content(内容)、Person In Charge(担当者)、各種操作ボタン
をカラムに持つテーブルです。

現時点では、サンプルとして3行ほどべた書きで
タスクを表示しています。

後々の作業でここは
LaravelAPIからデータを受け取り表示するようになります。

また、
Show、Edit、Deleteのボタンを設置していますが
いまはリンク先が設定されていません。

後々各コンポーネントを実装したらリンク先を設定していきます。
 
 
追加したタスク一覧コンポーネントを
Vue Routerに登録します。

resources/js/app.js
+ import VueRouter from 'vue-router';
  import HeaderComponent from "./components/HeaderComponent";
+ import TaskListComponent from "./components/TaskListComponent";


  window.Vue = require('vue');


+ Vue.use(VueRouter);
+ 
+ const router = new VueRouter({
+     mode: 'history',
+     routes: [
+         {
+             path: '/tasks',
+             name: 'task.list',
+             component: TaskListComponent
+         },
+     ]
+ });


  const app = new Vue({
      el: '#app',
+     router
  });

VueRouter自体の詳しい解説は省略しますが、
ポイントはここです。

routes:[{path:'/tasks',name:'task.list',component:TaskListComponent},]

ここで、
「/tasks」のURLでアクセスしたら
「TaskListComponent」を表示する。
このルーティングの名前は「task.list」である。
と設定しています。

別ページ(コンポーネント)を追加した際は、
同じようにこの routesに設定を加えていくことになります。
 
 

そして、ルーティングで紐づけられたコンポーネントを表示するために、
ベースのbladeに <router-view>を配置する必要があります。

resources/views/app.blade.php
<div id="app">
     <header-component></header-component>


+    <router-view></router-view>
</div>

先ほどVue Routerで設定したとおり、
URLに紐づくコンポーネントがこの
<router-view>の部分に表示されることになります。

この状態で
http://localhost:8000/tasks
にアクセスしてみましょう。
※ビルドを忘れずに

お手本で見た通りの
一覧テーブルが表示されていると思います。

ついでに、
ヘッダーコンポーネントにある
「List」ボタンのリンク先を設定しておきましょう。

resources/js/components/HeaderComponent.vue
<nav class="navbar navbar-dark">
    <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span>
    <div>
        <button class="btn btn-success">List</button>
+        <router-link v-bind:to="{name: 'task.list'}">
<button class="btn btn-success">List</button>
+        </router-link>
<button class="btn btn-success">ADD</button>
    </div>

</nav>

このように <route-link>v-bind:to
リンク先のルーティング名を設定することで
SPAのリンクとして動作させることができます。

commit:タスク一覧コンポーネント実装

タスク詳細コンポーネント実装

次に、タスク詳細コンポーネントを追加します。

まずコンポーネントファイル作成。

resources/js/components/TaskShowComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row border-bottom">
+                         <label for="id" class="col-sm-3 col-form-label">ID</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id"
+                                v-bind:value="taskId">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title"
+                                value="title title">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content"
+                                value="content content">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge"
+                                value="Ichiro">
+                     </div>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {
+         props: {
+             taskId: String
+         }
+     }
+ </script>

taskIdをURLパラメータとして受け取って、
そのIDのみ
<input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
v-bind:value="taskId"部分で動的に表示しています。

それ以外のcontent、person-in-chargeは
まだべた書きにしているだけです。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import VueRouter from 'vue-router';
import HeaderComponent from "./components/HeaderComponent";
import TaskListComponent from "./components/TaskListComponent";
+ import TaskShowComponent from "./components/TaskShowComponent";
{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},+ {
+     path: '/tasks/:taskId',
+     name: 'task.show',
+     component: TaskShowComponent,
+     props: true
+ },

これで、/tasks/:taskIdのURLでアクセスすると、
TaskShowComponentが表示されます。

:taskIdの部分は、任意のタスクIDが入ります。

このURLパラメータが、
先ほどのタスク詳細コンポーネントの中で使われていた
taskIdとなります。

http://localhost:8000/tasks/3
のように :taskIdの部分に好きな数字を入れてアクセスすると
タスク詳細コンポーネントが表示されます。

ついでにタスク一覧コンポーネントに置いていた
「Show」ボタンのリンク先を設定しておきましょう。

resources/js/components/TaskListComponent.vue
+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 1}}">
<button class="btn btn-primary">Show</button>
+    </router-link>
+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 2}}">
<button class="btn btn-primary">Show</button>
+    </router-link>
+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}">
<button class="btn btn-primary">Show</button>
+    </router-link>

これで、一覧ページの「Show」ボタンをクリックすると
タスク詳細ページに遷移するようになりました。

commit:タスク詳細コンポーネント実装

タスク登録コンポーネント実装

次にタスク登録コンポーネントを実装します。

まずコンポーネントファイル作成。

resources/js/components/TaskCreateComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control" id="title">
+                     </div>
+                     <div class="form-group row">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control" id="content">
+                     </div>
+                     <div class="form-group row">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+                     </div>
+                     <button type="submit" class="btn btn-primary">Submit</button>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

ただ空のフォームを表示しているだけです。
現時点では送信処理は書いていません。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import VueRouter from 'vue-router';
import HeaderComponent from "./components/HeaderComponent";
import TaskListComponent from "./components/TaskListComponent";
+ import TaskCreateComponent from "./components/TaskCreateComponent";
import TaskShowComponent from "./components/TaskShowComponent";
{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},+ {
+     path: '/tasks/create',
+     name: 'task.create',
+     component: TaskCreateComponent
+ },
{
    path: '/tasks/:taskId',
    name: 'task.show',
    component: TaskShowComponent,
    props: true
},

これで、
http://localhost:8000/tasks/create
でアクセスすればタスク登録ページが表示されます。

ついでにヘッダーコンポーネントに置いていた
「Add」ボタンのリンク先を設定しておきます。

resources/js/components/HeaderComponent.vue
<div>
    <router-link v-bind:to="{name: 'task.list'}">
        <button class="btn btn-success">List</button>
    </router-link>
+    <router-link v-bind:to="{name: 'task.create'}">
<button class="btn btn-success">ADD</button>
+    </router-link>
</div>

commit:タスク登録コンポーネント実装

タスク編集コンポーネント実装

次に、タスク編集コンポーネントを実装します。

まずコンポーネントファイルを作成。

resources/js/components/TaskEditComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row">
+                         <label for="id" class="col-sm-3 col-form-label">ID</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
+                     </div>
+                     <div class="form-group row">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control" id="title">
+                     </div>
+                     <div class="form-group row">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control" id="content">
+                     </div>
+                     <div class="form-group row">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+                     </div>
+                     <button type="submit" class="btn btn-primary">Submit</button>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {
+         props: {
+             taskId: String
+         }
+     }
+ </script>

詳細ページと同様に、
taskIdをURLパラメータで受け取り、
IDの欄にデータを表示しています。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import TaskCreateComponent from "./components/TaskCreateComponent";
import TaskShowComponent from "./components/TaskShowComponent";
+ import TaskEditComponent from "./components/TaskEditComponent";
{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},
{
    path: '/tasks/create',
    name: 'task.create',
    component: TaskCreateComponent
},
{
    path: '/tasks/:taskId',
    name: 'task.show',
    component: TaskShowComponent,
    props: true
},+ {
+     path: '/tasks/:taskId/edit',
+     name: 'task.edit',
+     component: TaskEditComponent,
+     props: true
+ },

これで、
http://localhost:8000/tasks/:taskId/edit
にアクセスするとタスク編集ページが表示されます。

:taskIdの部分は任意のタスクIDになります。

ついでにタスク一覧コンポーネントに置いていた
「Edit」ボタンのリンク先も設定しておきます。

resources/js/components/TaskListComponent.vue
+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 1}}">
<button class="btn btn-success">Edit</button>
+    </router-link>
+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 2}}">
<button class="btn btn-success">Edit</button>
+    </router-link>
+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 3}}">
<button class="btn btn-success">Edit</button>
+    </router-link>

commit:タスク編集コンポーネント実装

おわりに

これで、
・タスク一覧ページ
・タスク詳細ページ
・タスク登録ページ
・タスク編集ページ
が実装できました。

現時点ではAPIでデータを取得する処理はできていませんが、
この状態でもVue.jsによる 静的な SPAにはなっています。

もしデータベースを利用しないような
ウェブサイトなどをVue.jsでSPAとして構築する場合は
今回解説した内容を基本として
ページの追加をしていくだけです。

それでは、次にLaravelのAPI実装に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編


Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編

$
0
0

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の3本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

前回まで

前回は、Vue.jsでフロントエンドのみ実装し、
静的なSPAができました。

べた書きのサンプルデータが表示されている状態で、
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページが完成しています。

API実装の進め方

この全体図の緑色部分にある
5つのAPIを実装していきます。

Untitled Diagram.png

今回は一番シンプルな形で進めるので、
各APIの処理は全てコントローラ内で数行で完結します。

また、API自体の実装の前に
DBのセットアップや最低限のテストデータも準備します。

SQLiteのセットアップ

今回は作業簡略化のため
MySQLやPostgreSQLを用意せず
SQLiteを使います。

まずはSQLiteのストレージとなるファイルを用意します。

database/database.sqliteに空のファイルを作成すればOKです。

次に、.envのDB接続情報を修正します。

.env
- DB_CONNECTION=mysql
- DB_HOST=127.0.0.1
- DB_PORT=3306
- DB_DATABASE=laravel
- DB_USERNAME=root
- DB_PASSWORD=
+ DB_CONNECTION=sqlite

これでSQLiteを利用するための設定は完了です。

ただし、PHPのSQLiteドライバーが有効になっている必要がありますので
もしなっていなければ有効にしてください。
https://awesome-linus.com/2019/05/24/php-sqlite-driver-install/

migration作成

migrationでタスクテーブルを作成します。

まずは下記コマンドでmigrationファイルを生成。

php artisan make:migration create_tasks_table

生成されたmigrationのupメソッドの中をこのように書き換えます。

create_tasks_table.php
Schema::create('tasks',function(Blueprint$table){$table->bigIncrements('id');$table->string('title',100);$table->string('content',100);$table->string('person_in_charge',100);$table->timestamps();});

commit:migration作成

モデル作成

次に、タスクテーブルに対応する
タスクモデルを作ります。

php artisan make:model Task

生成されたモデルファイルに、
$fillableのみ追記しておきます。

app/Task.php
  class Task extends Model
  {
+    protected $fillable = [
+        'title',
+        'content',
+        'person_in_charge',
+    ];
  }

commit:タスクモデル作成

seeder作成

次に、テストデータを自動生成するための
seederを作成します。

まずは下記コマンドでseederファイルを生成。

php artisan make:seeder TasksTableSeeder

生成されたseederファイルのrunメソッドを
このように修正します。

database/seeds/TasksTableSeeder.php
 public function run()
 {
+    for ($i = 1; $i <= 10; $i++) {
+        Task::create([
+                'title' => 'title' . $i,
+                'content' => 'content' . $i,
+                'person_in_charge' => 'person_in_charge' . $i,
+            ]
+        );
+    }
 }

また、このseederを実行するためにDatabaseSeederファイルも修正します。

database/seeds/DatabaseSeeder.php
 public function run()
 {
-    // $this->call(UsersTableSeeder::class);
+    $this->call(TasksTableSeeder::class);
 }

commit:タスクseeder作成

テーブル、テストデータ生成

テーブルとテストデータを生成する準備は整いましたので、
実際に生成しましょう。

php artisan migrate --seed

これで先ほど作成したmigrationとseederが実行され、
テーブルとテストデータが10件できてるはずです。

データがちゃんと入っているか確認した場合は
tinkerを使ってみてください。

$ php artisan tinker


>>> Task::all();

これでタスクテーブルのデータが一覧で表示されます。

タスク一覧取得API実装

それでは早速API実装を始めます。
まずはタスク一覧取得APIから。

ルーティングを追加。

routes/api.php
+ Route::get('/tasks', 'TaskController@index');

次に、タスクコントローラを作成し、
そこにindexメソッドを追加します。

まずはartisanコマンドでコントローラファイル自体を生成。

php artisan make:controller TaskController

そして、indexメソッド追加。

app/Http/Controllers/TaskController.php
+ <?php
+
+ namespace App\Http\Controllers;
+
+ use App\Task;
+
+ class TaskController extends Controller
+ {
+     public function index()
+     {
+         return Task::all();
+     }
+ }

ただTaskモデルから全件取得してreturnするだけです。

POSTMANなどで
http://localhost:8000/api/tasks
にリクエストすると
タスク一覧が取得できると思います。
routes/api.phpにルーティング定義すると、自動でパスの頭に/apiがつきます。

レスポンスはこのようなjson形式になります。

レスポンス形式
[{"id":1,"title":"title1","content":"content1","person_in_charge":"person_in_charge1","created_at":"2019-12-17 00:43:38","updated_at":"2019-12-17 00:43:38"},{"id":2,"title":"title2","content":"content2","person_in_charge":"person_in_charge2","created_at":"2019-12-17 00:43:38","updated_at":"2019-12-17 00:43:38"},]

commit:タスク一覧取得API実装

タスク詳細取得API実装

次にタスク詳細取得APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
+ Route::get('/tasks/{task}', 'TaskController@show');

コントローラにshowメソッドを追加。

app/Http/Controllers/TaskController.php
+ public function show(Task $task)
+ {
+     return $task;
+ }

URLパラメータで受け取ったタスクモデルを
そのままreturnするだけです。
※これでLaravelが勝手にjson形式のレスポンスを返却します

commit:タスク詳細取得API実装

タスク登録API実装

次に、タスク登録APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
+ Route::post('/tasks', 'TaskController@store');
  Route::get('/tasks/{task}', 'TaskController@show');

※ルーティングの定義順を間違えると正しく動かないので、この通りに記述してください

コントローラにstoreメソッド追加。

app/Http/Controllers/TaskController.php
  use App\Task;
+ use Illuminate\Http\Request;
+ public function store(Request $request)
+ {
+     return Task::create($request->all());
+ }

リクエストで受け取ったデータをそのまま
モデルのcreateでデータ登録しているだけです。

このようなjson形式のデータを受け取ることを想定しています。

リクエスト形式
{"title":"new title","content":"new content","person_in_charge":"new person_in_charge1"}

commit:タスク登録API実装

タスク更新API実装

次に、タスク更新APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
  Route::post('/tasks', 'TaskController@store');
  Route::get('/tasks/{task}', 'TaskController@show');
+ Route::put('/tasks/{task}', 'TaskController@update');

コントローラにupdateメソッド追加。

app/Http/Controllers/TaskController.php
+ public function update(Request $request, Task $task)
+ {
+     $task->update($request->all());
+
+     return $task;
+ }

受け取るリクエストの形は、
登録APIと同じjson形式です。

URLパラメータで受け取ったTaskモデルのupdateメソッドで
そのままデータを更新するだけです。

commit:タスク更新API実装

タスク削除API実装

次はタスク削除API。

ルーティング追加。

routes/api.php
  Route::get('/tasks/{task}', 'TaskController@show');
  Route::put('/tasks/{task}', 'TaskController@update');
+ Route::delete('/tasks/{task}', 'TaskController@destroy');

コントローラにdestroyメソッド追加。

app/Http/Controllers/TaskController.php
+ public function destroy(Task $task)
+ {
+     $task->delete();
+ 
+     return $task;
+ }

URLパラメータでTaskを受け取り、
それをそのままdeleteします。

commit:タスク削除API実装

おわりに

これで今回必要なAPIはすべて実装完了です。

POSTMANなどを利用して、
各APIの動作を確認するといいと思います。

本来は、このAPIでは
バリデーションを入れたり、
検索処理を入れたりすることになるかと思います。

次回は、
フロントのVueからAjaxで
このAPIに対してリクエスト送信し、
実際にデータの表示、更新、登録、削除ができるようにします。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

$
0
0

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の4本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編
↑↑今ここ↑↑

前回まで

Vue.jsでフロントエンド実装と、
LaravelのAPI実装が完了しました。

APIにつないでない状態の
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページと、
・タスク一覧取得API
・タスク詳細取得API
・タスク登録API
・タスク更新API
・タスク削除API
が完成している状態です。

今回はこの静的ページと
APIを繋ぎ込んでいきます。

この全体図の赤色部分になります。

Untitled Diagram.png

axios

今回、フロントページから
AjaxでAPIにリクエストを送信して
データの取得や更新を行います。

Ajax通信を簡単に実装するため、
今回はaxiosというパッケージを利用します。
https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9

特に難しいところはありませんが、
axiosの使い方を簡単に把握しておきましょう。

laravel/uiでベースを構築したので、
自分でインストールや設定作業などしなくても
最初からaxiosが利用できる状態です。

タスク一覧取得API繋ぎ込み

早速、タスク一覧ページとタスク一覧取得APIを繋ぎ込んでみましょう。

まずは<script>に必要なデータ、メソッドを定義します。

resources/js/components/TaskListComponent.vue
<script>
-    export default {}
+    export default {
+        data: function () {
+            return {
+                tasks: []
+            }
+        },
+        methods: {
+            getTasks() {
+                axios.get('/api/tasks')
+                    .then((res) => {
+                        this.tasks = res.data;
+                    });
+            }
+        },
+        mounted() {
+            this.getTasks();
+        }
+    }
</script>

まず dataには空配列の tasksを用意します。

そして、 methodsにある getTasks()メソッドで、
タスク一覧取得APIにリクエストして
そのレスポンスを先ほどの tasksの中に入れています。
(このメソッドで先ほど話したaxiosを利用してリクエストしています)

そして、画面描画時にこの getTasks()メソッドが実行されるように、
mounted()でメソッドを呼び出しています。

これで<script>側は完了です。

次に<templete>側も修正します。

resources/js/components/TaskListComponent.vue
- <tr>
-     <th scope="row">1</th>
-     <td>Title1</td>
-     <td>Content1</td>
-     <td>Ichiro</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
- <tr>
-     <th scope="row">2</th>
-     <td>Title2</td>
-     <td>Content2</td>
-     <td>Jiro</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
- <tr>
-     <th scope="row">3</th>
-     <td>Title3</td>
-     <td>Content3</td>
-     <td>Saburo</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
+ <tr v-for="task in tasks">
+     <th scope="row">{{ task.id }}</th>
+     <td>{{ task.title }}</td>
+     <td>{{ task.content }}</td>
+     <td>{{ task.person_in_charge }}</td>
+     <td>
+         <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">
+             <button class="btn btn-primary">Show</button>
+         </router-link>
+     </td>
+     <td>
+         <router-link v-bind:to="{name: 'task.edit', params: {taskId: task.id }}">
+             <button class="btn btn-success">Edit</button>
+         </router-link>
+     </td>
+     <td>
+         <button class="btn btn-danger">Delete</button>
+     </td>
+ </tr>

まずはべた書きで表示していた
3行のデータを削除します。

そして、先ほど定義したtasksデータをv-forで表示します。
<tr v-for="task in tasks">

ID、Title、Content、Person In Chargeの
各カラムは {{ task.title }}のようにデータを動的に表示させます。

- <td>Title1</td>
+ <td>{{ task.title }}</td>

また、「Show」「Edit」ボタンの
リンクURLのパラメータもべた書きしていたので、
ちゃんと動的にidを設定します。

- <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}">
+ <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">

これで、
APIからデータを取得し
それをv-forで画面に一覧表示できるようになりました。

commit:タスク一覧ページAPI繋ぎ込み

タスク一覧ページ完成です。

タスク詳細取得API繋ぎ込み

次に、タスク詳細ページとタスク詳細取得APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskShowComponent.vue
<script>
    export default {
        props: {
            taskId: String
        },
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            getTask() {
+                axios.get('/api/tasks/' + this.taskId)
+                    .then((res) => {
+                        this.task = res.data;
+                    });
+            }
+        },
+        mounted() {
+            this.getTask();
+        }
    }

</script>

一覧ページと同じように、
dataに空のtaskを用意。
methodsgetTask()でAPIからタスクデータを取得。
mounted()で画面描画時にメソッド呼び出し。
としています。

次に<templete>側。

resources/js/components/TaskShowComponent.vue
<div class="form-group row border-bottom">
    <label for="id" class="col-sm-3 col-form-label">ID</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id"
-           v-bind:value="taskId">
+           v-model="task.id">
</div>

<div class="form-group row border-bottom">
    <label for="title" class="col-sm-3 col-form-label">Title</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title"
-           value="title title">
+           v-model="task.title">
</div>

<div class="form-group row border-bottom">
    <label for="content" class="col-sm-3 col-form-label">Content</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content"
-           value="content content">
+           v-model="task.content">
</div>

<div class="form-group row border-bottom">
    <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge"
-           value="Ichiro">
+           v-model="task.person_in_charge">
</div>

各データをv-modelで表示するようにしました。

これでAPI取得したデータをタスク詳細ページに表示できました。

commit:タスク詳細ページAPI繋ぎ込み

タスク詳細ページ完成です。

タスク登録API繋ぎ込み

次に、タスク登録ページとタスク登録APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskCreateComponent.vue
<script>
-    export default {}
+    export default {
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            submit() {
+                axios.post('/api/tasks', this.task)
+                    .then((res) => {
+                        this.$router.push({name: 'task.list'});
+                    });
+            }
+        }
+    }
</script>

空のtaskデータを用意するところは先ほどと同じです。

methodssubmit()メソッドで、
taskデータをタスク登録APIにPOST送信する処理を書いています。

また、APIによるデータ登録完了後、
this.$router.push({name: 'task.list'});でタスク一覧ページにリダイレクトしています。

 
 
次に<templete>側。

resources/js/components/TaskCreateComponent.vue
- <form>
+ <form v-on:submit.prevent="submit">
<div class="form-group row">
        <label for="title" class="col-sm-3 col-form-label">Title</label>
-        <input type="text" class="col-sm-9 form-control" id="title">
+        <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title">
</div>
    <div class="form-group row">
        <label for="content" class="col-sm-3 col-form-label">Content</label>
-        <input type="text" class="col-sm-9 form-control" id="content">
+        <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content">
</div>
    <div class="form-group row">
        <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
-        <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+        <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge">
</div>
    <button type="submit" class="btn btn-primary">Submit</button>

</form>

各フォームはv-modeltaskデータとバインディングすることで、
フォームにデータが入力されたら
<scripts>側のtaskデータも更新されるようになっています。

そして、
<form v-on:submit.prevent="submit">
で、フォーム送信時に先ほど定義したsubmitメソッドを呼び出すようにしています。

これで、入力内容が反映されたtaskデータを
submitメソッドでAPI送信できる状態になっています。

commit:タスク登録ページAPI繋ぎ込み

これでタスク登録ページ完成です。

タスク更新API繋ぎ込み

次に、タスク編集ページとタスク更新APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskEditComponent.vue
<script>
    export default {
        props: {
            taskId: String
        },
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            getTask() {
+                axios.get('/api/tasks/' + this.taskId)
+                    .then((res) => {
+                        this.task = res.data;
+                    });
+            },
+            submit() {
+                axios.put('/api/tasks/' + this.taskId, this.task)
+                    .then((res) => {
+                        this.$router.push({name: 'task.list'})
+                    });
+            }
+        },
+        mounted() {
+            this.getTask();
+        }
    }

</script>

タスク詳細ページとタスク登録ページでやったことを
両方やっているだけです。

空のtaskデータを用意し、
getTask()メソッドでAPIから取得したデータをセットする。

submitメソッドでは、
タスク更新APIにputリクエストを送信しています。

 
 
次に<template>

resources/js/components/TaskEditComponent.vue
- <form>
+ <form v-on:submit.prevent="submit">
<div class="form-group row">
        <label for="id" class="col-sm-3 col-form-label">ID</label>
-        <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
+        <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-model="task.id">
</div>
    <div class="form-group row">
        <label for="title" class="col-sm-3 col-form-label">Title</label>
-        <input type="text" class="col-sm-9 form-control" id="title">
+        <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title">
</div>
    <div class="form-group row">
        <label for="content" class="col-sm-3 col-form-label">Content</label>
-        <input type="text" class="col-sm-9 form-control" id="content">
+        <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content">
</div>
    <div class="form-group row">
        <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
-        <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+        <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge">
</div>
    <button type="submit" class="btn btn-primary">Submit</button>

</form>

これはタスク登録ページと同じです。

各フォームはv-modeltaskデータとバインディングして、
formの v-on:submit.prevent="submit"sumitメソッドを呼んでいます。

commit:タスク編集ページAPI繋ぎ込み

これでタスク編集ページは完成。

タスク削除API繋ぎ込み

最後に、タスク一覧ページのDeleteボタンとタスク削除APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskListComponent.vue
methods: {
    getTasks() {
        axios.get('/api/tasks')
            .then((res) => {
                this.tasks = res.data;
            });
    },
+    deleteTask(id) {
+        axios.delete('/api/tasks/' + id)
+            .then((res) => {
+                this.getTasks();
+            });
+    }
},

deleteTask()メソッドを追加しました。
タスクIDを引数で受け取り、
タスク削除APIにリクエストを送信しています。

削除完了したら、
getTasks()メソッドを呼んで
タスク一覧を再読み込みしています。

次に<template>

resources/js/components/TaskListComponent.vue
<td>
-    <button class="btn btn-danger">Delete</button>
+    <button class="btn btn-danger" v-on:click="deleteTask(task.id)">Delete</button>
</td>

もともと設置していたDeleteボタンに
v-on:click="deleteTask(task.id)"を追加しました。

これで、このボタンをクリックしたら deleteTask()メソッドが呼ばれます。

commit:タスク一覧ページ削除API繋ぎ込み

これでタスク一覧ページの削除処理もできたので、
全ページ、全機能が完成しました。

おわりに

シンプルなCRUD機能のアプリを
Vue.jsのSPAとLaravelのAPIで構築しました。

Vue側もLaravel側もほとんど難しいところもなく、
かなり簡単に書けたと思います。

今回はできるだけ簡単に一通りの機能を作るチュートリアルとしたかったため、
本来実装すべき処理を省いた箇所が多いです。

Vue側では
Ajaxのエラーハンドリングや
API送信前のバリデーションなど
本来は実装すべきです。

Laravel側もバリデーションや
APIの認証処理などがあるといいです。

今回のチュートリアルで
ざっくりと全体イメージをまずはつかんで、
今後上記のような詳細な処理を少しずつ追加していくといいかと思います。

Laravel + Vue.jsでクイズを作った

$
0
0

作ることになったきっかけ

以前からLaravelのアウトプットとして何か作りたいと思っていたのですが、なかなかネタが思い浮かばずずっと先延ばしになっていました。
そんなある日、趣味の競馬観戦でレースの条件(開催月や距離、競馬場)が自分の記憶のものとかなり変わっていることが度々あり、自分の脳内番組表(競馬の開催スケジュールのこと)のアップデートが必要だと感じました。
そこでLaravelと、ついでにVue.jsも使ってクイズを作ることにしました。

ざっくり仕様

  • レースはGI、GⅡ、GⅢのようにグレードが定められているので、そのグレードごとにカテゴリー分けして、トップページで挑戦するカテゴリーを選択します。
  • 問題は全10問。1つの画面で開催月、距離、競馬場を選択して解答します。
  • 「回答する」ボタンをクリックすると、答えと解説が表示されます。
  • 「次の問題へ」をクリックすると、次の問題が出題されます。 10問目まで解答が終わると、結果画面が表示されます。

出題〜解答までの動き

今回はシンプルに問題の出題のみ行うので、テーブルはproblemsテーブル1つのみです。
せっかくなら画面遷移のないSPAにしたいので、コンポーネントとしてトップページ、出題ページ、結果ページの3つのVueファイルを作成します。
簡単に処理の流れを書くと、
1. fetchItemで問題をテーブルからランダムに1件取得、開発ツールからのカンニングを防ぐため、idとレース名のみ取得。
2. fetchAnswerで該当の問題をidで検索、答え合わせをする。このとき、出題済みの問題がまた出題されないようにprocessed_flgをtrueにします。
3. fetchNewItemで次の問題を取得。

ProblemCpntroller.php
publicfunctionindex($category){DB::table('problems')->update(['processed_flg'=>false]);$query=Problem::query();$query->where('category','=',$category);$problem['count']=$query->where('processed_flg','=',false)->count();if($query->exists()){$query->select('id','title')->get();$problem=$query->inRandomOrder()->first();return$problem;}}publicfunctionfetchNewItem(Request$request){$category=$request->input('category');$query=Problem::query();$query->where('category','=',$category);$query->where('processed_flg','=',false);if($query->exists()){$query->select('id','title')->get();$problem=$query->inRandomOrder()->first();return$problem;}}publicfunctionfetchAnswer(Request$request){$id=$request->input('id');$problem=Problem::find($id);$problem->processed_flg=true;$problem->save();return$problem;}
Problem.vue
methods:{asyncfetchItem(){this.is_question=trueconstresponse=awaitwindow.axios.get(`/api/ploblems/category/${this.category}`)this.problem=response.data.titlethis.is_answer=falsethis.answer.id=response.data.id},asyncfetchNewItem(){if(this.counter===9){this.next_message='結果発表'}elseif(this.counter===10){this.$router.push({name:'result',params:{score:this.score}})}this.errors={}this.answer={}this.counter++this.is_question=trueconstresponse=awaitwindow.axios.get('/api/ploblems/category/',{params:{category:this.category}})this.problem=response.data.titlethis.is_answer=falsethis.answer.id=response.data.id},asyncfetchAnswer(){constanswer=awaitwindow.axios.get('/api/ploblems/',{params:{id:this.answer.id}})this.get_answer=answer.datathis.is_answer=truethis.is_question=falseif(this.answer.month!==this.get_answer.month){this.errors.month=true}if(this.answer.distance!==this.get_answer.distance){this.errors.distance=true}if(this.answer.place!==this.get_answer.place){this.errors.place=true}if(!this.errors.month&&!this.errors.month&&!this.errors.place){this.score++}}

実際の動き

raceq_ .gif

作ってみた感想

時間の関係で、とにかく動くことを優先してしまったので、リファクタリングの余地ありありになってしまいました。
答え合わせのときの表示も、もっと分かりやすく表示させたかったですね。
でもこうしてアウトプットをすることで、自分の理解の浅い部分を洗い出すことができました。今後も作ってみたい物はあるので、LaravelやVue.jsに限らずいろいろ挑戦したいです!

学習記録 その6(10日目)

$
0
0

学習記録(10日目)

勉強開始:12/7(土)〜
使用書籍:大重美幸『詳細! Python3 入門ノート』(ソーテック社、2017年)

【Numpyの配列(Ch.15 / p.380)】 から再開(9日目)、
【手書き文字の分類(Ch.16 / p.396)】 まで終了(10日目)

本日から機械学習に入ります。

機械学習(手書き文字の分類)

(1)学習データを訓練データとテストデータに分ける。
(2)訓練データと教師データを学習器に入れる。→ 学習済みモデル(分類器)
(3)テストデータと教師データを分類器に入れ、性能を評価する。

・scikit-learn(サイキットラーン)という学習器を使用
・手書き文字の分類(sklearnパッケージのdatasetsモジュール使用)
 パッケージは複数のモジュールをまとめたもの。
・scikit-learnの数字画像データを使用して練習
 今回は画像データ(digits.data)と教師データ(digits.target)を分割して使用する。
 画像データの2/3が訓練データ、1/3がテストデータ
 教師データについても上に対応するよう分割する。
 アルゴリズムはSVM(サポートベクターマシーン)のSVCを使用
・テストデータを分類器に入れたところ、以下のエラーが発生
 Classification metrics can't handle a mix of multiclass-multioutput and multiclass targets
 → 全て打ち直したら解決した。原因不明・・・。再現中に、全角スペースが原因で何回かエラーが出たから、その可能性もあるかも。

サポートベクターマシーンのSVCにおけるgamma

・gammaに限らず、svm.svc()の引数で学習器のパラメータを調整できる。
 書籍上はgamma=0.001で精度96.3%だったが、gamma=1としたところ、9.8%まで精度は落ち込んだ。
 逆にgamma=0.001としたところ、精度93.2%に。低ければよいというものでもないらしい。
 このあたりの調整がよく聞くチューニングというものに該当するのかな?

データベース基礎(MySQL)

$
0
0

データベースとは

  「複数で共有」「検索、加工」される、一定の形式で整理されたデータの集まりのこと。

データベースの活用

  事例1)
   ①facebookの自分のページにアクセス
   ②facebookのデータベース内にある自分の情報を探す
    (氏名、メールアドレス、投稿内容、投稿した写真等)
   ③インターネットを経由して自分の情報を呼び出し、ブラウザ上で表示

  事例2)
   ①ショッピングサイトにアクセスし、欲しい商品を検索
   ②検索条件を受け、データベース内でマッチした商品情報を探す
   ③インターネットを経由してマッチした商品情報を呼び出し、ブラウザ上で表示

リレーショナルデータベース(RDB)

   一般的に使用されているデータベースは下記の表のようなリレーショナル・データベースである。

氏名電話番号年齢住所
山田 太郎090-0000-000020歳東京都千代田区
佐藤 次郎090-0000-000025歳千葉県千葉市
鈴木 花子090-0000-000030歳神奈川県横浜市

  表全体をtable
  表の列をカラム=フィールドという
  表の行をロー=レコードという

SQLとは

  プログラミング言語とは違い、RDBMS(データベース管理システム/Relational DataBase Management System)専用に作られた問い合わせ言語。
  SQL(Structured Query Language)
  

MySQLとは

  MySQLは、世界で最も利用されているデータベース管理システム。
  大容量のデータに対しても高速で動作し、機能も豊富で実用性が高い。

クエリとトランザクション

  MySQLに限らず、データベースは、使用者が発行する命令によって動作しており、
  この命令を一般にクエリという。
  そのクエリの集合をトランザクションという。
  それらのクエリがすべて適用できた場合のみデータベースに反映される。
  一つでも適用できないクエリがあった場合、そのまとまりすべてのクエリの結果は反映されない。

コミットとロールバック

  データベースのユーザーは、トランザクションにおいて、コミット(COMMIT)ロールバック(ROLLBACK)を行うことができる。
  処理を確定する場合にはコミット、データベースに対する変更処理をすべて取り消す場合、ロールバックを行う。
  ※コミットを行うと、ロールバックによる処理の取り消しはできなくなる

データ型

  MySQLにおけるデータ型
■整数型

バイト最小値最大値
TYNYINT1-128127
SMALLINT2-3276832767
MEDIUMINT3-838860883388607
INTEGER4-21474836482147483647
BIGINT8-92233720368547758089223372036854775807

■浮動小数点型











バイト最小値最大値
FLOAT4-3.4-2823466E+38〜-1.175494351E-381.175494351E-38〜3.402823466E+38
DOUBLE8-1.7976931348623157E+308〜-2.2250738585072014E-3082.2250738585072014E-308〜1.7976931348623157E+308

■文字列型

バイト特徴
CHAR255固定長文字列
VARCHAR255可変長文字列
TEXT655535文章に利用

■日付・時刻型

内容範囲
DATETIME日付と時刻'1000-01-01 00:00:00'〜'9999-12-31 23:59:59'
TIMESTAMP日付と時刻'1970-01-01 00:00:01.000000'UTC〜'2038-01-19 03:14:07.999999'
DATE日付'1000-01-01'〜'9999-12-31
TIME時刻'-838:59:59〜:838:59:59'
YEAR時刻'1901〜2155'

MySQLの使用方法

  ①レンタルサーバーを借りて、そのサーバー上でMySQLを使用する
  ②Linuxサーバを自分のPCにインストールして、その上でMySQLを使用する
  ③XAMPPを自分のPCにインストールして、その上でMySQLを使用する

XamppとMamp

  Xamppは、Windows、Linux、maxOS、Solarisなどの様々なOSと、Apache(webサーバ)
  MariaDB(SQLデータベースサーバ;旧バージョンはMySQL)、PHPがパッケージになったもの。

  Mampは、Xampp同様の開発環境がWindowsとmacOSのデスクトップ上で使用できる。
  ※macユーザーは一般的にMampを使用することが多い

テーブルの作成と削除

対応するSQL文操作内容/意味/活用事例
Create文データベースにテーブルを作成する
Drop文作成したテーブルを削除する

テーブルの操作

 データベース操作は大きく4つに分かれる。
 ※CRUDと呼ばれる

名称対応するSQL文操作内容/意味/活用事例
Createinsert文データをテーブルに書き込む。 Facebookに新規で自分の名前を登録する際には、システムの裏側で、insert文が走っている
Readselect文テーブルに入っているデータを呼び出す。 アカウントを持っているFacebookにログインした際に、自分のプロフィールや投稿内容が表示されるとき、その裏側ではselect文が走っている。
Updateupdate文テーブルにすでに入っているデータを呼び出す。 facebookにすでに登録してあるメールアドレスを変更する際、その裏側ではupdate文が走っている。
Deletedelete文テーブルにすでに入っているデータを削除する。 facebookにすでに登録してあるアカウントを削除する際に、その裏側でdelete文が走っている。

SQL文

 データベースを操作する文のことを総称して、SQL文という。
 ※SQL分は、大文字小文字の区別がなく、改行してもしなくても良い

create文

  テーブルを作成しデータを書き込むSQL文。
  create table 任意のテーブル名(
  カラム名 データ型、 //int(数字)、varchar(文字列)、decimal(金額)等がある
  カラム名 データ型
  );

 例)テーブル「addresslist」を作成し、user_id、name、mail、tellとういう名前のカラムを作成
  create table addresslist(
   user_id int(11),
   name varchar(255),
   mail varchar(255),
   tell varchar(255),
   prefecture varchar(255)
  );

 ●デーブル/データベースが存在していない場合、それを作成する場合
  create table/database if note existsテーブル名/データベース名;

insert文

  空のテーブルにデータを挿入するSQL文。
  insert into テーブル名 values
  ("挿入するデータ","挿入するデータ","挿入するデータ");

 例1)データを1行追加するinsert文
  insert into addresslist VALUES
   ("1","山田太郎","abcd123@yahoo.co.jp","03-0000-0000","東京");

 例2)データを複数行追加するinsert文
  insert into addresslist VALUES
   ("2","佐藤花子","xyz777@yahoo.co.jp","073-0000-0000","神奈川"),
   ("3","田中浩史","hello888@yahoo.co.jp","045-0000-0000","静岡"),
   ("4","鈴木次郎","efg123@yahoo.co.jp","080-0000-0000","沖縄"),
   ("5","藤田三郎","ccc999@yahoo.co.jp","090-0000-0000","千葉");

select文

  テーブルに入っているデータを抽出するために使用するSQL文
  select 抽出対象のカラム名 from 抽出対象のテーブル名;
  ※すべてのカラムを対象とする場合はカラム名の箇所に「*」と記述する

例1)addresslistのすべてのデータを抽出するselect文
  select * from addresslist;

例2)addresslistの指定するカラム(name)を抽出するselect文
  select name from addresslist;

例3)addresslistのprefectureが千葉のフィールドのうち、指定するカラム(mail)を抽出するselect文
  select mail from addresslist where prefecture = "千葉";
  ※where以下には、演算子を使用することもできる

update文

  テーブルに入っているデータを上書きして更新するSQL文
  update 対象のテーブル名 set 上書き対象のカラム名 = "上書きするデータ" where 上書きするフィールドの指定

 例1)addresslistのuser_idが1のレコードのprefectureを埼玉に上書きするupdate文
  update addresslist set prefecture = "埼玉" where user_id = 1;

delete文

  テーブルに入っているデータを削除するSQL文
  delete from 対象のテーブル名;

 例2)addresslistのuser_idが3の行を削除するdelete文
  delete from addresslist where user_id = "3"'

ソート

  select文でデータを抽出する際に、データを並び替えて表示できる。
  これをソートという。
  select カラム名 from テーブル名 order by カラム名 asc;
  ※ascは昇順、descは降順となる、何も書かなければ昇順となる

 例1)fruit_stockテーブルのすべてのデータをnumberカラムの昇順に並べ替えるソート文
  select * from fruit_stock order by number asc;

 例2)fruit_stockテーブルのすべてのデータのうち、numberが25以上のものを、priceカラムの降順に並べ替えるソート文
  select * from fruit_stock where number >= 25 order by price desc;

データの集計

  select文を使って、データの合計値、最大値、平均値などのデータの集会ができる。

データの件数を取得

  select count(カラム名)from テーブル名;

 例1)fruit_stockテーブルのfruitカラムのデータ件数を取得するselect文
  select count(fruit) from fruit_stock;

 例2)fruit_stockテーブルの中から、priceが200以上で、madeinが日本のfruitカラムのデータ件数を取得する条件付きselect文
  select count(fruit) from fruit_stock where price >= 200 and madein = "日本";

データの合計値を取得

  select sum(カラム名) from テーブル名;

 例)fruit_stockテーブルのnumberカラムの合計値を取得するselect文
  select sum(number) from fruit_stock;

データの最大値を取得

  select max(カラム名) from テーブル名;

データの最大値を取得

  select min(カラム名) from テーブル名;

データの平均値を取得

  select avg(カラム名) from テーブル名;

コマンドプロンプト/ターミナルでのMySQL操作

  ターミナルでのMySQL操作の前に、以下の手順が必要。
  ①Xampp/Mampを起動し、ApacheとMySQLを起動
  ②MySQLにログインコマンドでログイン
  ③useコマンドで使用したいデータベースへ移動
  ④selectinsertdescコマンドなどでデータベースを操作

ログインコマンド

コマンド意味
mysqlmysqlにログインするためのコマンド
-uuser(ユーザー)オプション:-uの次にユーザー名を指定し、そのアカウントでログイン
rootroot(ルート)は最高権限をもつユーザー ※会社等の組織では、通常rootではなく、固定された権限を持つユーザーアカウントを使う
-ppassword(パスワード)オプション:このコマンドの後にパスワードが求められる

基本操作コマンド

コマンド意味
showデータベースやテーブルを見るためのコマンド
use使用したいデータベースへ移動するためのコマンド
descテーブルの構造やデータを見るためのコマンド
show

  データベース一覧を表示するコマンド
  show databases;

use

  使用したいデータベースにするコマンド
  use データベース名;

show

  テーブル一覧を表示するコマンド
  show tables;

desc

  テーブルの構造を表示するコマンド
  desc テーブル名;

select

  テーブルのデータを表示するコマンド
  select * from テーブル名;

drop

  テーブル/データベース自体を削除するコマンド
  drop table/database テーブル名/データベース名;

  デーブル/データベースが存在している場合、それを削除する
  drop table/database if existsテーブル名/データベース名;

ALTER TABLE

   一度作成されたテーブルはALTER TABLEというコマンドを使用してテーブル構造の変更・削除・追加ができる。
  alter table テーブル名;

  ◆テーブル名を変更
   alter table テーブル名 rename 新しいテーブル名;

  ◆カラム名を変更
   alter table テーブル名 change 変更するカラム名 データ型;

  ◆カラムのデータ型のみを変更
   alter table テーブル名 modify 変更するカラム名 新しいデータ型;

  ◆カラムを追加
   alter table テーブル名 add カラム名 データ型;

  ◆カラムを削除
   alter table テーブル名 drop カラム名;

--テーブルの作成mysql>createtablesumple01(->idint,->namevarchar(255),->emailvarchar(255),->classenum("no1","no2","no3")->);QueryOK,0rowsaffected(0.02sec)--テーブルにデータを入力mysql>insertintosumple01values->(1,"田中さん","tanaka@yahoo.co.jp","no1"),->(2,"山田さん","yamada@gmail.com","no3"),->(3,"木村さん","kimura@yahoo.co.jp","no2");QueryOK,3rowsaffected(0.01sec)Records:3Duplicates:0Warnings:0--テーブルをすべて表示mysql>select*fromsumple01;+------+--------------+--------------------+-------+|id|name|email|class|+------+--------------+--------------------+-------+|1|田中さん|tanaka@yahoo.co.jp|no1||2|山田さん|yamada@gmail.com|no3||3|木村さん|kimura@yahoo.co.jp|no2|+------+--------------+--------------------+-------+3rowsinset(0.00sec)--テーブルの構造を表示mysql>descsumple01;+-------+-------------------------+------+-----+---------+-------+|Field|Type|Null|Key|Default|Extra|+-------+-------------------------+------+-----+---------+-------+|id|int(11)|YES||NULL|||name|varchar(255)|YES||NULL|||email|varchar(255)|YES||NULL|||class|enum('no1','no2','no3')|YES||NULL||+-------+-------------------------+------+-----+---------+-------+4rowsinset(0.00sec)--テーブルの名前を変更mysql>altertablesumple01renamesample02;QueryOK,0rowsaffected(0.00sec)--テーブルを表示mysql>showtables;+--------------------+|Tables_in_lesson02|+--------------------+|sample02|+--------------------+1rowinset(0.00sec)--テーブルの中身を表示mysql>select*fromsample02;+------+--------------+--------------------+-------+|id|name|email|class|+------+--------------+--------------------+-------+|1|田中さん|tanaka@yahoo.co.jp|no1||2|山田さん|yamada@gmail.com|no3||3|木村さん|kimura@yahoo.co.jp|no2|+------+--------------+--------------------+-------+3rowsinset(0.00sec)--idカラムをNumberカラムに名称変更mysql>altertablesample02changeidNumberint;QueryOK,0rowsaffected(0.02sec)Records:0Duplicates:0Warnings:0--結果の確認mysql>select*fromsample02;+--------+--------------+--------------------+-------+|Number|name|email|class|+--------+--------------+--------------------+-------+|1|田中さん|tanaka@yahoo.co.jp|no1||2|山田さん|yamada@gmail.com|no3||3|木村さん|kimura@yahoo.co.jp|no2|+--------+--------------+--------------------+-------+3rowsinset(0.00sec)--emailカラムをmailaddlessカラムに名称変更mysql>altertablesample02changeemailmailaddlessvarchar(255);QueryOK,0rowsaffected(0.02sec)Records:0Duplicates:0Warnings:0--結果の確認mysql>select*fromsample02;+--------+--------------+--------------------+-------+|Number|name|mailaddless|class|+--------+--------------+--------------------+-------+|1|田中さん|tanaka@yahoo.co.jp|no1||2|山田さん|yamada@gmail.com|no3||3|木村さん|kimura@yahoo.co.jp|no2|+--------+--------------+--------------------+-------+3rowsinset(0.00sec)--テーブルの構造を表示mysql>descsample02;+-------------+-------------------------+------+-----+---------+-------+|Field|Type|Null|Key|Default|Extra|+-------------+-------------------------+------+-----+---------+-------+|Number|int(11)|YES||NULL|||name|varchar(255)|YES||NULL|||mailaddless|varchar(255)|YES||NULL|||class|enum('no1','no2','no3')|YES||NULL||+-------------+-------------------------+------+-----+---------+-------+4rowsinset(0.00sec)--classカラムの型のみをintからvarcharに変更mysql>altertablesample02modifyclassvarchar(255);QueryOK,3rowsaffected(0.03sec)Records:3Duplicates:0Warnings:0--結果の確認mysql>descsample02;+-------------+--------------+------+-----+---------+-------+|Field|Type|Null|Key|Default|Extra|+-------------+--------------+------+-----+---------+-------+|Number|int(11)|YES||NULL|||name|varchar(255)|YES||NULL|||mailaddless|varchar(255)|YES||NULL|||class|varchar(255)|YES||NULL||+-------------+--------------+------+-----+---------+-------+4rowsinset(0.00sec)--テーブルの中身を表示mysql>select*fromsample02;+--------+--------------+--------------------+-------+|Number|name|mailaddless|class|+--------+--------------+--------------------+-------+|1|田中さん|tanaka@yahoo.co.jp|no1||2|山田さん|yamada@gmail.com|no3||3|木村さん|kimura@yahoo.co.jp|no2|+--------+--------------+--------------------+-------+3rowsinset(0.00sec)--ageカラムを追加mysql>altertablesample02addageint;QueryOK,0rowsaffected(0.05sec)Records:0Duplicates:0Warnings:0--結果の確認mysql>select*fromsample02;+--------+--------------+--------------------+-------+------+|Number|name|mailaddless|class|age|+--------+--------------+--------------------+-------+------+|1|田中さん|tanaka@yahoo.co.jp|no1|NULL||2|山田さん|yamada@gmail.com|no3|NULL||3|木村さん|kimura@yahoo.co.jp|no2|NULL|+--------+--------------+--------------------+-------+------+3rowsinset(0.00sec)--classカラムの削除mysql>altertablesample02dropclass;QueryOK,0rowsaffected(0.05sec)Records:0Duplicates:0Warnings:0--結果の確認mysql>select*fromsample02;+--------+--------------+--------------------+------+|Number|name|mailaddless|age|+--------+--------------+--------------------+------+|1|田中さん|tanaka@yahoo.co.jp|NULL||2|山田さん|yamada@gmail.com|NULL||3|木村さん|kimura@yahoo.co.jp|NULL|+--------+--------------+--------------------+------+3rowsinset(0.00sec)

テーブル結合

  テーブル結合とは、作成してある既存のテーブル同士をつなぎ合わせて表示すること。

    
結合の種類コマンド/書き方意味/使い方
内部結合INNNER JOIN / JOINそれぞれのテーブルの指定した列の値が一致するデータだけを取得する。 nullとしてデータは取得しない。
外部結合左外部結合LEFT OUTER JOIN / LEFT JOINそれぞれのテーブルの指定した列の値が一致しない場合も、 nullとしてデータを取得。左のテーブル(selectの右に記述するテーブル)を基準にして結合。
右外部結合RIGHT OUTER JOIN / RIGHT JOINそれぞれのテーブルの指定した列の値が一致しない場合も、 nullとしてデータを取得。右のテーブル(joinの右に記述するテーブル)を基準にして結合。

内部結合(INNER JOIN)

  テーブル①とテーブル②を比較し、結合条件に合ったテーブル内の値を表示する
  SELECT カラム名 FROM テーブル名①
    INNER JOIN テーブル名② ON 結合の条件
  ※テーブル①の中で、結合の条件と合致したデータをテーブル②から取得する

左外部結合(LEFT OUTER JOIN)

  テーブル①を基準として、テーブル②から結合条件に合ったデータを取得し、テーブル①に結合して表示する
  SELECT カラム名 FROM テーブル名①
    LEFT OUTER JOIN テーブル名② ON 結合の条件
  ※テーブル①が基準となり、結合の条件と合致したデータをテーブル②から取得する

右外部結合(RIGHT OUTER JOIN)

  テーブル①を基準として、テーブル②から結合条件に合ったデータを取得し、テーブル①に結合して表示する
  SELECT カラム名 FROM テーブル名②
    LEFT OUTER JOIN テーブル名① ON 結合の条件
  ※テーブル①が基準となり、結合の条件と合致したデータをテーブル②から取得する

--fruitsデータベースの作成mysql>createdatabasefruits;QueryOK,1rowaffected(0.00sec)--fruitsデータベースの使用の宣言mysql>usefruits;Databasechanged--table_aテーブルの作成mysql>createtabletable_a(->fruit_aint,namevarchar(255),priceint)->;QueryOK,0rowsaffected(0.03sec)--table_aテーブルにデータを入力mysql>insertintotable_avalues->(1,"りんご",100),->(2,"みかん",150),->(3,"バナナ",140);QueryOK,3rowsaffected(0.01sec)Records:3Duplicates:0Warnings:0--table_aのデータの確認mysql>select*fromtable_a;+---------+-----------+-------+|fruit_a|name|price|+---------+-----------+-------+|1|りんご|100||2|みかん|150||3|バナナ|140|+---------+-----------+-------+3rowsinset(0.00sec)--table_bテーブルの作成mysql>createtabletable_b(->fruit_idint,placevarchar(255),stockint);QueryOK,0rowsaffected(0.03sec)----table_bテーブルにデータを入力mysql>insertintotable_bvalues->(1,"青森",5),->(2,"愛媛",30),->(3,"沖縄",20),->(4,"東京",50),->(5,"長野",10),->(6,"和歌山",25);QueryOK,6rowsaffected(0.00sec)Records:6Duplicates:0Warnings:0--table_bのデータの確認mysql>select*fromtable_b;+----------+-----------+-------+|fruit_id|place|stock|+----------+-----------+-------+|1|青森|5||2|愛媛|30||3|沖縄|20||4|東京|50||5|長野|10||6|和歌山|25|+----------+-----------+-------+6rowsinset(0.00sec)--table_cテーブルの作成mysql>createtabletable_c(->placevarchar(225),shopping_feeint->);QueryOK,0rowsaffected(0.02sec)--table_cにデータを入力mysql>insertintotable_cvalues->("青森",400),->("愛媛",400),->("沖縄",650),->("東京",250),->("長野",350),->("和歌山",350);QueryOK,6rowsaffected(0.01sec)Records:6Duplicates:0Warnings:0--table_cの構造の確認mysql>select*fromtable_c;+-----------+--------------+|place|shopping_fee|+-----------+--------------+|青森|400||愛媛|400||沖縄|650||東京|250||長野|350||和歌山|350|+-----------+--------------+6rowsinset(0.00sec)--table_aのfruit_aカラムをfruit_idに名称変更mysql>altertabletable_achangefruit_afruit_idint;QueryOK,0rowsaffected(0.03sec)Records:0Duplicates:0Warnings:0--内部結合mysql>select*fromtable_aINNERJOINtable_bontable_a.fruit_id=table_b.fruit_id;+----------+-----------+-------+----------+--------+-------+|fruit_id|name|price|fruit_id|place|stock|+----------+-----------+-------+----------+--------+-------+|1|りんご|100|1|青森|5||2|みかん|150|2|愛媛|30||3|バナナ|140|3|沖縄|20|+----------+-----------+-------+----------+--------+-------+3rowsinset(0.00sec)--table_bのfruit_idが5のものを1に変更mysql>updatetable_bsetfruit_id=1wherefruit_id=5;QueryOK,1rowaffected(0.00sec)Rowsmatched:1Changed:1Warnings:0--table_bの構造の確認(上記変更の確認)mysql>select*fromtable_b;+----------+-----------+-------+|fruit_id|place|stock|+----------+-----------+-------+|1|青森|5||2|愛媛|30||3|沖縄|20||4|東京|50||1|長野|10||6|和歌山|25|+----------+-----------+-------+6rowsinset(0.00sec)--table_bのfruit_idが6のものを2に変更mysql>updatetable_bsetfruit_id=2wherefruit_id=6;QueryOK,1rowaffected(0.01sec)Rowsmatched:1Changed:1Warnings:0--table_bの構造の確認(上記変更の確認)mysql>select*fromtable_b;+----------+-----------+-------+|fruit_id|place|stock|+----------+-----------+-------+|1|青森|5||2|愛媛|30||3|沖縄|20||4|東京|50||1|長野|10||2|和歌山|25|+----------+-----------+-------+6rowsinset(0.00sec)--内部結合mysql>select*fromtable_aINNERJOINtable_bontable_a.fruit_id=table_b.fruit_id;+----------+-----------+-------+----------+-----------+-------+|fruit_id|name|price|fruit_id|place|stock|+----------+-----------+-------+----------+-----------+-------+|1|りんご|100|1|青森|5||2|みかん|150|2|愛媛|30||3|バナナ|140|3|沖縄|20||1|りんご|100|1|長野|10||2|みかん|150|2|和歌山|25|+----------+-----------+-------+----------+-----------+-------+5rowsinset(0.00sec)--table_aの構造の確認mysql>select*fromtable_a;+----------+-----------+-------+|fruit_id|name|price|+----------+-----------+-------+|1|りんご|100||2|みかん|150||3|バナナ|140|+----------+-----------+-------+3rowsinset(0.01sec)--table_bの構造の確認mysql>select*fromtable_b;+----------+-----------+-------+|fruit_id|place|stock|+----------+-----------+-------+|1|青森|5||2|愛媛|30||3|沖縄|20||4|東京|50||1|長野|10||2|和歌山|25|+----------+-----------+-------+6rowsinset(0.00sec)--table_cの構造の確認mysql>select*fromtable_c;+-----------+--------------+|place|shopping_fee|+-----------+--------------+|青森|400||愛媛|400||沖縄|650||東京|250||長野|350||和歌山|350|+-----------+--------------+6rowsinset(0.00sec)--table_bのplaceカラムをplace_idに名称変更mysql>altertabletable_bchangeplaceplace_idvarchar(255);QueryOK,0rowsaffected(0.02sec)Records:0Duplicates:0Warnings:0--table_bの構造の確認(上記変更の確認)mysql>select*fromtable_b;+----------+-----------+-------+|fruit_id|place_id|stock|+----------+-----------+-------+|1|青森|5||2|愛媛|30||3|沖縄|20||4|東京|50||1|長野|10||2|和歌山|25|+----------+-----------+-------+6rowsinset(0.00sec)--内部結合mysql>select*fromtable_bINNERJOINtable_contable_b.place_id=table_c.place;+----------+-----------+-------+-----------+--------------+|fruit_id|place_id|stock|place|shopping_fee|+----------+-----------+-------+-----------+--------------+|1|青森|5|青森|400||2|愛媛|30|愛媛|400||3|沖縄|20|沖縄|650||4|東京|50|東京|250||1|長野|10|長野|350||2|和歌山|25|和歌山|350|+----------+-----------+-------+-----------+--------------+6rowsinset(0.00sec)--左外部結合mysql>SELECT->*fromtable_bleftouterjointable_aontable_a.fruit_id=table_b.fruit_id;+----------+-----------+-------+----------+-----------+-------+|fruit_id|place_id|stock|fruit_id|name|price|+----------+-----------+-------+----------+-----------+-------+|1|青森|5|1|りんご|100||1|長野|10|1|りんご|100||2|愛媛|30|2|みかん|150||2|和歌山|25|2|みかん|150||3|沖縄|20|3|バナナ|140||4|東京|50|NULL|NULL|NULL|+----------+-----------+-------+----------+-----------+-------+6rowsinset(0.00sec)--右外部結合mysql>select*fromtable_aRIGHTOUTERJOINtable_bontable_a.fruit_id=table_b.fruit_id;+----------+-----------+-------+----------+-----------+-------+|fruit_id|name|price|fruit_id|place_id|stock|+----------+-----------+-------+----------+-----------+-------+|1|りんご|100|1|青森|5||1|りんご|100|1|長野|10||2|みかん|150|2|愛媛|30||2|みかん|150|2|和歌山|25||3|バナナ|140|3|沖縄|20||NULL|NULL|NULL|4|東京|50|+----------+-----------+-------+----------+-----------+-------+6rowsinset(0.00sec)

フィールドオプション

 テーブル作成のオプション
 create tableを使う際に使用する主なオプション

●NOT NULL

 入力を必須とする

●AUTO_INCREMENT

 連続した数値を自動でカラムに格納する
 create table テーブル名 (カラム名 カラムのデータ型 AUTO_INCREMENT);

●PRIMARY KEY(主キー)

 PRIMARY KEYが指定されたカラムは、重複禁止のインデックスとして定義され、NOT NULL成約が自動的に付与される
 データベースのデータを一意に識別するための項目で、項目を主キーとして使うには
 ・中身が空でない
 ・中身がキー内で重複していない(一意である)
 必要がある

●UNIQUE 重複した値を登録できなくする

 UNIQUEが指定されたカラムは、重複禁止のインデックスとして定義され、NULLを許容する

●FOREGN KEY

 外部テーブルとリンクするためのキー
 他のテーブルのカラムを参照するキー
 参照先である他テーブルのカラムに登録されている値以外を登録できなくする

createtabletable1(idINT(11)PRYMARYKEYAUTO_INCREMENT,/*primary keyとauto_incrementの指定*/column1VARCAHR(255)NOTNULL,/*not nullの指定*/column2VARCHAR(255)UNIQIE,/*uniqueの指定*//*foregn keyの使い方*/FOREGNKEY(column1)参照先テーブル名REFERENCES参照テーブル名(カラム名));

Python数理最適化入門 中学数学の問題をpulpで解く

$
0
0

中学数学の問題をPythonで解いてみました。

問題
4X+3Y=9
2X-3Y=9

frompulpimport*#数理モデル作成
prob=LpProblem(sense=LpMinimize)#変数
x=LpVariable('x',cat='Continious')y=LpVariable('y',cat='Continious')#制約式の定義(問題の部分)
prob+=4*x+3*y==9prob+=2*x-3*y==9#求解
status=prob.solve()print(LpStatus[status])print('x:%.if y:%if'%(value(x),value(y)))

スクリーンショット 2019-12-18 16.36.44.png

はい。答えが出ました。塾とかでわからない人が出てくる中学数学の連立方程式もプログラミング使うと一瞬ですね。

次は文章題です。

1個120円のりんごと1個150円のみかんを合わせて22個買ったら。合計が2711円でした。りんごとみかんをそれぞれ何個買いましたか?

prob=LpProblem(sense=LpMinimize)#変数
x=LpVariable('x',cat='Continious')y=LpVariable('y',cat='Continious')#制約式の定義
prob+=120*x+150*y==2711prob+=x+y==22#求解
status=prob.solve()print(LpStatus[status])print('x:%.if y:%if'%(value(x),value(y)))

スクリーンショット 2019-12-18 16.37.12.png

はい答えが出ました。りんご19個、みかん2個ですね。こんなめんどくさい問題も一瞬で解けるのは便利ですね。

Javaユーザーがよく見るイベントサイトは? connpassとDoorkeeperで収集したデータをRevealで可視化してみた。

$
0
0

はじめに

JavaユーザはーTECH系イベントに参加するとき、どんなイベントサイトを参照しているのか?
有名なTECHイベントサイトの"connpass"と"Doorkeeper"のJavaコミュニティの数値取得し、Revealでダッシュボードを作ってみました。
(2019年12月の情報に基づいた記事です)

対象サイトを選定

有名なIT系イベントサイトをリストアップ

まずは、私も利用経験のあるイベントサイトをリストアップ。

  • connpass
  • Doorkeeper
  • ATND
  • TECH PLAY

最終的に2つ(connpass と Doorkeeper)に絞り込み

それぞれのサイトでJavaの情報を収集し、
そこそこのボリュームが得られたconnpassとDoorkeeperを対象にデータを収集することにしました。

データの収集対象

connpass

connpass はカテゴリ「Java」のグループに所属しているメンバー数の合計を取得。
image.png

Doorkeeper

Doorkeeper はトピック「Java」のコミュニティ所属しているメンバー数の合計を取得(※)
image.png

※ Doorkeeperはトピックのフォロワー数を知ることも出来ますが、connpassとなるべく近い集計手順を実施するために、コミュニティに所属しているメンバー数の合計としています。

収集結果

泥臭く収集した結果は、下記のExcelにまとめています。(疲れた...)
https://github.com/furugen/Reveal-Samples/raw/master/1218-RevealApp/CommunityData-Doorkeeper-connpass.xlsx

シンプルに3カラムを設けました。

  • サイト ... connpass / Doorkeeper
  • コミュニティ名 ... コミュニティの名称
  • コミュニティメンバー数 ... 参加メンバー数
  • image.png

Reveal でダッシュボード作成

出来たもの

image.png
image.png

ダッシュボードの作り方

別途動画とって公開します!(近日公開予定)

まとめ

Javaユーザーがテックイベントを探す場合は、Doorkeeperは必須!

Doorkeeperが圧倒的にJavaユーザ多し! その差、約8倍以上でした。
メンバー数上位コミュニティで1位の日本Javaユーザーグループ、2位の日本Springユーザ会がJavaコミュニティを引っ張っている結果でしょう。
※余談ですが昔所属していたJava Kueche(沖縄)が上位コミュニティにランクインしているのが嬉しい。

最近テックイベントはconnpassで探すことが多かったのすが、久々にJavaイベントを探そうとした際に、「あれ?Javaのイベントが少ない…?」と感じたのが調査のきっかけでした。
イベントサイトによっては、それぞれ特色があるので利用する方も気を付けないといけませんね。

本記事はJavaでしたが、.NET系(たぶんconnpassが強いはず)や、他の言語も機会があれば可視化していこうと思います。

次回もまたRevealの記事を執筆予定ですので、興味があればまたご覧ください!


DataikuのチュートリアルBasicsをやってみる

$
0
0

はじめに

Dataiku DSSをインストールし, いざ使おうと思ったときに一番最初に躓いたのはDataikuの説明がすべて英語であること. 頑張って英語を読んでチュートリアルを実施したので, その手順をまとめることにする.

実行環境はDataiku DSS 6.0.2

Dataiku Academy

https://academy.dataiku.com/latest/

Dataikuの説明書のようなものであるDataiku Academy. これを読めばDataikuの使い方をほぼマスターできる(ただし英語).
Dataiku Academyでは実際に動かしながら使い方を学べるチュートリアルが用意されている. 今回はチュートリアルの基礎の基礎, Tutorial: Basicsのやり方をまとめる.

Tutorial: Basics

https://academy.dataiku.com/latest/tutorial/basics/

このチュートリアルでは, Haiku T-Shirtという架空のオンラインのTシャツショップのデータを使ってDataikuの使い方を学んでいく.
今回はHaiku T-Shirtの注文履歴をDataikuに読み込み, データを大まかに見てそこから発見したルールを適用して新しいデータセットを作成するという手順を学ぶ.
ちなみに上のURLにアクセスすればDataikuの使い方を動画で見ることもできる.
以下のURLから今回使うデータ(orders.csv)をダウンロードしたらチュートリアルスタート.
https://downloads.dataiku.com/public/website-additional-assets/data/orders.csv

プロジェクトを作る

まず最初にDataikuのプロジェクトを作成する. このプロジェクトの中でデータの加工や分析を行っていく. 今回はチュートリアル用のプロジェクトが用意されているのでそれを使って進めていく.
まずDataikuにアクセスすると以下の画面になるはず.
toppage.png
この"Tutorial"というのをクリックすると以下のような画面が表示されるので"101: Basic(Tutorial)"をクリック. 「Create "DKU_TUTORIAL_BASICS"」という画面が出てきたらOKを押す.
project_select.png
プロジェクトを作るとよく以下のような画面が出てくるが, 画面の説明をしてくれているだけなので閉じるか"GOT IT"をクリックすればよい.
navigation_and_help.png

これでプロジェクトを作る工程は終了. 今回はTutorialのプロジェクトを使って進めるが, 自分のデータを読み込んで使いたいときは初めのページで"Blank project"を選べば良い.

プロジェクトとは

プロジェクトとはDataikuで...
時間があったら書く
参考
https://doc.dataiku.com/dss/latest/projects/index.html

データセットを作る(データの読み込み)

次に先ほどダウンロードしたデータセットをDataikuにインポートする. 画面の中央くらいにある, "+ IMPORT YOUR FIRST DATASET"をクリック.
project_top.png
以下の画面になったら一番左上の"Upload your files"を選択.
new_dataset.png
"ADD A FILE"をクリックし, 先ほどダウンロードしたorders.csvを選択するか, orders.csvを真ん中のエリアにドラッグ&ドロップすることでファイルをDataiku上にアップロードすることができる.
Upload_your_file.png
アップロードに成功すると以下のように画面の下部に"PREVIEW"というボタンが表示されるのでそれをクリック.
Upload_your_file_after.png
Dataikuは自動でCSVファイルを認識して表形式のデータにしてくれる. その際どのような表になるかをこの画面で確認する. 万が一自動で識別された結果が想定と違った場合には, ここで設定を変更し直すことができる. 特に日本語のデータを扱う場合は文字コードの設定などを行うことが多い. 今回は特に直すことはないのでデフォルトの状態でよい.
データの確認が終わったら, 右上の「New dataset name: 」というところでDataiku上で扱う際のデータセットの名前を付ける. 今回はファイル名でもある「orders」で問題ないのでここもデフォルトのまま右上の"CREATE"をクリック, あるいはCtrl+Sを押す. これでデータセットの作成は完了.
preview.png

データの調査(Exploreページの説明)

データセットの作成が完了すると以下の画面になる. このページはExploreページといい, 作成したデータセットを確認することができる. この画面からデータの調査を行っていく.
explore.png

サンプリング

ここはチュートリアルではあまり関係ないので読み飛ばしてもOK.

現在表示されているデータは実は読み込んだデータをすべて表示しているのではなく, データをサンプリング(一部を抽出)した状態で表示されている. これは, データが巨大であった場合にすべてを表示しているとPCのメモリを圧迫してしまうという問題に対応するための機能である.
デフォルトでは単純にデータの上から10000件を表示している. これだと表示は早いがデータが偏ってしまっている可能性もあるため, それが嫌な場合は設定を変更する必要がある.
まず, 少しわかりにくいが画面左側にパネルを開くためのボタンが存在する(以下の画像参照).

explore_panel_mark.png
すると以下のようにデータセットのサンプリング方法を設定する画面が表示される. 設定項目は上から

  • Sampling method $\cdots$ サンプリングの方法を設定. 上から順に, 下から順に,ランダムになど. ランダムの場合にはその方法も指定できる.
  • Nb. records $\cdots$ データを何件表示させるかを設定.
  • Filter records $\cdots$ チェックをつけると, 表示させるデータのルールを自分で設定できる.
  • Enforce max sample memory usage $\cdots$ チェックをつけると, データセットを表示させる際に使う最大メモリを設定できる. 設定したメモリを上回るデータセットは表示できなくなる.
  • Auto-refresh sample $\cdots$ チェックをつけると, データセットを変更するたびに自動で変更を適用してくれる. データセットが大きいと毎回処理が行われてしまい, 動作が重くなる. explore_panel_open.png

データの型と意味

表のそれぞれの列には, その列の名前とデータの型が表示されている. 黒色の文字で表示されている型は, 読み込んだ元データの型を示している. 今回はCSVファイルであるためすべてstring型になっている. 青色の文字で表示されているのは, Dataikuが自動で認識した意味である. IntegerやText, Decimalの他にも, DateやIP addresses, email, US statesというのも存在している. 残念ながら日本の地名は認識してもらえない.
ところで, 現在データの中に背景が赤くなっているセルがいくつかあるだろう. これは, Dataikuが認識したデータの意味と異なるデータが入っている場合にこのような表示になる. また, データの意味(青文字)のすぐ下にある緑や赤のバーは, その列のデータにどれだけ認識と違うデータが含まれているかを表している. 今回のデータには存在しないが, もしデータが入力されていないセルがあった場合は灰色で表現される.
data_meaning.png

このデータでは, customer_id列をIntegerと認識したのに, 「vft1eu」のようにテキストのデータが入っているので, そのようなセルが赤く表示されている. しかし, 今回においてはcustomer_idにテキストが入っているのも間違いではないため, 自分でデータの意味を選択する必要がある. 青文字の部分をクリックすると以下のようにデータの意味を選択できる画面が表示されるので, この中から"Text"を選択する.
data_meaning_select.png

すると, 以下のように意味がTextで固定され, セルは通常の色に, バーはすべて緑になる.
data_meaning_changed.png

可視化(Charts)

Dataikuではデータの可視化機能(Charts)も備わっている. 画面右上辺り(青矢印)の"Charts"をクリックすることで, 以下のような画面になる. 画面左側に表示されている各データ項目をドラッグ&ドロップするだけでデータの可視化が可能.
charts.png
今回は, Tシャツのタイプごとに注文されている数について可視化を行う.
画面左側のデータの列名リストの中から"tshirt_category"をXエリア(上画像緑枠)にドラッグ&ドロップ. 次に, リストから"Count of record"をYエリア(上画像赤枠)にドラッグ&ドロップ. すると以下の画像のようになる. ここでCount of recordという項目はデータに存在する列名ではなく, X軸に選択したデータの数を数える際に用いるCharts用の項目である.
charts_tshirts_histogram.png
このままだとX軸の順番がアルファベット順になっているので, もし降順(昇順)に変更したい場合はXエリアの"tshirt_category"横の▼を押し, 「Sorting」の"Count of records, descending"("Count of records, ascending")を選択することで変更可能.
charts_sorting.png
可視化した結果を見てみると, tshirt_categoryの値が統一されていないということがわかる. 例えば, 「Black」が「Bl」となっていたり, 「White」が「Wh」となっていたりなど. 次からはこれらの統一がされていない値についての対応を行っていく.

Visual Recipeを用いたデータの整形

ここでは, Dataiku DSSの重要な機能である, Visual Recipeを使用する.
Visual Recipeはデータの整形や加工を行うための機能で, 様々なことを行うことができる. Excelでいうと関数のようなものである. Dataiku DSSのVisual Recipeでは, データの整形の過程を記録することができるため, 後にデータを確認した際どのようなことを行ったかの確認をすることができる.

表記の統一

右上の"ACTIONS"ボタンをクリックし, 表示された項目の中から"Prepare"を選択する.
action_prepare.png
すると以下のような画面が表示される. この画面では整形を行いたいデータセットの選択(Input dataset)と整形後出力するデータセット(Output dataset)の名前の入力を行う.
今回はデフォルトで入力されている状態のままでよいので, このまま右下の"CREATE RECIPE"をクリックする.
prepare_new.png
すると, 以下のような画面になる. これがデータ整形を行うための画面である.
まず行いたいのは, tshirt_categoryのデータの表記の統一である. そのために, 列名"tshirt_category"をクリックする.
prepare.png
そして表示されたメニューから"Analyze..."を選択する.
prepare_tshirt_category.png
すると以下の画面が表示される. この画面では選択した列に関する情報が表示されている.
まず, 「White T-shirt M」と「Wh Tshirt M」に関する表記のというつを行う. 表示されているデータの中から, 「White T-shirt M」と「Wh Tshirt M」にチェックを入れる.
analyze_tshirt_category.png
その後, "MASS ACTIONS"ボタンをクリックし, 表示されたメニューの中から"Merge selected"を選択する.
analyze_merge.png
そして, 表示された"MERGE"ボタンをクリックすると, 表記の統一が完了する. この際, 左のテキストボックスで統一する際の値を指定できルが, 今回はデフォルトのままでよい.
analyze_merge2.png
これと同様の操作を「Black T-Shirt M」と「Bl Tshirt M」, 「White T-Shirt F」と「Wh Tshirt F」, 「Black T-Shirt F」と「Bl Tshirt F」に行うと最終的に以下のようにデータの種類が5つに統一される.
analyze_after_merge.png
Analyze画面を閉じると, 画面が以下のように変化している.
今回行った処理がscriptとして画面左側に表示され, そのscriptによって変更が行われたセルの背景が青色になっている.
prepare_after_merge.png

さらなるデータ加工

他のデータ加工についても, いくつか実行していく.
次に実行するのは日付の認識である. Dataiku DSSは日付のフォーマットを選択するとその通りの日付として認識し, 記述方法を変更することができる.
"order_date"をクリックし, 表示されたメニューから"Parse date..."を選択.
prepare_order_date.png
表示された以下のダイアログにおいて, データに適した日付のフォーマットを選択する.
今回は「年/月/日」となっているので"yyyy/MM/dd"を選択し, 右下のOKを押す.
smart_date.png
すると, order_dateの隣にorder_date_parsedという列が追加される. しかし, 今回は日付を表す列は一つでいいので, 日付の認識後の列を新しく追加するのではなく, もともとあった列に上書きするという出力にする. そのためには, 左側に表示されているscriptの「Output column」というテキストボックスに入力されているものを削除するだけでよい.
prepare_after_parse.png
prepare_after_parse2.png

これで日付の認識は終了.
次に行うデータの加工は, 注文ごとの合計金額の計算である.
今回用いているデータセットには, 注文されたTシャツの値段と(tshirt_price)そのTシャツを注文された数(tshirt_quantity)を示す列がある. しかし, その注文の合計金額を示す列がないためその列を追加する処理を行う.
まず, 画面左下の"+ ADD A NEW STEP"(上画像緑枠)をクリックする. 以下のようにProcesser(データ加工のプロセス)のリストが表示されるので, その中から"Formula"を選択する.
add_new_step.png
画面左側のscriptに新しくFormulaが追加されるので, そこを以下のように編集していく.

  • 「Output column」にtotalと入力.
  • 「Expression」にtshirt_price*tshirt_quantityと入力.

すると, 合計金額を示す列が追加される.
formula.png
もうTシャツの値段と数の列は必要がないのでtshirt_pricetshirt_quantityを削除する. それぞれ列名をクリックし, "Delete"を選択.
delete_column.png
これでデータの整形は終了.
最後に行うのは, この整形した結果を実際にデータ全体に適用することである.
ここで, 現在データ整形を行っている際に表示されているデータが一部をサンプリングしたものであることを思い出してほしい. 今回行った変更を全体に適用させるために, もう1ステップ必要である.
まず, 左下にある"RUN"ボタンをクリックする.
prepare_after_delete.png

すると以下のようなダイアログが表示される. これは, 今回出力するデータセットが変更されていることが問題ないかを確認されている. 基本的にこのダイアログは無視してかまわないので, チェックもつけたまま"UPDATE SCHEMA"をクリックする.
update_schema.png
データセットに対して処理が実行され, それが成功すれば下に「Job suceeded」という画面が表示される. これで, 変更を適用した新たなデータセットの出力が完了である. "Explore dataset orders_prepared"をクリックすることで出力したデータセットを見ることができる.

出力したデータセットの確認

出力したデータセットでまず注目してほしいのは, データの型の部分である. 前に確認した時にはすべてstring型だったが, 出力されたデータセットではその列のデータを認識した通りの型に変換されている. 例えばpages_visitedはbigint, totalはdoubleなど.
データを整形したり, 新たに列を追加したりしたので, それについての可視化も行っていく. 今回確認するのはどのTシャツの売り上げが一番高いのか. それをグラフで見るためにまたChartsを開く.
orders_prepared.png
Xエリアにtshirt_category, Yエリアにtotalをそれぞれドラッグ&ドロップをすることで, 以下のようなグラフを得ることができる.
charts_total_tshirt.png
このグラフを一見するとTennis Shirtの売り上げが一番多いように見えるが, 実はこの段階ではTシャツの種類ごとにtotalの平均をとってしまっているため, 純粋な売り上げの比較になっていない. これをTシャツごとの売り上げ比較ができるように変更するには, Yエリアに置いた"total"をクリックしメニューを開く. その中の「Aggregate」のドロップダウンを開き, SUMを選択.
charts_total_avg.png
すると, グラフがTシャツの種類ごとの売り上げのグラフに変更される(この時, X軸の方も降順表示になっていない場合は変更しておくとグラフが見やすくなる).
charts_total_sum.png

顧客ごとにグループ化

最終的にこのデータを用いて行いたいことはHaiku T-Shirtsの顧客について理解することである. このチュートリアルの最後に, 顧客ごとに過去の注文データをすべてまとめたデータを作成する.
画面右上の"ACTION"をクリックしてメニューを開き, "Group"を選択する.
action_group.png
表示されたダイアログにおいて「Group By」のプルダウンから, "customer_id"を選択する.
group_new.png
そして, 出力するデータセットの名前がデフォルトのままだと長すぎるため, 少し短く「orders_by_customer」に変更し, "CREATE RECIPE"をクリック.
group_new2.png
以下のような画面になるので,

  • order_date: Min
  • pages_visited: AVG
  • total: Sum

をそれぞれクリックしチェックを入れる.
その後"RUN"ボタンを押すことで, 顧客ごとにグループ化したデータセットが出力される.
group.png
出力されたデータセットは以下のようになる.
orders_by_customer.png

最後に, 左上のFLOWボタンをクリックすることで, 今までの処理を振り返ることができる.

これでこのチュートリアルは終了.
次のチュートリアルはまた.

Railsチュートリアル 第12章 パスワードの再設定 - 本番環境での動作に関する演習

$
0
0

1.production環境でユーザー登録を試してみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?

スクリーンショット 2019-12-17 12.38.59.png

まずは「Sign up」画面から、ユーザー登録に必要な情報を入力し、「Create my account」ボタンをクリックします。

スクリーンショット 2019-12-17 12.39.10.png

上記の画面が出て、入力したメールアドレスが有効ならば、実際にメールが到着しているはずです。確認してみましょう。

スクリーンショット 2019-12-17 12.42.29.png

確かにメールが到着していました。

2. メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。

ヒント: ターミナルからheroku logsコマンドを実行してみましょう。

スクリーンショット 2019-12-17 12.47.29.png

まずは、受信したメールの「Activate」リンクをクリックします。

スクリーンショット 2019-12-17 12.48.23.png

無事ユーザーが有効化されました。

ユーザー有効化処理時のサーバーログ

まずはUsersコントローラーのnewアクションに関するログです。新規ユーザー登録情報の入力画面が表示されるところまでです。

2019-12-17T03:38:26.701082+00:00 heroku[router]: at=info method=GET path="/signup" host=warm-woodland-62915.herokuapp.com request_id=9ef43fd8-9976-4b6c-9793-fde8257190ab fwd="103.5.140.188" dyno=web.1 connect=1ms service=28ms status=200 bytes=3428 protocol=https
2019-12-17T03:38:26.675341+00:00 app[web.1]: I, [2019-12-17T03:38:26.675221 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Started GET "/signup" for 103.5.140.188 at 2019-12-17 03:38:26 +0000
2019-12-17T03:38:26.676239+00:00 app[web.1]: I, [2019-12-17T03:38:26.676157 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Processing by UsersController#new as HTML
2019-12-17T03:38:26.677651+00:00 app[web.1]: I, [2019-12-17T03:38:26.677545 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendering users/new.html.erb within layouts/application
2019-12-17T03:38:26.678404+00:00 app[web.1]: I, [2019-12-17T03:38:26.678315 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered shared/_error_messages.html.erb (0.1ms)
2019-12-17T03:38:26.686611+00:00 app[web.1]: I, [2019-12-17T03:38:26.686532 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered users/_form.html.erb (8.5ms)
2019-12-17T03:38:26.686868+00:00 app[web.1]: I, [2019-12-17T03:38:26.686762 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered users/new.html.erb within layouts/application (9.1ms)
2019-12-17T03:38:26.688192+00:00 app[web.1]: I, [2019-12-17T03:38:26.688122 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered layouts/_rails_default.erb (0.9ms)
2019-12-17T03:38:26.688627+00:00 app[web.1]: I, [2019-12-17T03:38:26.688523 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered layouts/_shim.html.erb (0.1ms)
2019-12-17T03:38:26.689527+00:00 app[web.1]: I, [2019-12-17T03:38:26.689418 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered layouts/_header.html.erb (0.6ms)
2019-12-17T03:38:26.690255+00:00 app[web.1]: I, [2019-12-17T03:38:26.690186 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab]   Rendered layouts/_footer.html.erb (0.3ms)
2019-12-17T03:38:26.690776+00:00 app[web.1]: I, [2019-12-17T03:38:26.690656 #4]  INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Completed 200 OK in 14ms (Views: 13.3ms | ActiveRecord: 0.0ms)

続いて、Usersコントローラーのcreateアクションが開始されました。ユーザーの操作としては、「新規ユーザー登録情報の入力画面で、「Submit」ボタンがクリックされたところ」です。

2019-12-17T03:39:00.628200+00:00 app[web.1]: I, [2019-12-17T03:39:00.628068 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Started POST "/signup" for 103.5.140.188 at 2019-12-17 03:39:00 +0000
2019-12-17T03:39:00.629006+00:00 app[web.1]: I, [2019-12-17T03:39:00.628922 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Processing by UsersController#create as HTML
2019-12-17T03:39:00.629110+00:00 app[web.1]: I, [2019-12-17T03:39:00.629041 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   Parameters: {"utf8"=>"✓", "authenticity_token"=>"kPI+VonM8nuUu11tRyJNCgkw++6CAdlBZgdH7pgF3GwQjj4/Ah6f7x8yXoXNG4AwTYpDgE+jwtNJ13XXCZMh8w==", "user"=>{"name"=>"Hoge Hoge", "email"=>"[有効なメールアドレス]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"}
2019-12-17T03:39:00.714970+00:00 app[web.1]: D, [2019-12-17T03:39:00.714832 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]    (1.7ms)  BEGIN
2019-12-17T03:39:00.718390+00:00 app[web.1]: D, [2019-12-17T03:39:00.718293 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   User Exists (2.1ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "[有効なメールアドレス]"], ["LIMIT", 1]]
2019-12-17T03:39:00.807361+00:00 app[web.1]: D, [2019-12-17T03:39:00.807224 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   SQL (10.7ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["name", "Hoge Hoge"], ["email", "[有効なメールアドレス]"], ["created_at", "2019-12-17 03:39:00.718914"], ["updated_at", "2019-12-17 03:39:00.718914"], ["password_digest", "$2a$10$tLq83mKKaJN4XGBafxp0BOMpmTDw6iKYj729bNu9dE6jrGYu7w/o6"], ["activation_digest", "$2a$10$CKnDgcIkfShDxkQLli6OU.6vszmQ3rrXdSMa7khjybONYxxNWxNf2"]]
2019-12-17T03:39:00.811145+00:00 app[web.1]: D, [2019-12-17T03:39:00.811013 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]    (3.0ms)  COMMIT

有効化トークンに対応するダイジェストは以下の文字列ですね。

$2a$10$CKnDgcIkfShDxkQLli6OU.6vszmQ3rrXdSMa7khjybONYxxNWxNf2

続いて、メイラーに関するログが返されています。

2019-12-17T03:39:00.817822+00:00 app[web.1]: I, [2019-12-17T03:39:00.817728 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   Rendering user_mailer/account_activation.html.erb within layouts/mailer
2019-12-17T03:39:00.818892+00:00 app[web.1]: I, [2019-12-17T03:39:00.818826 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   Rendered user_mailer/account_activation.html.erb within layouts/mailer (0.9ms)
2019-12-17T03:39:00.819846+00:00 app[web.1]: I, [2019-12-17T03:39:00.819782 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   Rendering user_mailer/account_activation.text.erb within layouts/mailer
2019-12-17T03:39:00.820407+00:00 app[web.1]: I, [2019-12-17T03:39:00.820338 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa]   Rendered user_mailer/account_activation.text.erb within layouts/mailer (0.4ms)
2019-12-17T03:39:01.140739+00:00 app[web.1]: D, [2019-12-17T03:39:01.140608 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] UserMailer#account_activation: processed outbound mail in 328.5ms
2019-12-17T03:39:01.383306+00:00 app[web.1]: I, [2019-12-17T03:39:01.383191 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Sent mail to [有効なメールアドレス] (242.3ms)
2019-12-17T03:39:01.383340+00:00 app[web.1]: D, [2019-12-17T03:39:01.383285 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Date: Tue, 17 Dec 2019 03:39:01 +0000

続いて、ユーザー有効化URL通知用メールのヘッダーが返されます。

2019-12-17T03:39:01.383343+00:00 app[web.1]: From: noreply@example.com
2019-12-17T03:39:01.383345+00:00 app[web.1]: To: [有効なメールアドレス]
2019-12-17T03:39:01.383347+00:00 app[web.1]: Message-ID: <5df84dd5239a7_42b04df0df05c5129@b042e83a-3492-4249-911e-6b41b5e09cd5.mail>
2019-12-17T03:39:01.383349+00:00 app[web.1]: Subject: Account activation
2019-12-17T03:39:01.383351+00:00 app[web.1]: Mime-Version: 1.0
2019-12-17T03:39:01.383353+00:00 app[web.1]: Content-Type: multipart/alternative;
2019-12-17T03:39:01.383355+00:00 app[web.1]: boundary="--==_mimepart_5df84dd521d3b_42b04df0df05c50d3";
2019-12-17T03:39:01.383358+00:00 app[web.1]: charset=UTF-8
2019-12-17T03:39:01.383360+00:00 app[web.1]: Content-Transfer-Encoding: 7bit
2019-12-17T03:39:01.383362+00:00 app[web.1]: 
2019-12-17T03:39:01.383364+00:00 app[web.1]: 
2019-12-17T03:39:01.383366+00:00 app[web.1]: ----==_mimepart_5df84dd521d3b_42b04df0df05c50d3

以下のログには、テキストメールの本文が返されています。

2019-12-17T03:39:01.383368+00:00 app[web.1]: Content-Type: text/plain;
2019-12-17T03:39:01.383370+00:00 app[web.1]: charset=UTF-8
2019-12-17T03:39:01.383372+00:00 app[web.1]: Content-Transfer-Encoding: 7bit
2019-12-17T03:39:01.383374+00:00 app[web.1]: 
2019-12-17T03:39:01.383376+00:00 app[web.1]: Hi Hoge Hoge,
2019-12-17T03:39:01.383377+00:00 app[web.1]: 
2019-12-17T03:39:01.383380+00:00 app[web.1]: Welcome to the Sample App! Click on the link below to activate your account:
2019-12-17T03:39:01.383382+00:00 app[web.1]: 
2019-12-17T03:39:01.383384+00:00 app[web.1]: https://warm-woodland-62915.herokuapp.com/account_activations/iNFYhM1X7rDUmVORFrGMbA/edit?email=[有効なメールアドレス]
2019-12-17T03:39:01.383386+00:00 app[web.1]: 
2019-12-17T03:39:01.383387+00:00 app[web.1]: 
2019-12-17T03:39:01.383390+00:00 app[web.1]: ----==_mimepart_5df84dd521d3b_42b04df0df05c50d3

以下のログには、HTMLメールの本文が返されています。

2019-12-17T03:39:01.383392+00:00 app[web.1]: Content-Type: text/html;
2019-12-17T03:39:01.383393+00:00 app[web.1]: charset=UTF-8
2019-12-17T03:39:01.383395+00:00 app[web.1]: Content-Transfer-Encoding: 7bit
2019-12-17T03:39:01.383397+00:00 app[web.1]: 
2019-12-17T03:39:01.383402+00:00 app[web.1]: <!DOCTYPE html>
2019-12-17T03:39:01.383404+00:00 app[web.1]: <html>
2019-12-17T03:39:01.383406+00:00 app[web.1]: <head>
2019-12-17T03:39:01.383409+00:00 app[web.1]: <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
2019-12-17T03:39:01.383410+00:00 app[web.1]: <style>
2019-12-17T03:39:01.383412+00:00 app[web.1]: /* Email styles need to be inline */
2019-12-17T03:39:01.383414+00:00 app[web.1]: </style>
2019-12-17T03:39:01.383416+00:00 app[web.1]: </head>
2019-12-17T03:39:01.383418+00:00 app[web.1]: 
2019-12-17T03:39:01.383420+00:00 app[web.1]: <body>
2019-12-17T03:39:01.383422+00:00 app[web.1]: <h1>Sample App</h1>
2019-12-17T03:39:01.383424+00:00 app[web.1]: 
2019-12-17T03:39:01.383426+00:00 app[web.1]: <p>Hi Hoge Hoge,</p>
2019-12-17T03:39:01.383428+00:00 app[web.1]: 
2019-12-17T03:39:01.383430+00:00 app[web.1]: <p>
2019-12-17T03:39:01.383432+00:00 app[web.1]: Welcome to the Sample App! Click on the link below to activate your account:
2019-12-17T03:39:01.383434+00:00 app[web.1]: </p>
2019-12-17T03:39:01.383435+00:00 app[web.1]: 
2019-12-17T03:39:01.383438+00:00 app[web.1]: <a href="https://warm-woodland-62915.herokuapp.com/account_activations/iNFYhM1X7rDUmVORFrGMbA/edit?email=[有効なメールアドレス]">Activate</a>
2019-12-17T03:39:01.383440+00:00 app[web.1]: 
2019-12-17T03:39:01.383441+00:00 app[web.1]: </body>
2019-12-17T03:39:01.383443+00:00 app[web.1]: </html>
2019-12-17T03:39:01.383445+00:00 app[web.1]: 
2019-12-17T03:39:01.383447+00:00 app[web.1]: ----==_mimepart_5df84dd521d3b_42b04df0df05c50d3--

メール本文から、有効化トークンは以下の文字列であることがわかります。

iNFYhM1X7rDUmVORFrGMbA

続いてのログにより、Usersコントローラーのcreateアクションの最後は / へのリダイレクトで完了したことがわかります。

2019-12-17T03:39:01.383918+00:00 app[web.1]: I, [2019-12-17T03:39:01.383828 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Redirected to https://warm-woodland-62915.herokuapp.com/
2019-12-17T03:39:01.384190+00:00 app[web.1]: I, [2019-12-17T03:39:01.384101 #4]  INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Completed 302 Found in 752ms (ActiveRecord: 17.6ms)

今度は、AccountActivationsコントローラーに対するeditが開始されたところからのログです。「メール本文中に記載されたURLにアクセスがあった」というタイミングですね。

2019-12-17T03:48:03.998174+00:00 app[web.1]: I, [2019-12-17T03:48:03.998081 #4]  INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Started GET "/account_activations/iNFYhM1X7rDUmVORFrGMbA/edit?email=[有効なメールアドレス]" for 103.5.140.188 at 2019-12-17 03:48:03 +0000
2019-12-17T03:48:03.999604+00:00 app[web.1]: I, [2019-12-17T03:48:03.999504 #4]  INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Processing by AccountActivationsController#edit as HTML
2019-12-17T03:48:03.999666+00:00 app[web.1]: I, [2019-12-17T03:48:03.999597 #4]  INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5]   Parameters: {"email"=>"[有効なメールアドレス]", "id"=>"iNFYhM1X7rDUmVORFrGMbA"}
2019-12-17T03:48:04.013441+00:00 app[web.1]: D, [2019-12-17T03:48:04.013350 #4] DEBUG -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5]   User Load (4.0ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[有効なメールアドレス]"], ["LIMIT", 1]]
2019-12-17T03:48:04.101912+00:00 app[web.1]: D, [2019-12-17T03:48:04.101770 #4] DEBUG -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5]   SQL (12.9ms)  UPDATE "users" SET "activated" = 't', "activated_at" = '2019-12-17 03:48:04.087544' WHERE "users"."id" = $1  [["id", 104]]
2019-12-17T03:48:04.102898+00:00 app[web.1]: I, [2019-12-17T03:48:04.102788 #4]  INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Redirected to https://warm-woodland-62915.herokuapp.com/users/104
2019-12-17T03:48:04.103242+00:00 app[web.1]: I, [2019-12-17T03:48:04.103156 #4]  INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Completed 302 Found in 103ms (ActiveRecord: 18.7ms)

(再掲)以下のログのSQL文を見るに、ユーザーの有効化処理は正常に完了したようです。

2019-12-17T03:48:04.101912+00:00 app[web.1]: D, [2019-12-17T03:48:04.101770 #4] DEBUG -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5]   SQL (12.9ms)  UPDATE "users" SET "activated" = 't', "activated_at" = '2019-12-17 03:48:04.087544' WHERE "users"."id" = $1  [["id", 104]]

3. アカウントを有効化できたら、今度はパスワードの再設定を試してみましょう。正しくパスワードの再設定ができたでしょうか?

まず、ログインフォームから「forgot password」のリンクをクリックします。

スクリーンショット 2019-12-17 18.38.02.png

パスワード再設定の対象となるユーザーのメールアドレスを入力します。

スクリーンショット 2019-12-17 18.28.09.png

メールが送られてきます。ハイライトした部分は再設定用トークンです。

リンクをクリックして、パスワード再設定フォームへ進みます。

スクリーンショット 2019-12-17 18.28.37.png

新しいパスワードを入力します。

スクリーンショット 2019-12-17 18.29.10.png

パスワードの再設定が完了し、パスワードを再設定したユーザーのプロフィールページが表示されます。

スクリーンショット 2019-12-17 18.29.19.png

パスワード再設定に関するサーバーログ

まずはPasswordResetsコントローラーのnewアクションに関するログです。

2019-12-17T09:27:37.630786+00:00 app[web.1]: I, [2019-12-17T09:27:37.630687 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Started GET "/password_resets/new" for 175.177.6.7 at 2019-12-17 09:27:37 +0000
2019-12-17T09:27:37.632157+00:00 app[web.1]: I, [2019-12-17T09:27:37.632064 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Processing by PasswordResetsController#new as HTML
2019-12-17T09:27:37.634071+00:00 app[web.1]: I, [2019-12-17T09:27:37.633999 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243]   Rendering password_resets/new.html.erb within layouts/application
2019-12-17T09:27:37.635383+00:00 app[web.1]: I, [2019-12-17T09:27:37.635314 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243]   Rendered password_resets/new.html.erb within layouts/application (1.2ms)
2019-12-17T09:27:37.635971+00:00 app[web.1]: I, [2019-12-17T09:27:37.635907 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243]   Rendered layouts/_rails_default.erb (0.4ms)
2019-12-17T09:27:37.636126+00:00 app[web.1]: I, [2019-12-17T09:27:37.636073 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243]   Rendered layouts/_shim.html.erb (0.0ms)
2019-12-17T09:27:37.636548+00:00 app[web.1]: I, [2019-12-17T09:27:37.636470 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243]   Rendered layouts/_header.html.erb (0.2ms)
2019-12-17T09:27:37.636862+00:00 app[web.1]: I, [2019-12-17T09:27:37.636799 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243]   Rendered layouts/_footer.html.erb (0.1ms)
2019-12-17T09:27:37.637075+00:00 app[web.1]: I, [2019-12-17T09:27:37.637018 #4]  INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Completed 200 OK in 5ms (Views: 3.7ms | ActiveRecord: 0.0ms)

ここまでのログは、「forgot password」リンクのクリックから、パスワード再設定対象のメールアドレスの入力画面がWebブラウザに表示されるところまでに対応しています。

続いて、PasswordResetsコントローラーのcreateアクションが開始されました。ユーザーの操作としては、「パスワード再設定対象のメールアドレスの入力画面で、「Submit」ボタンがクリックされたところ」です。

2019-12-17T09:28:10.677268+00:00 app[web.1]: I, [2019-12-17T09:28:10.677144 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Started POST "/password_resets" for 175.177.6.7 at 2019-12-17 09:28:10 +0000
2019-12-17T09:28:10.678356+00:00 app[web.1]: I, [2019-12-17T09:28:10.678278 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Processing by PasswordResetsController#create as HTML
2019-12-17T09:28:10.678449+00:00 app[web.1]: I, [2019-12-17T09:28:10.678378 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   Parameters: {"utf8"=>"✓", "authenticity_token"=>"s6/JcwQzuM6JAiREt9u1PDuPaKc5we/TAWuJbaIJyxcyV3rusF2/H0ksby8hky03XDCj8W0j/jxFcmxVT61haQ==", "password_reset"=>"[FILTERED]", "commit"=>"Submit"}
2019-12-17T09:28:10.686514+00:00 app[web.1]: D, [2019-12-17T09:28:10.686418 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   User Load (2.8ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[有効なメールアドレス]"], ["LIMIT", 1]]
2019-12-17T09:28:10.766666+00:00 app[web.1]: D, [2019-12-17T09:28:10.766571 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   SQL (3.7ms)  UPDATE "users" SET "reset_digest" = '$2a$10$uOXG8lKmc/1ytMzVC5QHAOiY0S/GjmMYHYMb7lVd.dNK8vIsQFYtG', "reset_sent_at" = '2019-12-17 09:28:10.762063' WHERE "users"."id" = $1  [["id", 104]]

(再掲)パスワード再設定トークンに対応するダイジェストをRDBに保存する処理には、以下のログが対応しています。

2019-12-17T09:28:10.766666+00:00 app[web.1]: D, [2019-12-17T09:28:10.766571 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   SQL (3.7ms)  UPDATE "users" SET "reset_digest" = '$2a$10$uOXG8lKmc/1ytMzVC5QHAOiY0S/GjmMYHYMb7lVd.dNK8vIsQFYtG', "reset_sent_at" = '2019-12-17 09:28:10.762063' WHERE "users"."id" = $1  [["id", 104]]

ダイジェストは以下の文字列ですね。

$2a$10$uOXG8lKmc/1ytMzVC5QHAOiY0S/GjmMYHYMb7lVd.dNK8vIsQFYtG

続いて、メイラーに関するログが返されています。

2019-12-17T09:28:10.771901+00:00 app[web.1]: I, [2019-12-17T09:28:10.771829 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   Rendering user_mailer/password_reset.html.erb within layouts/mailer
2019-12-17T09:28:10.772669+00:00 app[web.1]: I, [2019-12-17T09:28:10.772606 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   Rendered user_mailer/password_reset.html.erb within layouts/mailer (0.7ms)
2019-12-17T09:28:10.773513+00:00 app[web.1]: I, [2019-12-17T09:28:10.773447 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   Rendering user_mailer/password_reset.text.erb within layouts/mailer
2019-12-17T09:28:10.773958+00:00 app[web.1]: I, [2019-12-17T09:28:10.773897 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1]   Rendered user_mailer/password_reset.text.erb within layouts/mailer (0.4ms)
2019-12-17T09:28:10.987227+00:00 app[web.1]: D, [2019-12-17T09:28:10.987125 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] UserMailer#password_reset: processed outbound mail in 219.8ms
2019-12-17T09:28:11.369313+00:00 heroku[router]: at=info method=POST path="/password_resets" host=warm-woodland-62915.herokuapp.com request_id=3885b893-52a2-4eed-bc9d-d14c153f57e1 fwd="175.177.6.7" dyno=web.1 connect=1ms service=692ms status=302 bytes=1049 protocol=https
2019-12-17T09:28:11.363917+00:00 app[web.1]: I, [2019-12-17T09:28:11.363795 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Sent mail to [有効なメールアドレス] (376.4ms)
2019-12-17T09:28:11.364042+00:00 app[web.1]: D, [2019-12-17T09:28:11.363974 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Date: Tue, 17 Dec 2019 09:28:10 +0000

続いて、パスワード再設定URL通知用メールのヘッダーが返されます。

2019-12-17T09:28:11.364046+00:00 app[web.1]: From: noreply@example.com
2019-12-17T09:28:11.364048+00:00 app[web.1]: To: [有効なメールアドレス]
2019-12-17T09:28:11.364051+00:00 app[web.1]: Message-ID: <5df89faaf1d68_42b033a8e4c6c36478@0a287fc0-3c85-4948-82ec-158df909485c.mail>
2019-12-17T09:28:11.364053+00:00 app[web.1]: Subject: Password reset
2019-12-17T09:28:11.364055+00:00 app[web.1]: Mime-Version: 1.0
2019-12-17T09:28:11.364057+00:00 app[web.1]: Content-Type: multipart/alternative;
2019-12-17T09:28:11.364059+00:00 app[web.1]: boundary="--==_mimepart_5df89faaf0b60_42b033a8e4c6c36371";
2019-12-17T09:28:11.364062+00:00 app[web.1]: charset=UTF-8
2019-12-17T09:28:11.364064+00:00 app[web.1]: Content-Transfer-Encoding: 7bit

以下のログには、テキストメールの本文が返されています。

2019-12-17T09:28:11.364070+00:00 app[web.1]: ----==_mimepart_5df89faaf0b60_42b033a8e4c6c36371
2019-12-17T09:28:11.364072+00:00 app[web.1]: Content-Type: text/plain;
2019-12-17T09:28:11.364074+00:00 app[web.1]: charset=UTF-8
2019-12-17T09:28:11.364077+00:00 app[web.1]: Content-Transfer-Encoding: 7bit
2019-12-17T09:28:11.364078+00:00 app[web.1]: 
2019-12-17T09:28:11.364081+00:00 app[web.1]: To reset your password click the link below:
2019-12-17T09:28:11.364082+00:00 app[web.1]: 
2019-12-17T09:28:11.364085+00:00 app[web.1]: https://warm-woodland-62915.herokuapp.com/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス]
2019-12-17T09:28:11.364087+00:00 app[web.1]: 
2019-12-17T09:28:11.364089+00:00 app[web.1]: This will expire in two hours.
2019-12-17T09:28:11.364091+00:00 app[web.1]: 
2019-12-17T09:28:11.364093+00:00 app[web.1]: If you did not request your password to be reset, please ignore this email and
2019-12-17T09:28:11.364095+00:00 app[web.1]: your password stay as it is.
2019-12-17T09:28:11.364097+00:00 app[web.1]: 
2019-12-17T09:28:11.364099+00:00 app[web.1]: 
2019-12-17T09:28:11.364101+00:00 app[web.1]: ----==_mimepart_5df89faaf0b60_42b033a8e4c6c36371

以下のログには、HTMLメールの本文が返されています。

2019-12-17T09:28:11.364103+00:00 app[web.1]: Content-Type: text/html;
2019-12-17T09:28:11.364105+00:00 app[web.1]: charset=UTF-8
2019-12-17T09:28:11.364107+00:00 app[web.1]: Content-Transfer-Encoding: 7bit
2019-12-17T09:28:11.364109+00:00 app[web.1]: 
2019-12-17T09:28:11.364110+00:00 app[web.1]: <!DOCTYPE html>
2019-12-17T09:28:11.364112+00:00 app[web.1]: <html>
2019-12-17T09:28:11.364115+00:00 app[web.1]: <head>
2019-12-17T09:28:11.364117+00:00 app[web.1]: <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
2019-12-17T09:28:11.364119+00:00 app[web.1]: <style>
2019-12-17T09:28:11.364121+00:00 app[web.1]: /* Email styles need to be inline */
2019-12-17T09:28:11.364123+00:00 app[web.1]: </style>
2019-12-17T09:28:11.364125+00:00 app[web.1]: </head>
2019-12-17T09:28:11.364128+00:00 app[web.1]: 
2019-12-17T09:28:11.364130+00:00 app[web.1]: <body>
2019-12-17T09:28:11.364132+00:00 app[web.1]: <h1>Password reset</h1>
2019-12-17T09:28:11.364133+00:00 app[web.1]: 
2019-12-17T09:28:11.364135+00:00 app[web.1]: <p>To reset your password click the link below:</p>
2019-12-17T09:28:11.364137+00:00 app[web.1]: 
2019-12-17T09:28:11.364140+00:00 app[web.1]: https://warm-woodland-62915.herokuapp.com/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス]
2019-12-17T09:28:11.364141+00:00 app[web.1]: 
2019-12-17T09:28:11.364143+00:00 app[web.1]: <p>This will expire in two hours.</p>
2019-12-17T09:28:11.364145+00:00 app[web.1]: 
2019-12-17T09:28:11.364147+00:00 app[web.1]: <p>
2019-12-17T09:28:11.364150+00:00 app[web.1]: If you did not request your password to be reset, please ignore this email and
2019-12-17T09:28:11.364152+00:00 app[web.1]: your password stay as it is.
2019-12-17T09:28:11.364153+00:00 app[web.1]: </p>
2019-12-17T09:28:11.364155+00:00 app[web.1]: 
2019-12-17T09:28:11.364157+00:00 app[web.1]: </body>
2019-12-17T09:28:11.364159+00:00 app[web.1]: </html>
2019-12-17T09:28:11.364161+00:00 app[web.1]: 
2019-12-17T09:28:11.364164+00:00 app[web.1]: ----==_mimepart_5df89faaf0b60_42b033a8e4c6c36371--

メール本文から、パスワード再設定トークンは以下の文字列であることがわかります。

itlJPL32OY-XMHORblVHpQ

続いてのログにより、PasswordResetsコントローラーのcreateアクションの最後は / へのリダイレクトで完了したことがわかります。

2019-12-17T09:28:11.364629+00:00 app[web.1]: I, [2019-12-17T09:28:11.364565 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Redirected to https://warm-woodland-62915.herokuapp.com/
2019-12-17T09:28:11.364841+00:00 app[web.1]: I, [2019-12-17T09:28:11.364781 #4]  INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Completed 302 Found in 686ms (ActiveRecord: 6.5ms)

今度は、PasswordResetsコントローラーに対するeditが開始されたところからのログです。「メール本文中に記載されたURLにアクセスがあった」というタイミングですね。

2019-12-17T09:28:40.413354+00:00 heroku[router]: at=info method=GET path="/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス]" host=warm-woodland-62915.herokuapp.com request_id=ed1e4c85-a8ad-4e30-90a9-2d581a582d69 fwd="175.177.6.7" dyno=web.1 connect=1ms service=98ms status=200 bytes=3413 protocol=https
2019-12-17T09:28:40.314045+00:00 app[web.1]: I, [2019-12-17T09:28:40.313937 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Started GET "/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス]" for 175.177.6.7 at 2019-12-17 09:28:40 +0000
2019-12-17T09:28:40.314979+00:00 app[web.1]: I, [2019-12-17T09:28:40.314889 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Processing by PasswordResetsController#edit as HTML
2019-12-17T09:28:40.315025+00:00 app[web.1]: I, [2019-12-17T09:28:40.314972 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Parameters: {"email"=>"[有効なメールアドレス]", "id"=>"itlJPL32OY-XMHORblVHpQ"}
2019-12-17T09:28:40.319097+00:00 app[web.1]: D, [2019-12-17T09:28:40.319022 #4] DEBUG -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   User Load (1.7ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[有効なメールアドレス]"], ["LIMIT", 1]]
2019-12-17T09:28:40.398272+00:00 app[web.1]: I, [2019-12-17T09:28:40.398149 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendering password_resets/edit.html.erb within layouts/application
2019-12-17T09:28:40.403019+00:00 app[web.1]: I, [2019-12-17T09:28:40.402925 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendered shared/_error_messages.html.erb (0.7ms)
2019-12-17T09:28:40.405414+00:00 app[web.1]: I, [2019-12-17T09:28:40.405331 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendered password_resets/edit.html.erb within layouts/application (7.0ms)
2019-12-17T09:28:40.406458+00:00 app[web.1]: I, [2019-12-17T09:28:40.406378 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendered layouts/_rails_default.erb (0.7ms)
2019-12-17T09:28:40.406920+00:00 app[web.1]: I, [2019-12-17T09:28:40.406841 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendered layouts/_shim.html.erb (0.0ms)
2019-12-17T09:28:40.407613+00:00 app[web.1]: I, [2019-12-17T09:28:40.407535 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendered layouts/_header.html.erb (0.4ms)
2019-12-17T09:28:40.408136+00:00 app[web.1]: I, [2019-12-17T09:28:40.408058 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69]   Rendered layouts/_footer.html.erb (0.2ms)
2019-12-17T09:28:40.408613+00:00 app[web.1]: I, [2019-12-17T09:28:40.408534 #4]  INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Completed 200 OK in 93ms (Views: 10.7ms | ActiveRecord: 1.7ms)

ここまでのログは、パスワード再設定画面がWebブラウザに表示されるところまでに対応しています。

続いて、PasswordResetsコントローラーのupdateアクションが開始されました。ユーザーの操作としては、「パスワード再設定画面で、「Submit」ボタンがクリックされたところ」です。

2019-12-17T09:29:12.595676+00:00 app[web.1]: I, [2019-12-17T09:29:12.595538 #4]  INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Started PATCH "/password_resets/itlJPL32OY-XMHORblVHpQ" for 175.177.6.7 at 2019-12-17 09:29:12 +0000
2019-12-17T09:29:12.596922+00:00 app[web.1]: I, [2019-12-17T09:29:12.596829 #4]  INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Processing by PasswordResetsController#update as HTML
2019-12-17T09:29:12.597042+00:00 app[web.1]: I, [2019-12-17T09:29:12.596951 #4]  INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]   Parameters: {"utf8"=>"✓", "authenticity_token"=>"dg4e+oRJwi5xsE/Jz4Pmq+2UoA2OiDh5+ocmuTpFoVb2ch6TD5uvuvo5TCFFuiuRqS4YY0MqI+vVVxSAq9NcyQ==", "email"=>"[有効なメールアドレス]]", "user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Update password", "id"=>"itlJPL32OY-XMHORblVHpQ"}
2019-12-17T09:29:12.601036+00:00 app[web.1]: D, [2019-12-17T09:29:12.600952 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]   User Load (1.6ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "[有効なメールアドレス]"], ["LIMIT", 1]]
2019-12-17T09:29:12.677061+00:00 app[web.1]: D, [2019-12-17T09:29:12.676951 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]    (1.4ms)  BEGIN
2019-12-17T09:29:12.756980+00:00 app[web.1]: D, [2019-12-17T09:29:12.756856 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]   User Exists (1.6ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) AND ("users"."id" != $2) LIMIT $3  [["email", "[有効なメールアドレス]"], ["id", 104], ["LIMIT", 1]]
2019-12-17T09:29:12.759699+00:00 app[web.1]: D, [2019-12-17T09:29:12.759627 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]   SQL (1.4ms)  UPDATE "users" SET "password_digest" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["password_digest", "$2a$10$H0x3ghcOyu0l4DHat9NqLOsJ0.xRYZ.DDKTXvoaO18Ot7E5p/omzq"], ["updated_at", "2019-12-17 09:29:12.757425"], ["id", 104]]
2019-12-17T09:29:12.762463+00:00 app[web.1]: D, [2019-12-17T09:29:12.762374 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]    (2.4ms)  COMMIT
2019-12-17T09:29:12.763828+00:00 app[web.1]: D, [2019-12-17T09:29:12.763760 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]    (1.1ms)  BEGIN
2019-12-17T09:29:12.765871+00:00 app[web.1]: D, [2019-12-17T09:29:12.765779 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]   SQL (1.3ms)  UPDATE "users" SET "reset_digest" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["reset_digest", nil], ["updated_at", "2019-12-17 09:29:12.763908"], ["id", 104]]
2019-12-17T09:29:12.768224+00:00 app[web.1]: D, [2019-12-17T09:29:12.768124 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe]    (2.1ms)  COMMIT
2019-12-17T09:29:12.768664+00:00 app[web.1]: I, [2019-12-17T09:29:12.768603 #4]  INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Redirected to https://warm-woodland-62915.herokuapp.com/users/104
2019-12-17T09:29:12.768800+00:00 app[web.1]: I, [2019-12-17T09:29:12.768749 #4]  INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Completed 302 Found in 172ms (ActiveRecord: 15.9ms)

パスワードの再設定が正常に完了していますね。最後は当該ユーザーのプロフィールページにリダイレクトされています。

Pythonで入力された数字が素数か判定するプログラム

$
0
0

はじめに

私がPythonを勉強して一通り基本となる構文を学んだところで作った、「入力された整数nが素数なのか合成数なのか判定するプログラム」です。自分の日記的な意味合いも兼ねて書いています。
まだまだ至らない点が多いと思いますが、アドバイスなどが有りましたらよろしくおねがいします。

プログラムの原理

今回の素数の判定方法は「入力された n に対して、n の約数が√nまでの素数に無ければ素数である。」という事実に基づいて行いました。
詳しい証明は以下をご覧ください。
自然数nが√n以下のすべての素数で割り切れなければ,nは素数であることの証明

実際のプログラム

import math
from sympy import primerange
n = int(input("素数か確かめたい数を入力してください。>> "))
num = int(math.sqrt(n)) + 1
primlist = list(primerange(2,num)) #1

for i in primlist: #2
    if n % i == 0: #3
        print("合成数です。")
        break
    elif i == primlist[-1] :
        print("素数です。")

大まかな流れ

  1. 素数リストを√nまでの数字で作成する。
  2. nを素数リストで割ってみる。
  3. 割り切れたら「合成数です。」と表示する。素数リストの最後まで行ったら「素数です。」と表示する。

実際やっていること

1.は#1で、SymPyのprimerangeを用いることにより、素数リストを作成しました。
2.は#2で、for構文を用いて素数リストから文字を取り出しています。
3.は#3で、if構文を用いて「n を素数で割った余りが0か否か」で判定しています。

反省点

初めて素数判定を作りましたが、動くものができて個人的に満足しています。
しかし、n が大きくなると(7桁を超える)と途端に計算速度が遅くなってしまうので、なんとか改善したいです。
また、よく考えたらmathを使う必要がなかったので修正したいと思います。初めての自作プログラムなので今回は記念に残しています。
このプログラムを用いれば素因数分解もできそうな気がしてきたので手を加えてみたいとおもいます。
最後まで読んでくださいまして有難うございました。

PHPでお問い合わせフォームを作る

$
0
0

PHPでお問い合わせフォームを作ってみよう

はいはいはいはい、つい最近までオブジェクト指向理解していなかったオイラです。
そんな人でも作れるお問い合わせフォームをPHPで作ってみましょう。

全体の流れ

  1. 入力画面
  2. 確認画面
  3. 完了画面

てな感じやね

じゃあ、もうちょっと細かくみてみようか

入力画面

うん、これがないと始まらないね。
今回はお決まりの以下の入力項目にしてみようか
項目1 お名前
項目2 メールアドレス
項目3 お問い合わせ内容

お、お、ぉぅ 最低限の設定やね。。今後拡張していく予定やから我慢して。。。。

確認画面

そう、ここで入力した内容を表示して「送っていいんか?」って確認させるんよね。
ちなみにこの画面遷移の文化は日本独自らしいよ。
海外(範囲が雑なのは突っ込まないで)では入力フォームから完了画面までいくんだって

完了画面

まぁ、要するに「ありがとうございました」ページやね
ここでメール送信とかデータベースに接続とかいろいろあるんやけど、とりあえずはメール送信しとこうか

仕様確定

簡単にザ〜〜っと書いたけど作ろうとしているシステムの仕様は確定したね
が〜〜〜っと 説明すると、入力させて、確認させて、メール送信する仕組み

細かいとこ

「細いとこ」?? 「詳細設計」のこと ??? こんなのに詳細設計なんて必要か? ガッて作ればいいやん
でも、おじさんは過去に、そして今でもこれがないために苦しんだ事があるので一応書いておきます。
ただ、本記事はcssは使いません。
あくまで 'PHP' という言語を使ってデータを引き渡し、コミットするというところにフォーカスしています。

入力画面

このhtmlでやってみようか
'''
<!DOCTYPE html>



PHPでメール送信するよ





はじめてのハッカソン ~Yahoo! HackDay 2019~

$
0
0

はじめに

今回Yahoo! HackDayに会社の同僚と参加しました。

【メンバー】 計2名
私 :趣味でpython3、JavaScriptそれぞれ約1年くらい触った程度、サービス開発未経験
タカ:プログラミング未経験、ホームページビルダーなら触ったことあるよ!とのこと

おそらく今回のYahoo! HackDay 2019に参加したチームで、一番伸びしろがあったチームだったと思います。開発力なんて皆無なんだよォ!貧弱!貧弱ゥ!

はじめてのハッカソン、行く前に不安でどんな感じなのか自分で調べました。
が、開発未経験でいきなりハッカソンに出ようとした方はなかなかおらず
開発のレベル差を感じ、余計に不安になってしまいました。

これからハッカソンに参加しようと思っていて、同じような不安を感じている方の参考になれば幸いです!

参加経緯

もともとハッカソンには参加してみたいと思いながら、まだ自分の技術じゃなにも出来ないかなと思っていました。

会社で昼飯を食べているときに、ハッカソンの話をタカさんにしたら
HackDay 2019にチーム応募していました。

「当選したよ!メンバーはチケット購入必要らしいからよろしく!」

というメールで決心がつき、参加に至っています。

当初参加メンバーは5名ほど声がかかっていました。

他メンバーが出張予定等で段々と参加できなくなり、結果2人での参加に。
あきらめずに参加した自分をほめてあげることとします。

イベント時系列

 時系列
12/14   12:00会場到着
~14:00市場調査
~17:00企画練り直し、開発スタート
~21:00Web自動検索
12/15 ~5:00OCR機能導入
 ~6:00メール送信機能実装
 ~11:30Webサーバー構築検討
12:00開発終了
発表

HackDay前日まで

事前にどのようなプロダクトを作ろうか打ち合わせをしました。
方向性は

  • だれもが持っているスマートフォンで気軽に面白いことが出来るようにする
  • 被写体が動くのではなく、カメラ側を動かす(スマホをアクションカメラにする)

プログラミングの技術では絶対勝てないので、着想の目新しさでチャレンジしたいというものです。

会場到着、市場調査

会場は秋葉原UDX、参加チーム数は78チーム
すでに各チームいろいろな機材を持ち込んだりしていて、周りをみてワクワクしながらソワソワしてました。
会場の通信環境設定だったりをして、人生初めてのハッカソンスタート!HackTime!!

当社想定にあったのは、スマホをゆっくり落下させられるパラシュート的なものが作れないか、といもの。

スマホの加速度センサー Z軸方向数値の安定をトリガーにして、自然落下頂点から
バースト撮影を開始するようという部分にプログラミング要素を組み込もうと考えていました。。

パラシュート機構の部材・参考を探しにドンキ、ヨドバシ、ビックカメラを回ってみて2人は気づきました。
「アクションカメラの基本機能に投げて撮るってやつあるんだね~、しかも頑張ったら買えちゃう値段じゃん?」
すでにあるものに面白味を感じなくなり、昼飯を食べながら企画の練り直しをすることが決定しました。

この時すでに14:00、全体の1/12の時間を消費して振り出しに戻りました。

企画練り直し

事前に話して出てきたアイディアをもう一度出していきます。
大枠でのキーワード、方向性として、①スマホのカメラ機能を使って、②便利なことがしたい、というのは間違っていないということ。

情報科学とは異なる分野の技術屋さんに刺さるプロダクトを開発したい!ということで
行きついたのがこちら↓
molcali.png

分子量計算機 -MOLecular weight CALIculator-
略して「モルカリ」です。

考えた全体構造は下記の通り↓
概念図.png

全体像が決まり、
(1) スマホで情報を得たい化学式の写真を撮る
(2) OCRでテキスト情報に変換、Webから自動検索
(3) 得られた情報をスマホにフィードバック

まず3つのセグメントに分けて、順番に開発を進めていきます。

① OCRによるテキスト認識
② Web自動検索
③ メール自動送信

②、③に関しては、pythonで組んだことのある項目だったので
基本的なライブラリの組み合わせでたぶん作れるじゃん!作ろう!と思いました。

①はやったこともなく、どんなものが使えるかも分からなかったので
ひとまず情報収集をしてもらうことにしました。

なので、ひとまずそれぞれの役割分担としては、
私 :②、③部分のコーディング(使用言語:python)
タカ:OCRによる文字認識方法の調査

として開発スタートです。

Web自動検索

OCRから化学式がtext形式で得られたとして、そこから化学組成、沸点、融点などの情報を取得することが目標です。
今回はpython3のseleniumを使用しました。
Googleで検索しても、html構造が整っていて情報がまとまっているソースに辿りつけなかったため
Wikipediaをスクレイピング対象として、検討していきました。

① OCRで得られたテキスト値を検索窓に入力
② ページ遷移
③ 対象ページからtdタグ要素取得

・・・the 力技です。

td要素で得たい化学特性の日本語表現が微妙に違うとうまくヒットしませんでした。
(化学式と組成式 等)
正答率 50%くらい。

たぶん正規表現とかで、検索うまくかければうまくヒットするんだと思います。

webSerch.py
#Web自動検索
importsysimporttimefromseleniumimportwebdriverMetadataset="hogehoge"#ここにOCRで得たテキスト値を入力
#ChromeをHeadlessモードで起動
options=Options()options.add_argument('--headless')#Chromeのパスを指定
# ※Chromedriverが無いとseleniumuは動かない
# 下記パスにあるchromedriver.exeをディレクトリごとコピー
Cpath=r"C:\python\chromedriver\chromedriver.exe"#Chromeを起動
browser=webdriver.Chrome(Cpath,chrome_options=options)browser.get('https://ja.wikipedia.org/w/index.php?search=&title=%E7%89%B9%E5%88%A5%3A%E6%A4%9C%E7%B4%A2&go=%E8%A1%A8%E7%A4%BA')#ページが開くまで待つ
time.sleep(0.5)#検索入力
time.sleep(0.1)inputBox=browser.find_element_by_id('ooui-php-1')#htmlで要素idが'ooui-php-1'の要素を取得(検索ボックスの場所取得)
inputBox.send_keys(Metadataset)#取得したInputBoxの場所に変数:Metadatasetの値を入力
link_el=browser.find_element_by_class_name('oo-ui-actionFieldLayout-button')link_el.click()#検索ページでの選択
time.sleep(0.1)link_el=browser.find_elements_by_xpath("//div/div/div/div/ul/li/div/a")print(link_el[0])link_el[0].click()#化学式ページ閲覧
time.sleep(0.5)title=browser.find_elements_by_css_selector("h1")value=browser.find_elements_by_tag_name('td')targetText=[]#リストtargetTextにvalueをリストとして格納
#リストに格納するのは、表示項目を指定するため('化学式'の次の項目を拾う 等)
foriinvalue:addtext=i.text#i個目の要素をaddtextとする
targetText.append(addtext)#物質名
print('物質名:'+title[0].text)#ほんとは例外処理が必要(インデックスで見つからなかったとき)
#化学式
formulaNmb=targetText.index('化学式')formula=targetText[formulaNmb+1]print('化学式:'+formula)#モル質量
moler=targetText[formulaNmb+3]#化学式の下にモル質量が書いてあったので横着した
print('モル質量'+moler)#沸点
boilingPointNmb=targetText.index('沸点')boilingPoint=targetText[boilingPointNmb+1]print('沸点:'+boilingPoint)#融点
meltingPointNmb=targetText.index('融点')meltingPoint=targetText[meltingPointNmb+1]print("融点:"+meltingPoint)

試しに入力値指定して走らせる

Metadataset="NaOH"

出力結果

物質名:水酸化ナトリウム
化学式:NaOH
モル質量39.992509329 g mol−1
沸点:1388 °C, 1661 K, 2530 °F
融点:318 °C, 591 K, 604 °F

例外処理とかも組み込まないといけませんでしたが、他にも課題山積みなので
現状どこまで出来ているか打ち合わせを含め、夕食を食べに行きました。

OCR機能導入 -Cloud Vision API-

ここから未経験の領域に突入です。

まずは以下の記事を参考に、pythonとTesseract OCRを試してみます。

PythonとTesseract OCRで文字認識
https://qiita.com/henjiganai/items/7a5e871f652b32b41a18

まずは記事通りに画像を読み取らせて、出力したことを確認
これはいけるかもしれない!と思った矢先、写真を取り込むと文字認識しない。

コントラストの問題かな?と思い、前段で2値化変換をかけるようにプログラム改修して再チャレンジ
がっ・・・駄目っ・・・

この時点でかなり頭は沸騰しかけていました。

ここでOCR手法についていろいろ調べてもらっていたタカさんが、Cloud Vision APIを使った
OCR文字認識に辿りついていました。

■Google Cloud Visionを使ってみた
https://www.itbook.info/web/2016/11/google-cloud-vision%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F.html

先人たちがいろいろまとめてくれていて非常に助かります。

プログラミングや情報技術に初めて触れるタカさんは
APIキーってなに?JSON?pathを通すってどうやるの?という感じでした。
1年前の自分もそういう気持ちだったので非常に分かります。

pythonからAPIリクエストして返ってきたJSONデータを表示するサンプルコードを
タカさんにコピペしてもらい、APIキーなどを書き換えてもらいました。
人生初めてのプログラミングです。

無事に文字認識が成功、文章が返ってきてタカさんは感動していました。
エラーを潰しながらプログラムがちゃんと動いたときは感動しますよね。

さて、返ってきたJSONデータから得たい情報を抜き出します。
最初は返ってきた文章から化学式(アルファベットと数字の羅列)を抜き出すように考えました。
どう頭をひねっても文字長さが決まっていない文字列の抜き出し方が出てきませんでした。

写真の読み取り範囲を限定させるだけの技術はない...
いろいろ悩み、2人で現在どの程度出来ていて、残り何をしないといけないか打ち合わせました。
そうだ!偶然化学式だけ書かれた紙が手元にあることにしよう!

この時午前3:00、pptで化学式だけ書かれた紙を作成し
コンビニに印刷しに行きます。
外の寒さで眠気も若干緩和されました。

OCRで得たテキストデータを変数格納、Web検索して返すところまでひとまず完成しました。

メール自動送信

ここは以前組んだことのあるコードそのままに、自分のGmail宛に
web検索で得た情報を格納し、送付するものを作りました。

これでインプットからアウトプットまで一通りの流れが完成しました。

しかし、ここでタカさんの発言に戦慄を覚えます。

タカ「これってスマホ側で写真を撮ったら、自動的にプログラム動かすこととか出来るの?」

私 「Webサーバーとアプリケーションサーバー立てればやれるんじゃないすかねぇ、詳しくは知らないですけど」

タカ「やろうよ!」

・・・ん?

Webメールサーバー構築検討

地獄の時間がはじまりました。

ここにきて環境を準備するところからスタートすることに。
ここからのことは正直よく覚えていません。

タカさんにプレゼンの資料準備をお願いし、色々検討しました。

果たしてクライアントサーバー型の方がいいのか
それともスマホ側だけで完結できるアプリを開発できるのか
暗中模索を続け、疲労もピークでうまく頭が回っていませんでした。

結果的には時間内に終わらず、今後の検討課題となりました。

開発終了

12:00に開発終了、最初のプレゼングループはプレゼン会場に移動となります。

他の参加グループが作ったプロダクトはどれも面白くて素晴らしかったです。
90秒のプレゼンは1発勝負のこわさがありますね。

参加した感想

とても苦しくて楽しかった
一番の収穫は自分の限界を引き上げられた感覚が得られたことです。

苦しみながらなにかを生み出すことを、1日に凝縮して楽しむことが出来たと感じます。

まさにハック+マラソン

終わった直後は早く帰って寝たいと思っていましたが、この記事を書いているとまた来年も挑戦したい気持ちになってきます。

途中特に大事だなと感じたのが、時間を区切ってお互いに考えをすり合わせる時間を作ること
今出来ていることと今からやらなければならないことを都度メンバーの共通認識として持つことが出来たのは大きかったと思います。

つくるってたのしいね

【Vue.js】タブ切り替えが出来る単一ファイルコンポーネントサンプル

$
0
0

はじめに

Vue.jsでタブ切り替えが出来る単一ファイルコンポーネントのサンプルを作成してみました。

v-for
v-bind
v-show
Vue.jsの基本機能である上記3点を活用しています。

スクリーンショット 2019-12-18 23.02.42.png

環境

OS:macOS Catalina 10.15.1Vue:2.6.10vue-cli:4.1.1

サンプル確認用リンク

こちらから動きの確認ができます。
JSfiddle(外部サイト)へのリンク

※動きの確認用のため、若干下記ソースコードと書き換えているところがあります。

ソースコード

Tab.vue
<template><divclass="tabs"><divclass="btn-container"><buttonv-for="(tab, index) in tabs":key="tab.id":class="{ active: currentTab === index }"
              @click="currentTab = index">{{tab.tabName}}</button></div><divclass="tab-content"><divv-show="currentTab === 0"><h1>Tab1 content</h1><p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p></div><divv-show="currentTab === 1"><h1>Tab2 content</h1><p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p></div><divv-show="currentTab === 2"><h1>Tab3 content</h1><p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p></div></div></div></template><script>exportdefault{name:'tabs',data(){return{currentTab:0,id:0,tabName:"",tabs:[{id:1,tabName:'Tab1'},{id:2,tabName:'Tab2'},{id:3,tabName:'Tab3'}],}}}</script><stylelang="scss"scoped>.tabs{margin:10pxauto;padding:10px;width:80%;height:80%;background:rgb(242,242,242);border-radius:10px;}h1p{font-family:'Courier New',Courier,monospace;}h1{font-size:20px;}p{font-size:8px;}.btn-container{display:flex;justify-content:center;}button{width:20%;font-size:20px;text-align:center;margin:10px;padding:3px10px;background:rgb(186,214,238);border-radius:10px;}button.active{background:lightcoral;}.tab-contentdiv{padding:30px;background:#ffffff;width:80%;margin:0auto;}</style>

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

コンポーネント単位で作成出来ると、機能が同じならスタイルを変えて流用出来ていいですね。
やっとVue.jsの良さを実感出来てきました:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

リストレンダリング — Vue.js

生成した複数件のランダムな文字列において重複が発生しない確率は□%

$
0
0

はじめに

2019新卒 エンジニア Advent Calendar 2019 19 日目の記事です。

ソフトウエア開発において、ランダムな文字列を生成することがあります。例えば、アカウントの識別子(ID)という役割で利用されます。具体例としてTwitterでは、ユーザがアカウントを作成すると、長さ15のランダムな文字列をIDとして付与します。しかし、このような役割があるがゆえに、ランダムな文字列は重複してはいけません。それゆえ、万が一重複が発生したときに実行する相応の処理などもあるかと思います。

新卒として入社して以降、このような重複のないランダムな文字列を必要とするテストデータを作成する機会がありました。その時の興味として抱いていた、そもそもどれだけの確率で重複が発生するのか(上記、それ相応の処理が実行されることはあるのか)を明らかにしたいと思います。

※タイトルはトリビアの種のオマージュです。懐かしい番組ですね。🧠

実験

計算式

結果は以下の計算式で算出できます。

P = \prod_{i = 0}^{n} \frac{(c^l)-k}{c^l}

上記に示した計算式の各変数は以下の通りです。

  • c ・・・ランダムな文字列を構成する文字の種類
  • n ・・・ランダムな文字列の件数
  • l ・・・ランダムな文字列の長さ
  • P ・・・重複が発生しない確率

実数による確率の算出

今回の実験におけるランダムな文字列は、数字と英字(小文字)の合計36種の文字から構成されるものと定義します。(例:長さ10の場合、yttrj4ca8n)この場合、以下の計算式によって重複が発生しない確率が算出できます。

P = \prod_{i = 0}^{n} \frac{(36^l)-k}{36^l}

また今回の実験における他変数の値は以下とします。

  • n ・・・100,10000,1000000
  • l ・・・5,10,15

はじめに、以下の実数を当てはめます。Pythonのプログラムで計算します。

n = 100, 
l = 5
c=36n=100l=5P=1forkinrange(n):P=P*((c**l)-k)/(c**l)print(P)
0.999918139356006

重複が発生しない確率は99%以上ですね。

まとめて算出し、比較してみます。

  • 行:文字数
  • 列:件数
51015
1000.9999181393560060.99999999999864611.0
100000.43741561330806360.99999998632581141.0
10000001.5e-3220.99986325392530561.0

文字数10や15の場合ほとんど重複は発生しないようです。特に15は自分の環境におけるpythonによる計算ではわからない確率なんですね。

おわりに

ソフトウエア開発において、生成することもあろうランダムな文字列の生成について、重複が発生しない確率を算出しました。文字数が5だと少々怪しいですが、10や15の場合、100万件のランダムな文字列を精製したとしても、重複はほとんど発生しないことがわかりました。しかし、念には念をということで対応する処理の準備は必要ですね。

しかし、ここで強運を使ってしまうことだけは避けたいな…


電子工作初心者がESP32からFirebase RealtimeDatabaseにデータを投げるまで

$
0
0

この記事は、大阪工業大学 Advent Calendar 2019の19日目の記事です。

:自己紹介

自分は最近やっとアウトプットをしようかなと思い始めた大学2回生です。twitterやってます。最近セキュリティ系の勉強を始めたばかりの初心者です。まぁ今回はセキュリティは全く関係ないのですが... なので、いろいろ表現に誤りがあるとは思いますがそこは何卒ご容赦を。

:はじめに

自分は自宅でブルーベリーを栽培してるのですが、それをIoTにしようと思い始めました。まったくの初心者だったため何もかもググりまくって完成させました。その時自分が欲しい情報が少なかった(※見つけられなかった)ので少しでもこれから始める人が躓かないように記事にすることにしました。

:材料

ESP32-DevKitC ESP-WROOM-32開発ボード
Arduino用 土壌湿度センサー Soil Moisture Sensor
・ブレッドボードなど小物
・arduino IDE

:ESP32でアナログセンサーから読みよとる

最初にesp32は俗にいうarduino系のため開発もほとんどarduinoと同じようにできる。しかし一部ESP32特有のものがあるので気を付けなければならない。今回のアナログセンサーからの読み取りではarduinoと同じコードで読みとれるが、デジタルセンサーではarduinoと同じではできないので気を付けないといけない。

analogsensor.ino
voidsetup(){Serial.begin(115200);while(!Serial);}voidloop(){Serial.println("sensor value");intvalue=analogRead(35);Serial.println(value);delay(1000);}

まぁ見てわかる通りloop関数内の"analogRead()"で読みとることができる。そして初心者の僕が詰まったのはどのpinにさせばいいんだ!コード内にさらっと出てきた"35"ってだれだよ!ググれば出てくるんだがマイコンにはもちろん各pinに役割があってその中に"ADC"というのがあって、それに対応してるpinに刺せばいいらしい。詳しくはここのサイトを見ればESP32の仕様が大体わかるはずです。

:ESP32をWiFiにつなげる

先ほどセンサーからデータを読みとれたので次はデータを投げるためにまず、ネットにつなげる必要があります。そのために必要な”WiFi.h"をインクルードする必要があります。WiFiにつなげるだけならとても簡単な以下のコードでできます。

WiFiconnect.ino
#include <WiFi.h>
constchar*ssid="ssid";constchar*password="password";voidsetup(){Serial.begin(115200);while(!Serial);WiFi.begin(ssid,password);while(WiFi.status()!=WL_CONNECTED){delay(500);//再接続待機Serial.print(".");}Serial.println("WiFi connected");Serial.print("IP address");Serial.println(WiFi.localIP());}voidloop(){}

なんとWiFiにつなげるの"WiFi.begin(ssid, password)のたった一行で済みます。"WiFi.status()で接続状態を取得することができます

:実際にFirebase RealtimeDatabaseに投げる

Firebase RealtimeDatabaseを作成するところはほかのもっとわかりやすく書いてある記事があると思うのでそれを参照してください。
DBを作ったうえで今回必要な値は
・DBにアクセスするためのパス
・BDにアクセスするための認証キー(作った人だけ)
の二つだけです。
ここで覚えておきたいのはFirebase RealtimeDatabaseはDatabaseというよりディクショナリに近いイメージを持っておいてください
そこを踏まえたうえで、自分はブルーベリーの湿度管理なので時間を名前にしますが、そこはご自由に

esp32_FBDB.ino
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <String.h>
#define JST 3600* 9
WiFiClientSecureclient;constchar*ssid="ssid name";constchar*password="ssid password";constchar*server="DB host";constchar*firebase_auth="DB secret key";// firebase's secretStringuser_path="table directory";uint32_tWebGet_LastTime=0;structtmtimeInfo;Stringyear;Stringmon;Stringday;voidsetup(){Serial.begin(115200);while(!Serial);WiFi_conect();configTime(JST,0,"ntp.nict.jp","ntp.jst.mfeed.ad.jp");//set NTPWebGet_LastTime=200000;}voidloop(){Stringnow_time=create_time();//get timeintvalue=analogRead(35);//get humiditySendPatchRequest(now_time,String(value));//send the datadelay(28000);//every 30 seconds}//WiFiconnect functionvoidWiFi_conect(){WiFi.begin(ssid,password);//connect to WiFiwhile(WiFi.status()!=WL_CONNECTED){Serial.print(".");delay(1000);// reconect 1 sencond later}Serial.println("WiFi connected");Serial.println("IP address");Serial.println(WiFi.localIP());//esp32's localIPaddress}//get timevoidcreate_time(){Stringnow_time;getLocalTime(&timeInfo);//get timeyear=String(timeInfo.tm_year+1900);//convert to A.D.mon=String(timeInfo.tm_mon+1);//convert to real monthday=String(timeInfo.tm_mday);if(int(timeInfo.tm_hour)/10==0){//add "0" when the hour is single digitStringnow_hour="0"+String(timeInfo.tm_hour);now_time+=now_hour;}else{Stringnow_hour=String(timeInfo.tm_hour);now_time+=now_hour;}if(int(timeInfo.tm_min)/10==0){//add "0" when the min is single digitStringnow_min="0"+String(timeInfo.tm_min);now_time+=now_min;}else{Stringnow_min=String(timeInfo.tm_min);now_time+=now_min;}if(int(timeInfo.tm_sec)/10==0){//add "0" when the sec is single digitStringnow_sec="0"+String(timeInfo.tm_sec);now_time+=now_sec;}else{Stringnow_sec=String(timeInfo.tm_sec);now_time+=now_sec;}returnnow_time;}//write firebase DBvoidSendPatchRequest(Stringstr1,Stringstr2){if(!client.connect(server,443)){//connect DBSerial.println("Connection Failed!");}else{Serial.println("Connected to sucsess!");StringURL;URL="PATCH /";//http request methodURL+=user_path+"/"+year+"/"+mon+"/"+day;URL+=".json?auth=";URL+=String(firebase_auth);URL+=" HTTP/1.1\r\n";Stringbody="{\""+str1+"\":\""+str2+"\"}";Stringhead;head="Host: ";head+=String(server)+"\r\n";head+="Connection: keep-alive\r\n";head+="Content-Length: ";head+=String(body.length())+"\r\n";head+="\r\n";client.print(URL);client.print(head);client.print(body);Serial.println("####### Send HTTP Patch Request #######");Serial.print(URL);Serial.print(head);Serial.print(body);checkServerRespons();delay(2);client.stop();delay(2);}}//receive responsevoidcheckServerRespons(){Serial.println("####### Firebase server HTTP Response #######");while(client.connected()){Stringres=client.readStringUntil('\n');Serial.println(res);if(res=="\r"){Serial.println("-------------- Response Headers Recived");break;}}while(client.available()){charc=client.read();Serial.print(c);}Serial.println("\r\n------------ Body Received");}

サーバーにつなげるのに"WiFiClientSecure.h"をインクルードする必要があります。
自分が一番躓いたのがhttp requestを作るとこでした。""で囲った部分が可変です。
・中身
"method" "URL" HTTP/1.1\r\n
HOST: "distination server" Content-Length "body.length" \r\n
"body"

データをDBに投げるだけでいいのでHTTP request のメソッドはPATCHで十分です。
・GET  ほしいものを取得
・POST  データの挿入
・PATCH データの更新

URLはDBまでのpathを入れれば大丈夫です。
HOSTは今回はFirebaseのserverを入れてやります。

今回はレスポンスを考慮してないので特に書いてないのですが、処理があるならcheckServerRespons()に処理を書く必要があります。

:おわりに

継ぎ接ぎだらけのコードなのでググったらもっといいコードが出てくると思うのでこの記事ではこんな感じのコードを書けばいいんだなと思っていただけたら嬉しいです。今回はアナログセンサーを用いましたが、デジタルセンサー(bme280)でデータを読むとこまでですがコードもありますので参考までにどうぞ

VBAでセルの値に文字列を追加しつつクリップボードに放り込むツールを作ってみた

$
0
0

まえがき

前回記事のあとがきで書いた通りクリップボード扱いたいということでクリップボードを扱えるdataObjectをほんの少し触りました。前回記事は下記リンクをご参照下さい。
前回記事(VBAで文字列、数値をSQLのIN句に入れる形にする)

コード

文字列型変数に選択範囲の値を突っ込みクリップボードに放り込む!以上!

Copy.bas
SubString_Copy()DimstrHolderAsStringDimintNumAsIntegerDimdataObjectAsNewdataObjectstrHolder="'"&Selection(1).Value&"'"ForintNum=2ToSelection.CountStep1strHolder=strHolder&vbCrLf&",'"&Selection(intNum).Value&"'"Next'データオブジェクトに文字列をセットdataObject.SetTextstrHolder  'データオブジェクトにセットされた値をクリップボードへ格納dataObject.PutInClipboardEndSub

dataObjectを扱うためには参照設定が必要

エディタの上部から、ツール>参照設定から「Microsoft Forms 2.0 Object Library」を選択します。見つからなければ参照設定>参照から「C:\Windows\System32\FM20.DLL」を選択します。

クリップボード周りを触れるdataObject

今回はSetText,PutInClipboardを利用しましたが、他にも

  • GetFormat(クリップボード内のデータ形式を判別する時に)
  • GetText,GetFromClipboard(今回とは逆にクリップボード内からデータを取得する時に)
  • StartDrag(マウスのドラッグ操作を行いたい時に)
  • Clear(クリップボードを空にしたい時に)

というメソッドがあります。
ただ、調査時にちらと見かけたのはSetTextすると文字化けするというものです…
リンク(外部サイト)
私は遭遇したことはありませんが、注意は必要かもしれません。

あとがき

簡単に実装できるにしては結構役立ってます。業務作業によって形は変わると思いますが、Excelのデータを定型的に成形する場合に活用できると思います。
周りにもシェアして割と使ってもらえているようなので作った甲斐はあったのかなと思っています。
上記リンクを参考にもう少し知識深めながら弄っていこうかなと思っています。
もっと便利で合理的でシンプルなプロセスがあればご教示いただきたく存じます。
以上、宜しくお願い致します。

一念発起してYahoo!JapanTechConference2019 in Shibuyaに参加してきた

$
0
0

2019/12/13(金)に行われた、Yahoo!Japan(以後Yahoo)が主催するカンファレンスに参加してきました。

カンファレンスってレベルの高い人とかセッションだらけというイメージがあり、気後れして結構スルーしてきた過去があるのですが、
今回たまたま開催のお知らせが目に留まり、一念発起して仕事を(半ば無理に)休んで行ってきました。

せっかくだし写真いっぱい撮るぞ!と意気込んで参加したものの、セッション聞き入ったりツイッターで実況したりですっかり写真の事を失念してしまいました。
なので、写真を見て雰囲気を掴みたいという方にはあまり面白くないかもしれません。

会場の写真はYahoo公式のTech Blogにも掲載されておりますので、合わせてご覧下さい!

基調講演

基調講演ではCTOの藤門さんが登壇されました。
内容としては、

  • 藤門さんがYahooのCTOになってから5年間で、どの様な取り組みをしてきたか
  • これからYahooが目指すビジョン

のお話が中心でした。
基調講演についてはYoutubeにアップされています。(前述したBlog内にYoutubeへのリンクあり)

個人的に印象に残った点


以下、内容を一部抜粋した箇条書きです。

  • うまくいかない3つのレガシーを、それぞれモダナイゼーションしていった。(画像1枚目)

    • マインドのモダナイゼーション
      • 会社のトップ自らが人の前に立ちメッセージを伝える
        • トップダウンで伝言ゲームの様に伝えるだけでは真意は伝わらない
      • 自分ごとにして取り組む
        • 本気で世界を変えていくマインドを持つ
      • 困難なことにチャレンジする
        • チャレンジへの応援策:社内LT開催やイベント登壇の促進、技術補助費月1万、副業OK、海外イベントへの旅費も出す
    • テクノロジーのモダナイゼーション
      • とにかくサービスを世に出すスピードを早める
        • コンテナテクノロジーを活用したクラウドネイティブな開発環境
        • CI/CD導入
        • 全て1からMakeすることもできるが、スピードアップの為にMakeとUseを使い分ける(OSSを使っていく)
        • アプリケーション開発に注力できる環境を作る(画像2枚目)
    • 習慣のモダナイゼーション
      • 負債が増える習慣を変える
        • 取り組む理由を伝え続ける
        • ルールをシンプルにする ルールとドキュメントをそれぞれ一箇所に集約する
      • 負債が増えにくい開発手法を選択する
        • スクラム開発やペアプログラミングの実施
  • 結果的に、開発スピードが向上。世にリリースする回数も増えた

  • Yahoo!JapanのエンジニアリングはSpeedが重要!

基調講演を通して、いかにYahooが世に出すスピードを大切にしているかが伝わってきました。
そしてその為に技術だけではなくマインドや習慣も変えていこうという意識や達成するためへの取り組みは、
非常に力強く徹底されており組織全体に行き渡っているなと感じました。

個人的に刺さったのは、マインドのモダナイゼーションのお話しの中に出てきた「社内LT実施」です。
今私が勤めている会社でも社内LTの取り組みがありましたが、技術スタックの違いや発表できる場の少なさ、自らキャッチアップする人が多くないなど様々な要因であまり根付きませんでした。
お聞きしたところYahooさんでは技術ごとにLT大会が不定期に発生するそうで、自ら情報を発信・受信する技術的成長のマインドがとても強く根付いているなと感じました。
また、定期的に開かれるYahooさん主催イベントの「Bonfire」等、登壇する機会が多く技術者として成長できる場が多く提供されているのも魅力的だなと思います。
エンジニアとして成長できる環境が整っていて、とても羨ましいです:blush:

各セッション

特に印象に残ったセッションを抜粋してレポします。

B-1 PayPayモールのAMP活用とデザインシステム

https://www.slideshare.net/techblogyahoo/paypayamp-yjtc19-in-shibuya-b1-yjtc-204740937?ref=https://techblog.yahoo.co.jp/entry/20191213789272/

  • デザインシステム × AMPの実現
    • デザインの統一の為、スタイルガイドを制定した
      • 使う配色やpadding、Z-indexなどの原則を定めた
      • 上記原則をSCSS変数を活用し管理した
      • デザインはZeplinに載せ、PJ内でいつでもだれでも見られる様にした
    • コンポーネントはAtomic Design思考で作成した
      • AMPコンポーネントをベースにコンポーネント(UI上で使うパーツとか)が作れる
      • コンポーネント外部から使いたい情報を渡す前提で作り(Propsで渡す)、再利用性を高くした
  • 実現へのブレイクスルーとして、Nuxt.jsを導入した
    • 実現する為にはNuxt.jsとNPMパッケージで対応可能
    • 唯一AMP validatedなページにすることが上記では対応出来ない。
      • その為に、Nuxt.js hookによるビルド拡張を導入した
  • AMPの活用
    • AMPコンポーネントを用いれば、カルーセルなどが簡単に作成出来る
    • AMPキャッシュのパーソナライズを実現できた

今回のセッションの中で一番技術スタックが近く、特にSCSSで管理やAtomic Design採用の強みのところでめっちゃうんうん頷いていました。

後半のAMPの話は新鮮で、「なんか検索すると稲妻マークがつくページあるなあ」くらいの認識だったので新たな知見を得ることができました。
AMPコンポーネントもお初だったのですが、Material-uiの様に使えたり動作が軽かったりと、非常に魅力的に聞こえました。

ただ、AMPValidation時にはJSが使えないや自由度が低いというところが気になっていて、具体的にどんな感じなのかは試してみて実感したいと思います。

LT 新しいHTML<portal>を利用した画面遷移設計〜PayPayモールとYahoo!ニュースの事例を添えて〜

LTなのでスライドが公表されておりませんが、内容としては以下の記事のLTでした。

新しいHTMLタグportal、Portals機能で変わるWebの遷移体験! CDS2019で紹介されたヤフーの実装例 #UIUX
- Yahoo! JAPAN Tech Blogより
https://techblog.yahoo.co.jp/entry/20191118780830/

  • まだChromeでしか使えず、限定的な機能である
  • 今回紹介された「一覧画面→詳細画面への遷移時」を例にとると、以下の様な感じ(上記Blogに動画あり)

    1. 詳細画面への遷移リンクを踏む
    2. 用意した要素(ここでは詳細画面チックなスケルトン)を一覧画面のURLのまま表示させる(普通はここでローディングとかになる)
    3. 詳細画面へ遷移完了(URLも変わる)
  • <iflame><a>のいいとこ取りの様なもの

  • レンダリング処理をユーザーに見せない、シームレスな遷移体験を提供することができる

  • 提供されているAPIも簡単で、実装も容易

<portal>が他ブラウザにも普及していけば、今後多用されること間違いなしかな、と思わせるLTでした。
また、スピーカーの方もおっしゃっていましたがまだまだ便利な使い方を見つけることが出来そうです。
自分も是非お試し実装して体験してみたいと思います。

B-6 YDNレポートジョブ渋滞に機械学習でたちむかってみた

https://www.slideshare.net/techblogyahoo/ydn-yjtc19-in-shibuya-b6-yjtc-204743694?ref=https://techblog.yahoo.co.jp/entry/20191213789272/

  • YDNレポートとは、Yahooが提供する広告サービス「YDN」を利用する広告主(広告を出す側の人)に、出している広告がどれくらい見られているかをまとめたレポート
  • ある日を境に、ジョブ渋滞が発生
    • レポート生成のための集計処理にとてつもなく時間がかかるケースが大量に投入される様になった
    • 重い処理がQueueに入ると、後続の処理(軽い処理も含めて)が渋滞し処理不能になった
    • 軽い処理救済の為の救済として、重い処理専用のQueueを作ったが、軽い重いの判別をつけること自体に時間がかかってしまう
  • 処理の重さの判別を付けるために、機械学習を導入。 無事重い処理を専用Queueに流すことができる様になった。

自分は機械学習に疎いので、機械学習といえば「画像識別」や「株価予想」を連想しがちでした。
そんな中、処理フローの課題解決のために機械学習を活用するアイディアを聞き、その様な使い方があるのかと驚かされました。

課題解決の為に機械学習を使いこなすことが選択肢に入ってくると、より困難なケースが打破できる様になりそうですね。
他にも機械学習を生かして課題解決した事例があれば是非聞いてみたいなと思いました。

アフターパーティー


カンファレンス経験がほとんどないので、このようなアフターパーティーも初参加でした。
一時間ほどの短い時間でしたが、とても充実したアフターパーティーだったと思います。

特に登壇者のみなさんが各テーブルにいらっしゃり、直接お話しさせていただけたのが刺激になりました。
(CTOの藤門さんとも直接お話しできる貴重な機会!)

またYahooプロパーさんのみならず、他の参加者の方と技術的な話や仕事の話(愚痴?)も出来たりできる貴重な場でした。
(React v15からv16にあげたら結構違くて困る!とか、Kubernetes出来る人全然いない!とか、AMPめっちゃ便利そう!とか話しました。)

インプットだけになりがちなカンファレンスにおいて、質問したり感想会をしたりできるこういう場が大事なんだなあとしみじみ思いました。。。
「話せるかどうかわからないしカンファレンスは出るけどアフターパーティーは遠慮しがち、、、」はもったいないと思います!

感想

セッション一覧からもわかる様に、昨今流行っている機械学習や、PayPay関連のセッションが多くとても面白かったです。
特にPayPay関連のセッションである
「B-5 PayPayアプリにおけるオンラインとオフラインのユーザー体験」
「A-6 CtoCフリマアプリの作り方 〜6ヶ月間のPayPayフリマ開発を支えた設計〜」
には非常に多くの人が参加されていたのが印象的です。
(セッション会場に入場する長蛇の列ができていました:sweat_smile:
PayPayへの関心の高まりを肌に感じ、改めてYahooさんの影響力の高さを実感しました。

他に印象的だったのは、全体的にセッションの登壇者は若い方が多く、自分の同年代の方も登壇されていたことです。
世の中に自分と同年代の人で活躍している人はそれこそ沢山いるのですが、
同じエンジニアとして登壇されているところを目の当たりにし、強く刺激を受けました。

また、参加者の層はFE、BE、アプリエンジニアや営業職の方など様々な方が見受けられました。
自分はFE専なので他の分野の技術に明るくないのですが、カンファレンスを機に他分野の最新動向も知ることができました。

せっかくの機会なので、自分の分野以外のセッションも聞くと新たに知見が広がるかもしれません。

私もこれを機に積極的にカンファレンスや勉強会に参加していきたいと思います!

会場の雰囲気

かろうじて取れてた写真数枚を添えておきます。

かっこいいネオン

セッション前の演出も含めて、スタイリッシュな感じが全面にでてました。

LTセッション

前日のツイッターで急に発表されたおしながき。
発表者の方にお聞きしたところ、急にやることが決まったらしいです。
さすがYahooさん決めてからが早い。

飲み物コーナー

写真は水だけですが、この左の机にはコーヒーサーバーとオレンジジュース、お菓子などがありました。

会社説明ブース

ついこの間リリースされた、DS.INSIGHTというサービスの資料などが置いてありました。
また、Y!天気・災害やY!乗り換え案内などのアプリアイコンが印刷された缶バッチや、
Yさん主催のBonfireというエンジニア向けイベントのシールなどもありました。

アンケートの答えたらもらえました

置くだけで充電出来るアレ(語彙不足)
かっこいいので存分に使い倒したいと思います。

AWS入門、ついでにDocker入門

$
0
0

目次

1.概要
2.Dockerとは
3.動かしてみよう
4.管理してみよう


1.概要

・AWS/Docker初心者が
 AWSを使ってDockerを動かしてみました。

・きっかけはNotesに投稿された
 SCC所属Aさんの「初学者によるDocker入門&ハンズオン」

・AWSを使って様々な製品、技術に触れるハードルが下がればと思います。


2.Dockerとは

コンテナ型の仮想化環境を
作成、配布、実行するためのプラットフォーム

ホストOS上で仮想化するので
○起動が高速、環境構築が簡単・・・
△ホストOSと同一のOSしか使えない・・・
Docker比較.png

Dockerの起動イメージ

Dockerイメージ.png


3.Dockerを動かしてみよう

(1)AWS EC2の立ち上げ
(2)Dockerをインストール
(3)DockerイメージからDockerコンテナを動かす

動作イメージ.png

(1)AWS EC2の立ち上げ
EC2コンソール上からインスタンスを立ち上げる。
(今回はAmazon Linux 2)

(2)Dockerをインストール
AWS提供リポジトリからDockerが取得できる。
(数秒で完了)

docker1.log
$sudoyuminstalldocker読み込んだプラグイン:extras_suggestions,langpacks,priorities,update-motd~中略~インストール:docker.x86_640:18.09.9ce-2.amzn2依存性関連をインストールしました:containerd.x86_640:1.2.6-1.amzn2libcgroup.x86_640:0.41-21.amzn2pigz.x86_640:2.3.4-1.amzn2.0.1runc.x86_640:1.0.0-0.1.20190510.git2b18fe1.amzn2完了しました!

(3)DockerイメージからDockerコンテナを動かしてみる
「Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world」
→ローカルにhello-worldイメージが存在しないため、
 DockerHubのライブラリから取得。

docker2.log
$sudodockerrunhello-worldUnabletofindimage'hello-world:latest'locallylatest: Pullingfromlibrary/hello-world1b930d010525: PullcompleteDigest:sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e4194f4ee23e415e1064Status:Downloadednewerimageforhello-world:latestHellofromDocker!Thismessageshowsthatyourinstallationappearstobeworkingcorrectly.

4.Dockerを管理してみよう

(1)DockerHub
(2)Amazon ECS/ECR

(1)DockerHub
Dockerイメージを公開・共有できるサービス
DockerHub.png

(2)Amazon ECS/ECR
ECS:Amazon Elastic Container Service
Dockerコンテナの管理サービス。

ECS1.png
ECS2.png
ECS3.png
ECS$.png

ECR:Amazon Elastic Container Registry
Dockerイメージの管理サービス。
スクリーンショット 2019-12-19 3.05.59.png

エンジニア未経験者が、メンティとして意識して良かった3つの事

$
0
0

この記事は ZOZOテクノロジーズ #5 Advent Calendar 2019 19日目の記事です。
昨日は、@hmsnakrさんのVSCodeでのSQL Server(MSSQL)の開発でした。

前書き

エンジニア未経験でサーバーサイドの担当部署に異動し、現在2年目になります。
未経験時にメンターを付けて頂き、OJTで業務をしてきた中で、「メンティとして意識すると、開発・相談・質問がしやすくなった事」を3点にまとめました。
個人的な経験がベースの内容となっていますが、以下のような方の参考になればと思います。

・エンジニア未経験で入社/部署異動した方
・OJTでエンジニア業務をスタートした方

①書きたい処理を日本語で書く

book_sakubun_kodomo.png

コードを書くこと自体に慣れていないうちは、「こういう処理を書きたい→そのままコードとして書く」というやり方自体に手間を取られます。初めは処理の型や定型文の蓄積がないので、頭の中だけでは「想定の処理→コードに落とし込んだ記述」への変換がしにくいためです。
なので、【コードを書くこと】に慣れない間は、「まず書きたい処理を日本語で書く→それをコードに翻訳する」という意識で書き方を分解すると、コーディングへの心理的な抵抗が少なくなり、手を動かしやすくなると思います。

イメージとしては、下記のような形です。このベタの日本語を、コードに置き換えていくイメージです。

もし、明日晴れだったら
    自転車で行く
もし、明日雨だったら
    傘をさして徒歩で行く
上記以外
    徒歩で行く

上記のようなやり方をすることで、自分の理解を日本語でアウトプット出来ているため、開発に詰まった時も、自分の理解レベルを可視化した状態でメンターへの相談・質問をする事ができます。「一体どこでつまっているのか?」が見える化されているので、メンターとのコミュニケーションも円滑になります。自分にとっても、「いま何が分からないのか分からない」という混乱した状態を避けやすくなります。

②質問は選択式にする

wakaremichi_man.png

開発や仕様調査に詰まった際に、「~が分からない」という【状態】をそのまま相談すると、「どこまで分かっていて、どこからが分かっていないのか?」という理解レベルの共有が難しくなります。

そのため、「AとBという処理を考えたが、どちらがベターか?」「この仕様はAという理解、Bという理解、どちらの方が正しいか?」という選択式の質問として疑問を分解できていると、「自分の理解レベルを共有」+「メンター側の考えや選択時の基準」を引き出しやすくなります。
当たり前ですが、メンターはメンティの質問や相談の他にも、並行で業務をしています。そのため、メンターの思考のリソース+時間を余計に奪わないという意味でも、質問を整理→選択式にするメリットはあると思います。

③素直に分からないと伝える(自力の調査時間を設定の上)

kyosyu_man.png

3つの中で、この点が一番大事だと思います。
開発や調査内容によっては「そもそも何がなんだか分からない。。」という状態になる場合もあります。その場合、①のように理解を日本語で書き出すこと、②のように選択式に洗い出すことが難しくなります。今まで全く触ったことのない機能の改修や、新規案件で要件の作成から関わる場合、上記のような 「理解のスタートとなる地点が分からない」という状態になりやすいです。

そういった際は、自力で調査・理解する時間に 30分の制限を設けます。この時間をオーバーしても分からない時は、それ以上調査しても、自力では理解できない可能性が高いです。そもそもの調査のスタート地点が間違っていたり、押さえるべき要件が抜けていたりします。そういった時は、メンターなど他人の目を一度借り、軌道修正をした方が良い場合が多いです。

仮に自分が大規模プロジェクトの一部ファイルの開発にアサインされた際、上記のような修正を挟まなかったとすると、
・プロジェクト全体に関する理解不足を解消しないまま進む
・終盤で理解不足による開発不足が発見される
・関わっている人の開発タスクにマイナスの影響を及ぼす
という、避けたい事態に陥ります。

上のような状況を防ぐ意味でも、
1:制限時間を設けた上で理解(分解)する
2:理解できない時は、メンター(場合によってはプロジェクの責任者)に素直に分からない状況を伝える
3:メンターから貰ったフィードバックを、理解できるレベルまでテキストベースに落とし込み、共有・内容確認して貰う
という流れが効率的だと思います(蛇足ですが、テキストに落とし込むのは、①と同じく自分の理解を可視化・共有しやすくするためです。
「そもそも~という事が分からなくて困っている」と素直に伝える勇気も、エンジニア経験の浅いメンティには、大事な意識だと思います。ちなみにメンターと朝会や夜会を設定し、その日の疑問点等を話す時間があると、上記のような相談もしやすくなると思います

参考サイト

・イラスト
https://www.irasutoya.com/

Viewing all 21081 articles
Browse latest View live