コンテンツにスキップ

users テーブル定義

テーブル概要

会員の基本情報を管理するマスターテーブルです。ECCUBEのdtb_customerテーブル構造を参考に、シンプルで正規化された設計となっています。

テーブル名: users 説明: 会員マスター 用途: 会員の基本情報、個人情報、連絡先、住所、ステータス等を管理

カラム定義

カラム名 NULL デフォルト キー 説明
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PK 主キー
name VARCHAR(255) NOT NULL - - フルネーム(後方互換性のため維持)
last_name VARCHAR(255) NULL NULL INDEX
first_name VARCHAR(255) NULL NULL INDEX
last_name_kana VARCHAR(255) NULL NULL - 姓カナ
first_name_kana VARCHAR(255) NULL NULL - 名カナ
email VARCHAR(255) NOT NULL - UNIQUE メールアドレス
email_verified_at TIMESTAMP NULL NULL INDEX メール認証日時
password VARCHAR(255) NOT NULL - - パスワード(ハッシュ化)
remember_token VARCHAR(100) NULL NULL - ログイン保持トークン
gender_id BIGINT UNSIGNED NULL NULL FK 性別ID(genders.id)
birth_date DATE NULL NULL INDEX 生年月日
job_id BIGINT UNSIGNED NULL NULL FK 職業ID(job_masters.id)
company_name VARCHAR(255) NULL NULL - 会社名
phone_number VARCHAR(14) NULL NULL INDEX 電話番号
country_id BIGINT UNSIGNED NULL NULL FK 国ID(countries.id)
postal_code VARCHAR(8) NULL NULL INDEX 郵便番号/ZIP
state_province VARCHAR(255) NULL NULL INDEX 都道府県/州/省
address_line1 VARCHAR(255) NULL NULL - 住所1(市区町村・番地)
address_line2 VARCHAR(255) NULL NULL - 住所2(建物名等)
address_line3 VARCHAR(255) NULL NULL - 住所3(追加情報)
customer_status ENUM NOT NULL 'provisional' INDEX 会員ステータス
mailmagazine_subscribed BOOLEAN NOT NULL FALSE INDEX メールマガジン購読
admin_note TEXT NULL NULL - 管理メモ
created_at TIMESTAMP NULL NULL INDEX 作成日時
updated_at TIMESTAMP NULL NULL - 更新日時
deleted_at TIMESTAMP NULL NULL INDEX 削除日時(ソフトデリート用)

インデックス

種別 名称 対象カラム 説明
PRIMARY KEY pk_users id 主キー
UNIQUE KEY uk_users_email email メールアドレスの重複防止
INDEX idx_users_last_name last_name 姓検索
INDEX idx_users_first_name first_name 名検索
INDEX idx_users_email_verified email_verified_at メール認証状態検索
INDEX idx_users_birth_date birth_date 生年月日検索
INDEX idx_users_phone phone_number 電話番号検索
INDEX idx_users_postal_code postal_code 郵便番号検索
INDEX idx_users_state_province state_province 都道府県/州検索
INDEX idx_users_status customer_status 会員ステータス検索
INDEX idx_users_mailmagazine mailmagazine_subscribed メルマガ購読検索
INDEX idx_users_created created_at 登録日時検索
INDEX idx_users_deleted deleted_at 削除日時検索(ソフトデリート用)

外部キー制約

名称 対象カラム 参照先 ON DELETE ON UPDATE 説明
fk_users_gender gender_id genders(id) SET NULL RESTRICT 性別削除時はNULL
fk_users_job job_id job_masters(id) SET NULL RESTRICT 職業削除時はNULL
fk_users_country country_id countries(id) SET NULL RESTRICT 国削除時はNULL

リレーション

関連先テーブル 関連タイプ 外部キー 参照先 ON DELETE ON UPDATE 説明
genders N:1 gender_id id SET NULL RESTRICT 会員は1つの性別に属する
job_masters N:1 job_id id SET NULL RESTRICT 会員は1つの職業に属する
countries N:1 country_id id SET NULL RESTRICT 会員は1つの国に属する
customer_addresses 1:N id user_id CASCADE RESTRICT 1つの会員は複数の配送先住所を持つ
point_histories 1:N id user_id CASCADE RESTRICT 1つの会員は複数のポイント履歴を持つ
orders 1:N id user_id RESTRICT RESTRICT 1つの会員は複数の注文を持つ

データ例

id name last_name first_name email customer_status phone_number postal_code state_province address_line1
1 山田 太郎 山田 太郎 yamada@example.com active 090-1234-5678 554-0001 大阪府 大阪市此花区高見1-2-3
2 佐藤 花子 佐藤 花子 sato@example.com provisional 080-9876-5432 100-0001 東京都 千代田区千代田1-1

備考・注意事項

既存からの変更点

