🙆

面談記録管理アプリ開発:データベース構築

2024/12/14に公開

はじめに

前回で開発環境の構築を行いました。

前回の記事はこちら↓
https://zenn.dev/kenberu1200/articles/20f5525068f492

今回は、データーベースの実装を行っていきたいと思います!

フィーチャーブランチの作成

今回は、データベース構築部分の実装していきたいので、それ専用のブランチを作成します。

作成するには以下のコマンドを実行します。
ブランチ名はdatabaseにしています。

git flow feature start database

ブランチが作成されているか確認します。

git branch
  develop
* feature/database
  main

無事作成されていることがわかりました。

ブランチをGitHub上にアップロード

今のままでは、ローカル上にブランチが存在しているだけなので、GitHub上には反映していきます。
反映するには以下のコマンドを実行します。

git flow feature publish database 

GitHub上で確認して、ブランチが追加さていれば成功です。

マイグレーションファイルの作成

続いて、データーベースを構築するためにマイグレーションファイルを作成して、テーブル内容を記述していきます。

今回作成したいテーブルは以下の4つ

  • offices
    • 事業所の情報を記録したテーブル
  • members
    • 施設利用者の情報を記録したテーブル
  • meeting_logs
    • 利用者さんとの面談記録をと保存するテーブル
  • messages
    • 職員間でのチャットメッセージを記録するテーブル

マイグレーションファイルを作成するには以下のコマンドを実行します。

php artisan make:migration create_offices_table
php artisan make:migration create_members_table
php artisan make:migration create_meeting_logs_table
php artisan make:migration create_messages_table

usersテーブルの設定

usersテーブルは、breezeをインストールした際に、自動的に設定されているテーブルですが、アプリの仕様に合わせて少し修正する必要があります。

以下のように修正していきます。

database/migrations/0001_01_01_000000_create_users_table.php
////省略////
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('avatar')->nullable();
    $table->string('email')->unique();
    $table->foreignId('office_id')->constrained('offices')->onDelete('cascade');
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->boolean('is_archive')->default(false);
    $table->boolean('is_admin')->default(false);
    $table->boolean('is_global_admin')->default(false);
    $table->timestamps();
        });
////省略////

アイコン画像のパスを保存するためのavatarカラム、所属する事業所を表す外部キーであるoffice_id、管理者権限の有無を表すis_adminis_global_adminカラムを追加しています。

officesテーブルの設定

続いて、事業所情報を保存するためのofficesテーブルのマイグレーションファイルを記述していきます。

database/migrations/2024_08_28_021056_create_offices_table.php
////省略////
public function up(): void
{
    Schema::create('offices', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->integer('zip_code');
        $table->string('address');
        $table->string('phone_number')->nullable();
        $table->boolean('is_archive')->default(false);
        $table->timestamps();
    });
}
////省略////

電話番号はnullでも良いとしています。

membersテーブルの設定

また、施設利用者の情報を保存するためのmembersテーブルのマイグレーションファイルを記述していきます。

database/migrations/2024_08_28_021204_create_members_table.php
////省略////
public function up(): void
{
    Schema::create('members', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('sex');
        $table->foreignId('office_id')->constrained('offices')->onDelete('cascade');
        $table->string('status');
        $table->string('characteristics');
        $table->string('notes')->nullable();
        $table->timestamps();
    });
}
////省略////

性別を表すsexbooleanで表現しようと思いましたが、男女以外の表現方法を想定してstringとしています。

また、利用している事業所をと関連付けるためにofficesテーブルからoffice_idを外部キーとして取得しています。

meeging_logsテーブルの設定

さらに、面談内容を記録するためのmeeting_logsテーブルのマイグレーションファイルを記述します。

database/migrations/2024_08_28_021215_create_meeting_logs_table.php
////省略////
public function up(): void
{
    Schema::create('meeting_logs', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
        $table->foreignId('member_id')->constrained('members')->onDelete('cascade');
        $table->integer('condition');
        $table->longText('meeting_log');
        $table->timestamps();
    });
}
////省略////

面談を行った職員と受けた利用者とを関連付けるためにmembers/usersテーブルからIDを外部キーとして持ってきています。

messagesテーブルの設定

最後に、職員間のチャットメッセージを記録するmessagesテーブルのマイグレーションファイルを記述します。

database/migrations/2024_08_28_021226_create_messages_table.php
////省略////
public function up(): void
{
    Schema::create('messages', function (Blueprint $table) {
        $table->id();
        $table->longText('message');
        $table->foreignId('sender_id')->constrained('users');
        $table->foreignId('meeting_logs_id')->constrained('meeting_logs')->onDelete('cascade');
        $table->timestamps();
    });
}
////省略////

送信者と受信者を判別するためにusersテーブルからIDを外部キーとして取得しています。

また、各メッセージは各面談に紐付いているため、meeting_logsテーブルからIDを外部キーとして持ってきています。

モデルとファクトリーの作成

Office

