0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-17

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

画面遷移図

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

func_1.png

コンポーネントへのルーティング

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

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・収録地点・収録場所・収録年月日・話題・種別・データ名」をとってきてリスト表示するようにします。また、談話ごとに「発話一覧」ページへのリンクを貼ります。

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

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

発話一覧

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

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 テーブル自体にどんな属性があるかはとりあえず考えず、欲しい情報を欲しいだけ記述しておきましょう。

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>

発話編集

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

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 アクセスする)。

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');

各コントローラの作成

そうしたらコントローラを2つ作成します。

談話コントローラ

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

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

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

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

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

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

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

/discourse/01_b_099
discourseshow.png

改善点

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

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

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

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

次回

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?