日本語諸方言コーパスをDB化して遊ぶ (6) 談話ごとの発話総覧を作る

連載記事です。前回までにルーティングとモデルの準備まで行なったので、第６回は談話ごとの発話総覧を作ります。完全に自分用の作業メモで、説明もいろいろ足りていないと思いますが、ご容赦ください。

- 第１回： [日本語諸方言コーパスをDB化して遊ぶ (1) 構成を考える](https://qiita.com/a_eau_/items/dcb83886c4b580565ce7)
- 第２回： [日本語諸方言コーパスをDB化して遊ぶ (2) SQLite3 で DB 化](https://qiita.com/a_eau_/items/0bb4f3f8271c6dbc12a5)
- 第３回： [日本語諸方言コーパスをDB化して遊ぶ (3) PHP Laravel で操作する](https://qiita.com/a_eau_/items/09f648ede77fd22e7585)
- 第４回： [日本語諸方言コーパスをDB化して遊ぶ (4) サービスの全体像を決める](https://qiita.com/a_eau_/items/0af125f960dd1d16fcde)
- 第５回： [日本語諸方言コーパスをDB化して遊ぶ (5) データベースの移行とモデルの作成](https://qiita.com/a_eau_/items/3cad488add22bfacfeaa)
- 第６回： 日本語諸方言コーパスをDB化して遊ぶ (6) 談話ごとの発話総覧を作る　←今ここ
- 第７回： [日本語諸方言コーパスをDB化して遊ぶ (7) 話者ごとの発話総覧を作る](https://qiita.com/a_eau_/items/ccb472a0fc2ac6ca9919)
- 第８回： [日本語諸方言コーパスをDB化して遊ぶ (8) ファイル形式変換機能をつける](https://qiita.com/a_eau_/items/3f196631348fec1cfc8d)
- 第９回： [日本語諸方言コーパスをDB化して遊ぶ (9) Heroku でデプロイする](https://qiita.com/a_eau_/items/1ae966836d0cf4a6ba0e)

## 画面遷移図
画面遷移図を再掲します。

![func_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/15368f73-ec24-1c52-1b5b-b7bde0fd226b.png)

## コンポーネントへのルーティング
画面遷移図のとおりにルーティングします。まずはパスとコンポーネントを紐づけてガワを作ります。`props: true` にするとアドレスに含まれる変数を使用できます。

```diff:resouces/js/app.js
+ import DiscourseIndexComponent from "./components/DiscourseIndexComponent";
+ import DiscourseShowComponent from "./components/DiscourseShowComponent";
+ import UtteranceShowComponent from "./components/UtteranceShowComponent";
+ import UtteranceEditComponent from "./components/UtteranceEditComponent";
 
+        {
+            path: "/discourse",
+            name: "discourse.index",
+            component: DiscourseIndexComponent
+        },
+        {
+            path: "/discourse/:discourseid",
+            name: "discourse.show",
+            component: DiscourseShowComponent,
+            props: true
+        },
+        {
+            path: "/utterance/:discourseid/:utteranceid",
+            name: "utterance.show",
+            component: UtteranceShowComponent,
+            props: true
+        },
+        {
+            path: "/utterance/:discourseid/:utteranceid/edit",
+            name: "utterance.edit",
+            component: UtteranceEditComponent,
+            props: true
+        }
```

## 各コンポーネントの作成
それぞれのページのレイアウトを作成していきます。前回作成した `app.blade.php` の `<router-view>` 位置に入る部品を作っていくのですが、`<template>` でひな形を作成し、そこに `<script>` で規定したデータや関数を加えていく寸法になります。

### 談話一覧
まず「談話一覧」ページは、データベースから「談話ID・収録地点・収録場所・収録年月日・話題・種別・データ名」をとってきてリスト表示するようにします。また、談話ごとに「発話一覧」ページへのリンクを貼ります。

```vue:resouces/js/components/DiscourseIndexComponent.vue
<template>
    <div>
        <table class="table table-sm table-hover">
            <thead class="thead-dark">
                <tr>
                    <th>談話ID</th>
                    <th>収録地点</th>
                    <th>収録場所</th>
                    <th>収録年月日</th>
                    <th>話題</th>
                    <th>種別</th>
                    <th>データ名</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="d in discourses" v-bind:key="d.discourseid">
                    <td>
                        <router-link
                            v-bind:to="{
                                name: 'discourse.show',
                                params: {discourseid: d.discourseid}
                            }"
                        >
                            <button class="btn btn-success btn-sm">
                                {{ d.discourseid }}
                            </button>
                        </router-link>
                    </td>
                    <td>{{ d.prefecturename }}{{ d.placename }}</td>
                    <td>{{ d.recordplace }}</td>
                    <td>{{ d.recorddate }}</td>
                    <td>{{ d.topic }}</td>
                    <td>{{ d.genre }}</td>
                    <td>{{ d.reference }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<script>
export default {
    data: function() {
        return {
            discourses: []
        };
    },
    methods: {
        getDiscourses() {
            axios.get("/api/discourse").then(res => {
                this.discourses = res.data;
            });
        }
    },
    mounted() {
        this.getDiscourses();
    }
};
</script>
```

下部の `<script>` 部分で表示するためのデータをロードします。`data` にデータを入れる箱だけ用意しておいて、`methods` にデータを取得してその箱に格納する関数を定義する感じです。ここでは `axios` で非同期的にデータを取得する処理だけ書いて、データベースとの実際のやり取りは別箇所で定義します（`/api/discourse` というフェッチ先パスについては後述します）。`mounted()` に関数を書いておくと、ページのロード完了時に自動的に実行されます。

上部の `<template>` には bootstrap で適当にデザインをつけながら、先ほど取得したデータを１レコードずつ表示していきます。vue.js の記法を少し勉強する必要がありますが、それほど難しくありません。今回はテーブルの行をデータ数だけ繰り返し表示したいので `<tr>` 要素に `v-for` を指定して、各要素は `{{ }}` 記法で記入していきます。

また `<router-link>` は Vue Router の独自コンポーネントで、`app.js` で規定したルーティングの名前を利用してリンクを貼ることができますので、これも利用します。右端に［閲覧］のような専用ボタンを用意してもよかったのですが、ページ幅の都合から、談話IDに発話一覧へのリンクを兼ねさせることにしました。

### 発話一覧
先ほどとほとんど同じですが、パスに含まれる談話ID (discourseid) を利用できるように `props` 内に指定しておきます。

```vue:resouces/js/components/DiscourseShowComponent.vue
<template>
    <div>
        <table class="table table-sm table-hover">
            <thead class="thead-dark">
                <tr>
                    <th class="text-nowrap">談話ID</th>
                    <th class="text-nowrap">発話ID</th>
                    <th>話者</th>
                    <th>方言</th>
                    <th>標準語</th>
                    <th>始点</th>
                    <th>終点</th>
                    <th></th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="u in utterances" v-bind:key="u.utteranceid">
                    <td>{{ u.discourseid }}</td>
                    <td>{{ u.utteranceid }}</td>
                    <td>{{ u.speakerid }}</td>
                    <td>{{ u.dialecttext }}</td>
                    <td>{{ u.standardtext }}</td>
                    <td>{{ u.tmin }}</td>
                    <td>{{ u.tmax }}</td>
                    <td>
                        <router-link
                            v-bind:to="{
                                name: 'utterance.show',
                                params: {
                                    discourseid: u.discourseid,
                                    utteranceid: u.utteranceid
                                }
                            }"
                        >
                            <button class="btn btn-success btn-sm text-nowrap">
                                閲覧
                            </button></router-link
                        >
                    </td>
                    <td>
                        <router-link
                            v-bind:to="{
                                name: 'utterance.edit',
                                params: {
                                    discourseid: u.discourseid,
                                    utteranceid: u.utteranceid
                                }
                            }"
                        >
                            <button class="btn btn-success btn-sm text-nowrap">
                                編集
                            </button></router-link
                        >
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<script>
export default {
    props: {
        discourseid: String
    },
    data: function() {
        return {
            utterances: []
        };
    },
    methods: {
        getUtterances() {
            axios.get("/api/discourse/" + this.discourseid).then(res => {
                this.utterances = res.data;
            });
        }
    },
    mounted() {
        this.getUtterances();
    }
};
</script>
```

### 発話詳細
単一の発話について詳細を表示するだけなので、`v-for` を使う必要はありません。発話についてのすべての情報を表示したいので、データベースとのやり取りでは何度も結合を行なう必要がありますが、それはコントローラの責任なので、コンポーネントを作成する段階では意識しなくても大丈夫です。`utterance` テーブル自体にどんな属性があるかはとりあえず考えず、欲しい情報を欲しいだけ記述しておきましょう。

```vue:resouces/js/components/UtteranceShowComponent.vue
<template>
    <div>
        <table class="table table-sm table-hover">
            <tbody>
                <tr>
                    <td>談話ID</td><td>{{ utterance.discourseid }}</td>
                </tr>
                <tr>
                    <td>発話ID</td><td>{{ utterance.utteranceid }}</td>
                </tr>
                <tr>
                    <td>発話区間</td><td>{{ utterance.tmin }} ~ {{ utterance.tmax }}</td>
                </tr>
                <tr>
                    <td>方言</td><td>{{ utterance.dialecttext }}</td>
                </tr>
                <tr>
                    <td>標準語</td><td>{{ utterance.standardtext }}</td>
                </tr>
                <tr>
                    <td>話者ID</td><td>{{ utterance.speakerid }}</td>
                </tr>
                <tr>
                    <td>話者生年</td><td>{{ utterance.speakerbirthyear }}</td>
                </tr>
                <tr>
                    <td>話者性別</td><td>{{ utterance.speakersex }}</td>
                </tr>
                <tr>
                    <td>地点</td><td>{{ utterance.prefecturename }}{{ utterance.placename }}</td>
                </tr>
                <tr>
                    <td>収録場所</td><td>{{ utterance.recordplace }}</td>
                </tr>
                <tr>
                    <td>収録年月日</td><td>{{ utterance.recorddate }}</td>
                </tr>
                <tr>
                    <td>収録担当者</td><td>{{ utterance.recorder }}</td>
                </tr>
                <tr>
                    <td>編集担当者</td><td>{{ utterance.editor }}</td>
                </tr>
                <tr>
                    <td>話題</td><td>{{ utterance.topic }}</td>
                </tr>
                <tr>
                    <td>ジャンル</td><td>{{ utterance.genre }}</td>
                </tr>
                <tr>
                    <td>出典</td><td>{{ utterance.reference }}</td>
                </tr>
            </tbody>
        </table>
        <div class="text-right">
            <router-link
                v-bind:to="{
                    name: 'utterance.edit',
                    params: { discourseid: this.discourseid, utteranceid: this.utteranceid }
                }"
            >
                <button class="btn btn-success btn-sm text-nowrap">
                    編集
                </button></router-link
            >
        </div>
    </div>
</template>

<script>
export default {
    props: {
        discourseid: String,
        utteranceid: String
    },
    data: () => {
        return {
            utterance: {}
        };
    },
    methods: {
        getUtterance() {
            axios.get("/api/utterance/" + this.discourseid + "/" + this.utteranceid )
                .then(res => {
                    this.utterance = res.data[0];
                });
        }
    },
    mounted() {
        this.getUtterance();
    }
};
</script>
```

### 発話編集
方言テキストと標準語テキストだけ編集できるようにしたいので、この２つだけ `v-model` でバインドした入力欄を作ります。また、ログには新旧テキストを保存したいので、データの読み込み段階で元のテキストを `olddialecttext` および `oldstandardtext` として保持しておいて、変更後のテキストを `newdialecttext` および `newstandardtext` として送信するようにしています。

```vue:resouces/js/components/UtteranceShowComponent.vue
<template>
    <div>
        <table class="table table-sm table-hover">
            <tbody>
                <tr>
                    <td>談話ID</td><td>{{ utterance.discourseid }}</td>
                </tr>
                <tr>
                    <td>発話ID</td><td>{{ utterance.utteranceid }}</td>
                </tr>
                <tr>
                    <td>話者</td><td>{{ utterance.speakerid }}</td>
                </tr>
                <tr>
                    <td>発話区間</td><td>{{ utterance.tmin }} ~ {{ utterance.tmax }}</td>
                </tr>
                <tr>
                    <td>方言</td>
                    <td>
                        <input
                            class="w-100"
                            type="text"
                            id="dialecttext"
                            v-model="utterance.dialecttext"
                        />
                    </td>
                </tr>
                <tr>
                    <td>標準語</td>
                    <td>
                        <input
                            class="w-100"
                            type="text"
                            id="standardtext"
                            v-model="utterance.standardtext"
                        />
                    </td>
                </tr>
            </tbody>
        </table>
        <div class="text-right">
            <span>
                <button
                    class="btn btn-success btn-sm text-nowrap"
                    @click="submit"
                >
                    変更を反映
                </button>
            </span>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        discourseid: String,
        utteranceid: String
    },
    data: function() {
        return {
            utterance: {}
        };
    },
    methods: {
        getUtterance() {
            axios
                .get("/api/utterance/" + this.discourseid + "/" + this.utteranceid + "/edit")
                .then(res => {
                    this.utterance = res.data[0];
                    this.utterance.olddialecttext = this.utterance.dialecttext;
                    this.utterance.oldstandardtext = this.utterance.standardtext;
                });
        },
        submit() {
            axios.post("/api/utterance/update", {
                    newUtterance: JSON.stringify(this.utterance)
                })
                .then(res => {
                    message.textContent = "更新されました。";
                });
        }
    },
    mounted() {
        this.getUtterance();
    }
};
</script>
```


## コントローラへのルーティング
データベースとのやり取りやデータ処理はコントローラ上に実装するので、まずはコントローラへのルーティングを行ないます。SPA なので、ページ遷移するのではなく、API を介してデータだけ取得するようにします。API としてのルーティングは `routes/web.php`　ではなく `routes/api.php` で行ないます。

以下のようにパスとコントローラの各関数を紐づけておきます。`api.php` で定義したルーティングは使用時にプリフィックスとして `api/` をつけてアクセスすることになります（たとえば最上段は `/discourse` ではなく `/api/discourse` に GET アクセスする）。

```php:routes/api.php
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/discourse', 'DiscourseController@index');
Route::get('/discourse/{discourseid}', 'DiscourseController@show');
Route::get('/utterance/{discourseid}/{utteranceid}', 'UtteranceController@show');
Route::get('/utterance/{discourseid}/{utteranceid}/edit', 'UtteranceController@edit');
Route::post('/utterance/update', 'UtteranceController@update');
```

## 各コントローラの作成
そうしたらコントローラを２つ作成します。

### 談話コントローラ
談話コントローラには、談話一覧を取得する関数 `index` と談話ごとの発話一覧を取得する関数 `show` を定義します。後者は発話コントローラに定義してもよかった気がしますが、とりあえずこちらに定義しておきます。

`index` 関数ですが、`discourse` テーブルには県番号 prefecturenum や地点ＩＤ placeid はあるものの、県名 prefecturename や地点名 placename が存在しないので、他のテーブルと結合して取得します。

`show` 関数はセオリー通りで特に説明することはありません。

```php:app/Http/Controllers/DiscourseController.php
<?php
namespace App\Http\Controllers;
use App\Models\Discourse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class DiscourseController extends Controller{
    public function index(){
        $data = DB::table('discourse')
        ->join('place', function ($join) {
            $join
            ->on('discourse.prefecturenum', '=', 'place.prefecturenum')
            ->on('discourse.placeid', '=', 'place.placeid');
        })
        ->join('prefecture', 'discourse.prefecturenum', '=', 'prefecture.prefecturenum')
        ->select('discourse.*', 'place.placename', 'prefecture.prefecturename')
        ->get();
        return $data;
    }

    public function show(String $discourseid){
        $data = DB::table('utterance')
        ->select('utterance.*')
        ->where('discourseid', '=', $discourseid)
        ->get();
        return $data;
    }
}
```

### 発話コントローラ
発話コントローラには、発話詳細のためのデータ取得 `show`、発話編集のためのデータ取得 `edit`、発話レコードのアップデート `update` を定義します。

`show` 関数は先ほどと同様に、他テーブルと結合して必要な情報を取得します。条件にマッチする１レコードのみ取得すればよいので、コントローラ上で `->first()` してもいいのですが、今回はコンポーネント上で `data[0]` として絞り込んでいます。

`edit` 関数については、発話編集画面に必要な情報はシンプルなので、条件に一致するレコードを返すだけです。

`update` 関数では、utterance の更新と changelog へのログ追加を行ないます。`update` のみ POST アクセスとしてルーティングしましたので、パラメータは `$request->input('param')` のように取得しています。本来はトランザクション処理にするべきですが、Laravel のデフォルト設定だとこうした **複数テーブルへの処理は自動的に異なるコネクションとなる** ため、`DB::begintransaction()` → `DB::rollbaack()` などとやってもうまくロールバックしません。そのためトランザクションは作成せずそのまま処理しています（ダメでは？）。

- [LaravelのDBトランザクション落とし穴 | Qiita](https://qiita.com/sasakure-kei@github/items/bd4f64bae860e7287b8f)

```php:app/Http/Controllers/DiscourseController.php
<?php
namespace App\Http\Controllers;
use App\Models\Utterance;
use App\Models\Discourse;
use App\Models\Changelog;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;

class UtteranceController extends Controller{
    public function show(String $discourseid, String $utteranceid){
        $record = DB::table('utterance')
        ->select('utterance.*')
        ->where('utterance.discourseid', '=', $discourseid)
        ->where('utterance.utteranceid', '=', $utteranceid)
        ->join('speaker', 'utterance.speakerid', '=', 'speaker.speakerid')
        ->join('discourse', 'utterance.discourseid', '=', 'discourse.discourseid')
        ->join('place', function ($join) {
            $join
            ->on('discourse.prefecturenum', '=', 'place.prefecturenum')
            ->on('discourse.placeid', '=', 'place.placeid');
        })
        ->join('prefecture', 'discourse.prefecturenum', '=', 'prefecture.prefecturenum')
        ->select('*')
        ->get();
        return $record;
    }

    public function edit(String $discourseid, String $utteranceid){
        $record = DB::table('utterance')
        ->select('utterance.*')
        ->where('discourseid', '=', $discourseid)
        ->where('utteranceid', '=', $utteranceid)
        ->get();
        return $record;
    }

    public function update(Request $request){
        $json = $request->input('newUtterance');
        $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
        $arr = json_decode($json);
        
        try {
            $discourseid = $arr->discourseid;
            $utteranceid = $arr->utteranceid;
            $olddialect = $arr->olddialecttext;
            $oldstandard = $arr->oldstandardtext;
            $newdialect = $arr->dialecttext;
            $newstandard = $arr->standardtext;
            $log = new Changelog([
                'discourseid' => $discourseid,
                'utteranceid' => $utteranceid,
                'olddialecttext' => $olddialect,
                'oldstandardtext' => $oldstandard,
                'newdialecttext' => $newdialect,
                'newstandardtext' => $newstandard,
                'updatedtime' => date_create($request->date) 
            ]);
            $log->save();

            DB::table('utterance')
            ->where('discourseid', '=', $discourseid)
            ->where('utteranceid', '=', $utteranceid)
            ->update(['dialecttext'=>$newdialect, 'standardtext'=>$newstandard]);

        } catch (\Exception $e) {
            throw $e;
        }
    }
}
```

## 完成図
以下のようになっているはずです。

`/discourse`
一部日付データがバグってるのはご愛敬。
![discourseindex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/e6a4a867-24b7-a26e-1758-0908a93b6915.png)

`/discourse/01_b_099`
![discourseshow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/f8b161f1-275d-2bed-d07a-afdc265d508b.png)



## 改善点
本連載では最低限必要な部分だけに絞って解説しているので、実際には説明文を書いたり、ロード中のスピナーを仕込んだり、セキュリティへの配慮をもうちょっと頑張ったりしています。たとえば上のコードでは次のような点が危険です。

- パスに含まれる文字列をエスケープをせずに使っている
- `try - catch` が実質無意味

そこらへんは PHP や JavaScript の一般的な注意点と同様なので、この記事では解説しません。

あと書き終わった時点で「更新処理後のリダイレクト実装してない！」と気づきましたが、気力が尽きたので今は放っておきます。

## 次回
第２の機能「話者ごとの発話総覧」を作りこんでいきます。

- 第７回： [日本語諸方言コーパスをDB化して遊ぶ (7) 話者ごとの発話総覧を作る](https://qiita.com/a_eau_/items/ccb472a0fc2ac6ca9919)