Officeモデルとファクトリーの生成には以下のコマンドを実行します。

php artisan make:Model Office -f

生成されたMemberモデルを以下のように修正します。

app/Models/Office.php
////省略////
class Office extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'zip_code',
        'address',
        'phone_number',
    ];

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function members()
    {
        return $this->hasMany(Member::class);
    }
}

$fillableには、複数カラムのへの同時挿入を許可するカラムを指定します。
ここで指定されていないカラムへの挿入を他カラムとの同時挿入で行った場合、例外エラーが出たり、許可していないカラムのデータが正しく記録されなかったりします。

また、users/membersテーブルとは1対多の関係になっているので、$this->hasMany()を用いて関係性を表現しています。

この時点では、Memberモデルを作っていないのでエラーが出ていると思います。

ファクトリは以下のようにか記述していきます。

database/factories/OfficeFactory.php
////省略////
class OfficeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => 'ITスクール'. fake()->city(),
            'zip_code' => fake()->postcode(),
            'address' => fake()->address(),
            'phone_number' => fake()->phoneNumber(),
            'created_at' =>fake()->dateTimeBetween('-1 year', 'now'),
        ];
    }
}

fake()メソッドを使って、各カラムにランダムな値を生成するようにしています。

created_atカラムには、dateTimeBetween()メソッドを使って、生成日時から1年以内の日時を生成するようにしています。

User

breezeインストール時点でモデルとファクトリーが生成されているので、それらを修正していきます。

まずはモデルから、

app/Models/User.php
////省略////
class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'avatar',
        'email',
        'office_id',
        'email_verified_at',
        'password',
        'is_admin',
        'is_main_office',
    ];
    ////省略////
    public function office()
    {
        return $this->belongsTo(Office::class);
    }
}

UserOffice多対1の関係なので、belongsTo()メソッドでそれを表現しています。

続いて、ファクトリです。

////省略////
public function definition(): array
{
    $officeId = fake()->randomElement(\App\Models\Office::pluck('id')->toArray());

    return [
        'name' => fake()->name(),
        'email' => fake()->unique()->safeEmail(),
        'email_verified_at' => now(),
        'password' => static::$password ??= Hash::make('password'),
        'office_id' => $officeId,
        'remember_token' => Str::random(10),
    ];
}
////省略////

office_idは、officesテーブルに存在しているIDの中でランダムなものを割り当てるようにしています。

Member

続いて、Memberモデルとファクトリを生成していきます。

Office生成時と同様に、以下のコマンドを実行します。

php artisan make:Model Member -f

生成されたモデルとファクトリを記述していきます。

まずはモデルから、

app/Models/Member.php
class Member extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'sex',
        'office_id',
        'status',
        'characteristics',
        'notes',
    ];

    public function office()
    {
        return $this->belongsTo(Office::class);
    }

    public function meetingLogs()
    {
        return $this->hasMany(MeetingLog::class);
    }
}

membersテーブルは、officesテーブルに対して多対1の関係で、'meeting_logs'テーブルに対して1対多の関係なので、office()/meetingLogs()メソッドでその関係性を表現しています。

次に、ファクトリです。

database/factories/MemberFactory.php
////省略////
public function definition(): array
{
    $officeId = fake()->randomElement(\App\Models\Office::pluck('id')->toArray());

    return [
        'name' => fake()->name(),
        'sex' => fake()->randomElement(['男性', '女性', 'その他']),
        'office_id' => $officeId,
        'status' => fake()->randomElement(['利用中', '利用中止', '利用終了']),
        'characteristics' => fake()->realText(10),
        'notes' => fake()->realText(100),
        'created_at' => fake()->dateTimeBetween('-1 year', 'now'),
    ];
}
////省略////

Userファクトリと同様に、$officeIdofficesテーブルに存在するID
からランダムにIDを取得しています。

sex/statusは、randomElementメソッドを使用して、出力候補の中からランダムに値を取得します。

created_atOfficeファクトリと同様に、dateTimeBetweenメソッドを用いて、シード実行時から1年以内のに日時を取得します。

MeetingLog

さらに、MeegingLogモデルとファクトリを生成していきます。

生成には、以下のコマンドを実行します。

php artisan make:Model MeetingLog -f
app/Models/MeetingLog.php
////省略////
class MeetingLog extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'user_id',
        'member_id',
        'condition',
        'meeting_log',
        'last_message_id',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function member()
    {
        return $this->belongsTo(Member::class);
    }

    public function messages()
    {
        return $this->hasMany(Message::class);
    }
}

meeting_logsテーブルは、users/membersテーブルとそれぞれ多対1の関係なので、user()/member()メソッドで関係性を表現しています。

また、messagesテーブルと多対1の関係なので、messages()メソッドで関係性を表現しています。

次に、ファクトリーです。

