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 | - | 名カナ |
| 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 | メールアドレスの重複防止 | |
| 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 | 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');
}
}