新規追加カラム

基本情報 - last_name: 姓 - first_name: 名 - last_name_kana: 姓カナ - first_name_kana: 名カナ

個人情報 - gender_id: 性別ID(genders.id) - birth_date: 生年月日 - job_id: 職業ID(job_masters.id)

連絡先 - company_name: 会社名 - phone_number: 電話番号

住所(世界共通フォーマット) - country_id: 国ID(countries.id) - postal_code: 郵便番号/ZIP - state_province: 都道府県/州/省 - address_line1: 住所1(市区町村・番地) - address_line2: 住所2(建物名等) - address_line3: 住所3(追加情報)

ステータス・設定 - customer_status: 会員ステータス(enum: provisional, active, suspended, withdrawn) - mailmagazine_subscribed: メールマガジン購読フラグ

管理用 - admin_note: 管理メモ - deleted_at: 削除日時(ソフトデリート用)

残存カラム(後方互換性)

  • name: フルネーム(既存コード互換性のため維持)
  • アクセサでlast_name + first_nameから自動生成可能

会員ステータス(customer_status)

名称 説明
provisional 仮会員 メール認証前の会員
active 本会員 メール認証済みの有効会員
suspended 停止中 一時的に停止された会員
withdrawn 退会 退会した会員

購買情報の動的集計

購買関連情報はusersテーブルに持たず、Orderテーブルから動的に集計:

  • 購入回数: COUNT(orders)
  • 購入総額: SUM(orders.grand_total)
  • 初回購入日: MIN(orders.created_at)
  • 最終購入日: MAX(orders.created_at)

ポイント残高の動的集計

ポイント残高はusersテーブルに持たず、point_historiesテーブルから動的に集計:

SELECT
  SUM(CASE
    WHEN point_type = 'add' THEN points
    WHEN point_type IN ('use', 'expire') THEN -points
    ELSE 0
  END) as balance
FROM point_histories
WHERE user_id = ?
  AND (expires_at IS NULL OR expires_at > NOW())

住所設計の特徴

世界共通フォーマット

  • 5つのフィールド(country_id, postal_code, state_province, address_line1-3)で全世界対応
  • JSON不要でシンプル
  • 検索・ソートが高速

日本の例

  • country_id: 日本
  • postal_code: 554-0001
  • state_province: 大阪府
  • address_line1: 大阪市此花区高見1-2-3
  • address_line2: ○○マンション101号

アメリカの例

  • country_id: USA
  • postal_code: 10001
  • state_province: NY
  • address_line1: 123 Main Street
  • address_line2: Apt 4B
  • address_line3: New York

Eloquentモデル例

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model
{
    use SoftDeletes;
    protected $fillable = [
        'name',
        'last_name',
        'first_name',
        'last_name_kana',
        'first_name_kana',
        'email',
        'password',
        'gender_id',
        'birth_date',
        'job_id',
        'company_name',
        'phone_number',
        'country_id',
        'postal_code',
        'state_province',
        'address_line1',
        'address_line2',
        'address_line3',
        'customer_status',
        'mailmagazine_subscribed',
        'admin_note',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'birth_date' => 'date',
            'mailmagazine_subscribed' => 'boolean',
            'deleted_at' => 'datetime',
        ];
    }

    // リレーション
    public function gender(): BelongsTo
    {
        return $this->belongsTo(Gender::class);
    }

    public function job(): BelongsTo
    {
        return $this->belongsTo(Job::class);
    }

    public function country(): BelongsTo
    {
        return $this->belongsTo(Country::class);
    }

    public function customerAddresses(): HasMany
    {
        return $this->hasMany(CustomerAddress::class);
    }

    public function pointHistories(): HasMany
    {
        return $this->hasMany(PointHistory::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }

    // アクセサ
    public function getFullNameAttribute(): string
    {
        return trim(($this->last_name ?? '') . ' ' . ($this->first_name ?? ''));
    }

    public function getFullNameKanaAttribute(): string
    {
        return trim(($this->last_name_kana ?? '') . ' ' . ($this->first_name_kana ?? ''));
    }

    public function getCurrentPointsAttribute(): int
    {
        return $this->pointHistories()
            ->where(function ($query) {
                $query->whereNull('expires_at')
                    ->orWhere('expires_at', '>', now());
            })
            ->get()
            ->sum(function ($history) {
                return match ($history->point_type) {
                    'add' => $history->points,
                    'use', 'expire' => -$history->points,
                    default => 0,
                };
            });
    }

    public function getTotalPurchasesAttribute(): int
    {
        return $this->orders()->count();
    }

    public function getTotalSpentAttribute(): float
    {
        return $this->orders()->sum('grand_total');
    }

    public function getLastPurchaseDateAttribute(): ?\Carbon\Carbon
    {
        return $this->orders()->max('created_at');
    }
}