database/factories/MeetingLogFactory.php
public function definition(): array
{
    $officeId = fake()->randomElement(\App\Models\Office::pluck('id')->toArray());

    $userId = fake()->randomElement(\App\Models\User::where('office_id', '=', $officeId)->pluck('id')->toArray());

    $memberId = fake()->randomElement(\App\Models\Member::where('office_id', '=', $officeId)->pluck('id')->toArray());

    $createdAt = fake()->dateTimeBetween('-1 year', 'now')->format('Y-m-d');

    $title = fake()->randomElement(\App\Models\Member::where('id', '=', $memberId)
        ->pluck('name')
        ->toArray()) . '-' . $createdAt;


    return [
        'title' => $title,
        'user_id' => $userId,
        'member_id' => $memberId,
        'condition' => fake()->randomElement([1, 2, 3, 4, 5]),
        'meeting_log' => fake()->realText(200),
        'created_at' => $createdAt,
    ];
}

Message

最後に、Messageモデルとファクトリを生成します。

これまでと同様に、以下のコマンドを実行します。

php artisan make:Model Message -f

生成されたモデルとファクトリを記述していきます。

まずモデルを以下のように記述します。

app/Models/Message.php
class Message extends Model
{
    use HasFactory;

    protected $fillable = [
        'message',
        'sender_id',
        'meeting_logs_id',
    ];

    public function sender()
    {
        return $this->belongsTo(User::class);
    }

    public function meetingLog()
    {
        return $this->belongsTo(MeetingLog::class);
    }
}

次に、ファクトリを以下のように記述します。

database/factories/MessageFactory.php
class Message extends Model
{
    use HasFactory;

    protected $fillable = [
        'message',
        'sender_id',
        'meeting_logs_id',
    ];

    public function sender()
    {
        return $this->belongsTo(User::class);
    }

    public function meetingLog()
    {
        return $this->belongsTo(MeetingLog::class);
    }
}

messagesテーブルは、meeting_logs/usersテーブルと多対1の関係なので、その関係性をsender()/meetingLog()メソッドで表現しています。

シーダーの作成

ここまでで、各モデルとファクトリを実装することができたので、ダミーデータを実際に生成するためのシーダーを作成していきます。

シーダーの作成は、breezeインストール時にデフォルトで生成されているDatabaseSeeder.phpを使っていきます。

database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        Office::factory()->count(10)->hasUsers(10)->create();

        User::factory()->create([
            'name' => 'Kenberu',
            'email' => 'kenberu@example.com',
            'password' => bcrypt('12345678'),
            'office_id' => 1,
            'is_admin' => true,
            'is_global_admin' => true,
            'email_verified_at' => time(),
        ]);

        User::factory()->create([
            'name' => 'Hiroshi Akutsu',
            'email' => 'hakutsu@example.com',
            'password' => bcrypt('12345678'),
            'office_id' => 1,
            'is_admin' => true,
            'is_global_admin' => false,
            'email_verified_at' => time(),
        ]);

        Member::factory()->count(50)->create();

        MeetingLog::factory()->count(200)->create();

        Message::factory()->count(20000)->create();
    }
}

マイグレーション

ここまでで、データベースとダミーデータ生成の準備ができたので、実際にマイグレーションしていきます。

マイグレーションの実行には、以下のコマンドを実行します。

php artisan migrate:fresh --seed

今のままだと、おそらく以下のようなエラーが出ると思います。

  0001_01_01_000000_create_users_table ................ 65.92ms FAIL

 Illuminate\Database\QueryException 
  
  SQLSTATE[HY000]: General error: 1005 Can't create table `laravel`.`users` (errno: 150 "Foreign key constraint is incorrectly formed") (Connection: mysql, SQL: alter table `users` add constraint `users_office_id_foreign` foreign key (`office_id`) references `offices` (`id`) on delete cascade)

これは、usersテーブルを生成するときに、外部キーになっているofficesテーブルのIDをがないために起きています。

laravelのマイグレーションは、database/factoriesディレクトリ内のファイルを上から順番に実行していきます。

なので、usersテーブルのマイグレーションファイルがofficesテーブルのマイグレーションファイルよりあとに実行されるようにすれば解決します!

usersテーブルのマイグレーションファイルはデフォルトでdatabase/migrations/0001_01_01_000003_create_users_table.phpというような名前になっています。

一方、officesテーブルのマイグレーションファイルは、2024_08_28_021056_create_offices_table.phpとなっています。

なので、このofficesのマイグレーションファイルよりあとに配置されるように、usersのファイル名を変えれば良いことになります。

しかし、usersmeeting_logsにて外部キーとして引っ張ってきているので、meeting_logsよりも前に配置する必要があります。

私の場合、meeting_logsのファイル名は2024_08_28_021215_create_meeting_logs_table.phpとなっているので、usersのファイル名を2024_08_28_021200_create_users_table.phpにしました。

これで、再度マイグレーションを実行すると無事成功すると思います。

おわりに

DBの構築とダミーデータの生成ができました。

次回から、各種CRUD処理の実装を進めていきたいと思います!

ではでは!

Discussion