【Laravel】初めてのマイグレーション

Laravelでのモデル作成とマイグレーションファイル編集に関する備忘録。


# ■ モデル作成
- [Laravel公式リファレンス](https://laravel.com/docs/5.2/migrations#generating-migrations)
- **make:model** は、Eloquentモデルの生成コマンド。

```php:モデル作成（※モデル名は、大文字から始まる単数形）
% php artisan make:model モデル名             // app直下にファイル生成
% php artisan make:model Entities/モデル名    // app/Entities下にファイル生成される
% php artisan make:model モデル名 -m -c -r    // モデル、マイグレーション、コントローラーを一気に生成

% php artisan make:model -h                 // どんなオプションがあるか?
```
**【モデル作成（make:model）のオプション例】**

|オプション|機能|
|:--|:--|
|-a, --all|モデルの移行、ファクトリ、リソースコントローラー生成|
|-c, --controller|コントローラー生成|
|-f, --factory|モデルの新しいファクトリー作成|
|--force|モデルが存在する場合でもクラス作成|
|-m, --migration|マイグレーション作成|
|-p, --pivot|生成モデルをカスタム中間テーブルモデルにする必要があるか？を示す|
|-r, --resource|生成コントローラーをリソースコントローラーにする必要があるか?を示す|
|-h, --help|どんなオプションがあるかを見れる|
|-q, --quiet|メッセージを出力しない|
|-V, --version|アプリのバージョン表示|
|--ansi|ANSI強制出力|
|--no-ansi|ANSI出力を無効にする|
|-n, --no-interaction|インタラクティブな質問を省略|
|--env[=ENV]|コマンドを実行する環境の指定|
|-v, --verbose|メッセージの詳細度の指定。1:通常出力、2:詳細出力、3:デバッグ用|


- マイグレーションクラスは、2つのメソッドがある（up、down）。
  - upメソッド ： マイグレーション実行時の処理 を記述。
  - downメソッド ： ロールバック時の処理を記述（upメソッドの逆）。

```php:作成されるマイグレーションファイルのざっくりイメージ（database/migrations/xxxxx_create_xxxxx_table.php）
public function up() {           // マイグレーション実行時の処理は、up() に記述
    Schema::create('xxxxxs', function (Blueprint $table) {
      $table->increments('id');        // デフォルト
      $table->データ型('カラム名');        // カラム作成
      $table->tinyInteger('gender')->unsigned()->comment('性別 1:男、2:女');
      $table->timestamps();            // デフォルト
    });
}
public function down() {        // ロールバック時の処理は、down() に記述
    Schema::dropIfExists('xxxxxs');    // テーブル削除
    Schema::create('xxxxxs', function (Blueprint $table) {
      $table->dropColumn('カラム名');    // カラム削除
      $table->dropColumn('gender');
    });
}
```

## よく使うマイグレーション関連のコマンド

```php:マイグレーションに関するコマンド
% php artisan migrate:status              // 状態確認
% php artisan migrate                     // マイグレーション実行
% composer dump-autoload                  // 「クラスが見つかりません」エラーが発生したら、migrateコマンドを再発行してみる
% php artisan migrate:refresh             // 初期化→再実行（マイグレーションファイル追加でエラーが出た時など）
% php artisan migrate:fresh               // 全テーブル削除 →マイグレーション実行
% php artisan migrate:rollback            // ロールバック（1つ前）
% php artisan migrate:rollback --step=2   // ロールバック（戻る位置指定）
% php artisan migrate:reset               // 全マイグレーションをリセット（初期化）
```


# ■ マイグレーションファイルのみ作成したいなら

```php:例）マイグレーションファイルのみ作成したい場合
% php artisan make:migration create_boxs_table --create=boxs             // boxsテーブル生成
% php artisan make:migration add_カラム名_to_users_table --table=users     // usersテーブルにカラム追加（既存テーブルを更新）
% php artisan make:migration rename_旧テーブル名_to_新テーブル名_table        // テーブル名変更
% php artisan make:migration change_カラム名_既存カラム型_to_新カラム型_on_テーブル名_table --table=テーブル名    // カラム型変更
% php artisan make:migration change_boxs_table --table=boxs              // ざっくりテーブル変更

// マイグレーションファイルの編集ができたら、実行
% php artisan migrate
% composer require doctrine/dbal     // 注)変更・更新には、更新用のパッケージのインストールが必要
% php -d memory_limit=-1 /usr/local/bin/composer require doctrine/dbal    // メモリー制限エラーが出た時は、メモリーを上げてインストールしてみる
```






# ■ マイグレーションファイルの書き方
## テーブル関連

```php:テーブル
// テーブル名変更
Schema::rename("旧テーブル名", "新テーブル名");

// テーブル削除
Schema::drop('users');
Schema::dropIfExists('users');     // テーブルがあれば削除して、なければ何もしない（エラー回避）
```

## カラム関連
### カラム追加・削除

```php:カラム追加・削除
// カラム追加
$table->カラム型('カラム名');
$table->string('カラム名');

// カラム削除
$table->dropColumn('削除したいカラム名');
$table->dropColumn(['削除したいカラム名1', '削除したいカラム名2',..]);   // 複数のカラムを削除したい場合は、配列で渡す
```
- カラム削除のコマンド

|削除コマンド	|説明|
|:--|:--|
|$table->dropRememberToken( );|remember_tokenカラム（Eloquent認証ドライバーを扱うためのカラム ）|
|$table->dropSoftDeletes( );|deleted_atカラム（ソフトデリート用のNULL可能なカラム）|
|$table->dropSoftDeletesTz( );|↑ と同義|
|$table->dropTimestamps( );|created_atとupdated_atカラム|
|$table->dropTimestampsTz( );|↑ と同義|

- **ソフトデリート**とは？
  - 論理削除のこと。データは削除せず、データを削除したものをして取り扱う。

### カラム変更
- カラム変更前に、`composer.json`ファイルで**doctrine/dbal**の追加が必要。
  - 現在の状態を決め、指定カラムの修正を行うSQLクエリ生成に使われる。

```php:
composer require doctrine/dbal
```

```php:カラム変更
// カラム名変更
$table -> renameColumn('以前のカラム名', '新しいカラム名');

// カラム属性の変更
$table->string('name', 50) -> change();                     // nameカラムのサイズ変更
$table->string('name', 50) -> nullable() -> change();       // NULLを許可する場合
$table->string('name', 50) -> nullable(false) -> change();  // NULLを許可しない場合

// カラム型の変更
$table->新しいデータ型('カラム名')->change();

```
- **chengeメソッド** : カラム型、属性の変更。











## インデックスや制約など

### インデックス
- Laravelでは、デフォルトで意味が通る名前をインデックスに付ける。
  - インデックス名 ： `テーブル名_インデックスしたカラム名_インデックスタイプ`

#### インデックス 【追加】

|インデックス追加コマンド|概要|使用例|
|:--|:--|:--|
|index('カラム名')|インデックス追加|＄table->integer('category_id')->index('category_id');|
|unique('カラム名')|ユニークキー追加|＄table->unique('email');、$table->string('email')->unique();|
|primary('id');|主キー追加|＄table->primary('id');|
|primary(['id', 'parent_id']);|複合キー追加|＄table->primary(['id', 'parent_id']);|
|spatialIndex('カラム名');|空間インデックス追加(SQLite以外)|＄table->spatialIndex('location');|

#### インデックス 【削除】
- インデックス削除は、インデックス名を指定する。

|インデックス削除コマンド|概要|
|:--|:--|
|$table->dropIndex('テーブル名_カラム名 _index');|インデックス削除|
|$table->dropUnique('テーブル名_カラム名 _unique');|ユニークキー削除|
|$table->dropPrimary('テーブル名_id _primary');|主キー削除|
|$table->dropSpatialIndex('テーブル名_カラム名 _spatialindex');|空間インデックス削除(SQLite以外)|

- カラムの配列をインデックスの削除メソッドに渡すと、インデックス名が自動生成される。

```php:例）インデックス削除
$table -> dropIndex(['content']);   // 例） boxs_content_index
```






### 外部キー

#### 外部キー 【追加】
- 紐づけたいテーブルのid（unsined(符号無し)）と同様の制約をつけないと形式不一致でエラーになるので注意。

##### 既存カラムを外部キーにする（紐づける）場合

```php:例）既存のuser_idカラムを外部キーにする
$table->integer('user_id')->unsigned()->change();                  // 符号無し属性に変更
$table->foreign('user_id')->references('id')->on('users');         // 外部キー参照
```
##### 新規カラムとして、外部キーを作成する場合

```php:例）usersテーブルのidカラムを使って、postsテーブルにuser_idカラムを定義する
Schema::table('posts', function (Blueprint $table) {
  $table->integer('user_id')->unsigned();                           // 符号無し属性を作成
  $table->foreign('user_id')->references('id')->on('users');        // 外部キー参照
});

// 参）外部キーを違う名前にしたい場合
$table->unsignedBigInteger('外部キー名');
$table->foreign('外部キー名')->references('id')->on('テーブル名')->onDelete('cascade');
  // モデルでのリレーションの書き方例（少し違うので注意）
  public function テーブル名() {
      return $this->belongsTo('App\Models\モデル名', '外部キー名');
      return $this->belongsTo('App\Models\モデル名','外部キー名')->withDefault();
      return $this->hasMany('App\Models\モデル名','外部キー名');
  }
```


#### 外部キー 【削除】
- **dropForeignメソッド** : 他のインデックスと似た命名規則。
- `テーブル名_カラム名_foreign`

```php:外部キーの削除
$table->dropForeign('posts_user_id_foreign');
$table->dropForeign(['user_id']);              // 配列で渡せば、命名規則に従った名前が使用される
```
- 参）マイグレーション中の外部キー制約を**一時的に**、有効/無効化ができるメソッドも用意されてる。
  - 外部キー制約を無効にしてDBを操作し、終わったら有効に戻すような時に使う。

```php:参）外部キー制約の有効/無効化の変更
Schema::enableForeignKeyConstraints();    //外部キー制約を有効化
Schema::disableForeignKeyConstraints();   //外部キー制約を一時的に無効化
```



## マイグレーション関連のエラー
### MySQL接続エラー： SQLSTATE[HY000][2002]No such file or directory
> SQLSTATE[HY000] [2002] No such file or directory

- DBに接続できていないエラー。
- （原因例）
  - .envファイルとMySQL（DB）の、DB_DATABASE、DB_USERNAME、DB_PASSWORDが異なる。
  - .envファイルの修正後、キャッシュをクリアできてない。
  - rootユーザーのPW変更後に、MySQLを再起動してない。
  - MySQLソケットに関するファイル（mysql.sock）が読み込まれてない。 → database.phpで **unix_socket** を設定する。

```php:ターミナルからunix_socketを確認
// MySQL
% show variables like '%sock%';
            :
 | socket | /Applications/MAMP/tmp/mysql/mysql.sock |
```
```php:unix_socketの設定（database.php）
'mysql' => [
  'driver'      => 'mysql',
  'host'        => 'localhost',
          : 
  'unix_socket' => '/Applications/MAMP/tmp/mysql/mysql.sock', 
],
```














### MySQL文字列長エラー： SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table 'users' add unique 'users_email_unique'('email'))
#### 【原因】
公式キュメントによると、
>DB中への「絵文字」をサポートのため、デフォルトでutf8mb4文字セットを使ってる。MySQLがバージョン5.7.7未満の場合、マイグレーション時に、**インデックス用文字列長を明示的に設定する必要がある**。

実際に確認してみると、確かに、MySQLバージョン5.6を使っており、utf8mb4となってる。utf8mb4は、1文字 ＝ 4バイトなので、最大長の767バイトを超えてしまい、エラーとなってる模様。

```php:mySQLのバージョン確認
% mysql --version
mysql  Ver 14.14 Distrib 5.6.47, for osx10.15 (x86_64) using  EditLine wrapper
```
```php:app/config/database.php
  'mysql' => [
         :
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
         :
```
#### 【解決方法】
- [公式ドキュメント](https://readouble.com/laravel/7.x/ja/migrations.html)通りに、varcharカラムの最大長を191バイトに指定し、767バイト以上の文字列が入らないようにする。（767 ÷ 4 = 191.75）。
  - ↓の記述で、default値を変更できる（カラムごとに指定するのはめんどい）。マイグレーションをやり直せばエラーが消えるハズ。

```php:app/Providers/AppServiceProvider.php
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;     // 追記
 
class AppServiceProvider extends ServiceProvider
{
          :
  public function boot()
    {
      Schema::defaultStringLength(191);   // 最大長を191バイトに指定
    }
          :
}
```
- 若しくは、支障がないなら、、MySQLのバージョンを最新にする。





### テーブルがないよーエラー： SQLSTATE[42S02]: Base table or view not found: 1146 Table 'アプリ名.指定したテーブル名' doesn't exist (SQL: alter table '指定したテーブル名' add '追加しようとしたカラム名' int not null)
- 既存テーブルへのカラム追加時に、テーブル名が間違ってた。。  
→ マイグレーションファイルのテーブル名を修正して、マイグレーション実行 で解決。



### カラムの数と保存しようとしてるデータの数が合わない： SQLSTATE[21S01]: Insert value list does not match column list: 1136 Column count doesn't match value count at ...

- seederでカラムにデータを保存時に発生。
- 保存しないカラムはnullとしてあげることで解決。








# ■ カラム型 一覧

|コマンド	|概要|
|:--|:--|
|increments('id');|符号なしの数値（INT）を使ったID（主キー）。0〜4294967295。|
|bigIncrements('id');|符号なしのBIGINTを使ったID（主キー）。0〜18446744073709551615。※ 21億行以上にならないなら、↑ を使う。|
|tinyInteger('numbers');|数値（TINYINT型）。1バイト。0~255|
|smallInteger('カラム名');|数値（SMALLINT型）。2バイト。0~65,535|
|mediumInteger('カラム名');|数値（MEDIUMINT型）。3バイト。0～16,777,215|
|integer('カラム名');|数値（INT型）。4バイト。0～4,294,967,295|
|bigInteger('カラム名');|数値（BIGINT型）。8バイト。 0～18,446,744,073,709,551,615|
|float('カラム名', ○, △);|トータルで8桁、小数点以下2桁以内の小数|
|double('カラム名', ○, △);|トータルで15桁、小数点以下8桁以内の小数|
|decimal('カラム名', ○, △);|トータルで5桁、小数点以下2桁以内の小数|
|char('カラム名', 5);|固定長文字列（長さが決まってる文字列）|
|string('カラム名');|可変長文字列。デフォルトの最大文字数：255文字|
|string('カラム名', 100);|最大文字数指定の可変長文字列|
|text('カラム名');|文字列。65,535文字（約64Kバイト）|
|mediumText('カラム名');|長い文字列。16,777,215文字（約16Mバイト）|
|longText('カラム名');|めっちゃ長い文字列。4,294,967,295文字(約4Gバイト）|
|date('カラム名');|日付|
|year('カラム名');|年|
|time('カラム名');|時間|
|timeTz('カラム名');|時間（タイムゾーン付き）|
|dateTime('カラム名');|日時|
|dateTimeTz('カラム名');|日時（タイムゾーン付き）|
|boolean('カラム名')->default(false);|真偽値|
|enum('カラム名', ['定数', '定数']);|enum型|
|binary('カラム名');|バイナリデータ|
|json('カラム名');|JSON|
|jsonb('カラム名');|JSONB|
|timestamp('カラム名');|タイムスタンプ|
|timestampTz('カラム名');|タイムスタンプ（タイムゾーン付き）|
|timestamps();|created_atとupdated_at|
|nullableTimestamps();|↑と同じ（NULLを許可）|
|softDeletes();|NULL可能な deleted_at カラム。例：退会日時などの管理用途（nullでなければ、退会済）|
|ipAddress('カラム名');|IPアドレス|
|macAddress('カラム名');|MACアドレス|
|rememberToken();|Eloquent認証ドライバーを扱うためのカラム。remember_tokenVARCHAR（100）NULLとして追加|
|uuid('id');|UUID。重複の可能性が小さく、一意な識別子として扱えるID|
|morphs('カラム名');|符号なし数値（taggable_id）と文字列（taggable_type）を追加|





# ■ オプション（カラム修飾子）一覧

|カラム修飾子|概要|
|:--|:--|
|->comment('コメント内容')|コメント追加（MySQLのみ）|
|->nullable()|NULL許可|
|->unsigned()|整数カラムを符号なしに設定（MySQLのみ）。外部キーでよく使う|
|->default('値')、->default($value)|デフォルト値を設定|
|->first()|テーブルの最初(first)に設置（MySQLのみ）|
|->after('カラム名')|指定カラムの次にカラム追加（MySQLのみ）|
|->autoIncrement()|整数カラムをID（主キー）に設定|
|->charset('utf8')|キャラクタセットを指定(MySQLのみ)|
|->collation('utf8_unicode_ci')	|コロケーションを指定(MySQL/SQL Serverのみ)|
|->nullable($value = true)、->nullable()->default(null)|デフォルトでNULL値を指定|
|->storedAs($expression)|stored generatedカラムを生成（MySQLのみ）|
|->useCurrent()|TIMESTAMPカラムのデフォルト値をCURRENT_TIMESTAMPに指定|
|->virtualAs($expression)|virtual generatedカラムを生成（MySQLのみ）|
|->onDelete('cascade')|・ **CASCADE**：親テーブルを更新すると、子テーブルも更新<br>・ **SET NULL**：親テーブルを更新/削除すると、子テーブルはNULLに更新<br>・ **RESTRICT**、**NO ACTION**：エラー発生|















# 例） json型で連想配列を管理する場合

```php:1つのカラムで連想配列を管理したい
// マイグレーション
$table->json('json_column');

// モデル
protected $fillable = ['json_column',];
protected $casts = ['json_column' => 'json',];  // コレを指定しないと、stringと扱ってしまう

// コントローラー
// 作成
ModelName::create([
    'json_column' => ['test1' => ['a'=>1, 'b'=>2,], 'test2'=>'456', ],
]);
// 取得
$test = array('test' => json_decode($request->test));

// ビュー
{{ Form::hidden('expert_check[]', json_encode($expert_select,TRUE)) }}

```










# ■ データの作成
- テーブルが作成できたら、データを作成してみる。

## seederで初期値を作成する場合
- モデルごとに、Seederファイルを作成し、それをDatabaseSeeder.php で読み込んでデータ作成を実行する。

```php:例）CategoriesTableSeeder.php生成
% php artisan make:seeder CategoriesTableSeeder

// クラスが見つからないエラーが出たら、、
% composer dump-autoload       // autoloadされるクラスを再定義
```
```php:seederに初期データを記述する
// database/seeds/CategoriesTableSeeder.php
use Illuminate\Support\Facades\DB;     // ← この記述が必要（DBファサード使用の宣言）。忘れると、エラー Class 'Database\Seeders\DB' not found
public function run() {
  DB::table('categories') -> insert([
    ['name' => '情報発信'],
    ['name' => 'つぶやき'],
    ['name' => '備忘録'],
  ]);
}

// database/seeds/DatabaseSeeder.php で呼び出す
public function run() {
  $this -> call([
    CategoriesTableSeeder::class,
  ]);
}

// ターミナルで、データ作成を実行
% php artisan migrate:refresh --seed

// クラスが見つからないエラー（Class UsersSeeder does not exist）が生じた時
% composer dump-autoload    // クラスを再定義してみる
```
- 因みに、seederでPWデータを作成する場合は、注意が必要。

```php:seederでPWデータを作成する場合
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;   // ← この記述が必要（Hashファサード使用の宣言）
public function run() {
    DB::table('admins') -> insert([
        [
            'name' => 'ユーザー１',
            'email' => 'testuser1@gmail.com',
            'password' => Hash::make('testuser1'),    // 暗号化
        ],
                  :
```

## コンソール操作でレコードを作成する場合

```php:ターミナル操作で作成する方法
% php artisan tinker               // コンソール起動
  // レコード作成（ インスタンスを作成して、saveメソッドを使う ）
  >>> $box = new App\Box();        // インスタンス作成（※ 名前空間がデフォルトでAppなので、App\モデル名(); ）
  >>> $box -> title = 'タイトル1';
  >>> $box -> content = '詳細1';
  >>> $box -> save();              // 保存
  => true                         // レコード作成成功

  >>> App\Box::all();             // 作成したデータの確認
  >>> App\Box::all()->ToArray();  // 作成したデータの確認（配列で取得）
  >>> exit
```





# ■ リレーション
- テーブルの関連付け。
- Tinkerで確認できる。

|関係|リレーションの種類|
|:--|:--|
|１対１| hasOne |
|１対多| hasMany |
|多→１|belongsTo|
|多対多| belongsToMany（中間テーブル）|

```php:例）テーブルの関連づけ（モデルに記述）
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Shop extends Model {
  // ココに記述↓
  Public function profile(){
    // リレーションの書き方の基本
    return $this -> リレーションの種類（'関連付けるモデルファイルのディレクトリ'）;

    // 1対1 の場合
    return $this->hasOne('App\Models\モデル名');
    return $this->hasOne('App\Models\モデル名', 'foreign_key');         // 外部キーの場合
    return $this->hasOne('App\Models\モデル名', 'foreign_key', 'local_key');

    // １対多 の場合
    return $this->hasMany('App\Models\モデル名');
    return $this->hasMany('App\Models\モデル名', 'foreign_key');       // 外部キーの場合
    return $this->hasMany('App\Models\モデル名', 'foreign_key', 'local_key');

    // 多対1 の場合
    return $this->belongsTo('App\Models\モデル名');
    return $this->belongsTo('App\Models\モデル名', 'foreign_key');    // 外部キーの場合
    return $this->belongsTo('App\Models\モデル名', 'foreign_key', 'owner_key');

    // 多対多 の場合
    return $this->belongsToMany('App\Models\モデル名');
    return $this->belongsToMany('App\Models\モデル名', 'role_user');                         // 第2引数： 繋げたモデル名（中間テーブル名）
    return $this->belongsToMany('App\Models\モデル名', 'role_user', 'user_id', 'role_id');   // 第3引数： このモデルのid名（外部キー）、第4引数：　繋げたいテーブルのid名（外部キー）
    return $this->belongsToMany('App\Models\User')->using('App\Models\RoleUser');

    // 仲介テーブルを通した関連付け
    return $this->hasOneThrough('App\Models\モデル名1', 'App\Models\モデル名2');
  }
}
```