🦌

Laravel の insert メソッドで SQL Server 型変換エラーが発生する理由

に公開

はじめに

業務システムの開発中、SQL Server と Laravel の組み合わせで次のエラーに遭遇した。DECIMAL 型のカラムにデータを一括挿入する際に、以下のようなエラーが発生したのである。

SQLSTATE[22018]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
Conversion failed when converting the nvarchar value '5.75' to data type int.

実行環境のデータベースの定義ではDECIMAL(5,2)型、Laravel のモデルでは'float'とキャストを設定し、リクエストバリデーションでも'numeric'ルールを設定していた。にもかかわらず、SQL Server は「nvarchar 値を int に変換できない」と訴えてくる。

本記事では、この問題の原因を掘り下げ、再現コードとともに解決策を提示する。

環境情報

  • Laravel:12.37.0
  • PHP:8.4

問題の発生状況

エラーが発生した実装

実際のコードは以下のようなものであった。

// データを一括挿入
$dataToInsert = array_map(
    fn ($item) => [
        'id' => (string) Str::uuid(),
        'related_id' => $relatedId,
        'value' => $item['value'], // ここに問題がある
    ],
    array_filter($items, fn ($item) => $item['value'] > 0)
);

Model::insert($dataToInsert);

このコードは一見問題なさそうに見える。しかし、値を頻繁に変更するテスト中、時々エラーが発生するようになった。

データベース定義

CREATE TABLE sample_table (
    id VARCHAR(36) PRIMARY KEY,
    related_id VARCHAR(36) NOT NULL,
    value DECIMAL(5,2) NOT NULL,  -- DECIMAL型として正しく定義
    -- ...
);

Eloquent モデル定義

class SampleModel extends Model
{
    protected $casts = [
        'value' => 'float',  // キャストも設定済み
    ];
}

すべて正しく設定されているはずなのに、なぜエラーが発生するのか。

原因の究明

PHP レベルでのデバッグ

まず、PHP 側で型を確認するため、以下のようなログを仕込んだ。

foreach ($dataToInsert as $item) {
    \Log::info('Value type check', [
        'value' => $item['value'],
        'type' => gettype($item['value']),
        'is_int' => is_int($item['value']),
        'is_float' => is_float($item['value']),
    ]);
}

結果は以下の通りであった。

[2025-11-24 11:23:00] Value type check {"value":7.75,"type":"double","is_int":false,"is_float":true}
[2025-11-24 11:23:00] Value type check {"value":5,"type":"integer","is_int":true,"is_float":false}
[2025-11-24 11:23:00] Value type check {"value":6.5,"type":"double","is_int":false,"is_float":true}

ここに問題の核心があった。PHP の型が混在しているのである。

  • 7.75double
  • 5integer
  • 6.5double

問題の本質

この問題は、以下の 3 つの要因が組み合わさって発生する。

1. PHP の型の自動判定

JSON から送られてくる数値は、以下のように自動的に型が決定される。

$value1 = 7.75;  // double型
$value2 = 5;     // integer型(小数部がないため)
$value3 = 5.0;   // double型(明示的に小数部を持つ)

2. SQL Server の型優先順位システム

SQL Server には型の優先順位が存在する[1]

int > float > decimal

同一カラムに異なる型の値が混在する場合、SQL Server は優先度の高い型に統一しようとする。

3. PDO SQL Server ドライバの型推論

Model::insert()メソッドは、Eloquent のキャストをバイパスする。そのため、PDO SQL Server ドライバが PHP の型から直接 SQL Server の型を推論することになる。

この 3 つの要因が組み合わさると、以下のような問題が発生する。

  1. PDO が PHP のinteger型を検出
  2. SQL Server の型優先順位により、int型として処理しようとする
  3. しかし他の値(7.75など)はintに変換できない
  4. 結果として nvarchar(文字列)として解釈される
  5. nvarchar を int に変換しようとしてエラー

類似事例の調査

この問題は既知のものであることが判明した。

Laravel Framework Issue #28923

GitHub の Laravel リポジトリで同様の問題が報告されている[2]

  • MSSQL conversion error
  • 原因 SQL Server の型優先順位システムにより、intの優先度が高いため他の値もintに変換しようとする
  • 結論 Laravel 側では完全に解決できない、SQL Server の仕様による制限

Stack Overflow の事例

Stack Overflow でも複数の類似報告が存在する[3]。特に「Error converting data type nvarchar to numeric when trying to create a record using Eloquent」というタイトルの質問では、Laravel のinsert()create()使用時に PDO の型推論による問題として報告されている。

再現コードの実装

この問題を検証するため、簡易的な再現コードを実装した。

テーブル定義

Schema::create('test_table', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name');
    $table->decimal('value', 5, 2); // DECIMAL(5,2) カラム
    $table->timestamps();
});

Eloquent モデル

class TestModel extends Model
{
    use HasUuids;

    protected $table = 'test_table';

    protected $fillable = ['name', 'value'];

    protected $casts = [
        'value' => 'float',
    ];
}

エラー再現コード

public function reproduceBug()
{
    DB::table('test_table')->truncate();

    // 型が混在したデータ
    $testData = [
        ['name' => 'Test 1', 'value' => 7.75],  // double型
        ['name' => 'Test 2', 'value' => 5],     // integer型 ← 問題の原因
        ['name' => 'Test 3', 'value' => 6.5],   // double型
    ];

    $dataToInsert = array_map(
        fn ($data) => [
            'id' => (string) Str::uuid(),
            'name' => $data['name'],
            'value' => $data['value'], // 型が混在
            'created_at' => now(),
            'updated_at' => now(),
        ],
        $testData
    );

    // SQL Serverでエラーが発生
    DB::table('test_table')->insert($dataToInsert);
}

このコードを SQL Server 環境で実行すると、期待通りエラーが発生する。

解決策

① 明示的な型キャスト(推奨)

最もシンプルで効果的な解決策は、すべての値を明示的にfloat型にキャストすることである。

$dataToInsert = array_map(
    fn ($data) => [
        'id' => (string) Str::uuid(),
        'name' => $data['name'],
        'value' => (float) $data['value'], // 明示的にfloatにキャスト
        'created_at' => now(),
        'updated_at' => now(),
    ],
    $testData
);

DB::table('test_table')->insert($dataToInsert);

メリット

  • シンプルで理解しやすい
  • バルクインサートのパフォーマンスを維持
  • 1 行の変更で解決

デメリット

  • すべての insert 箇所で明示的にキャストする必要がある

② Eloquent のcreate()メソッドを使用

Eloquent のcreate()メソッドを使用すれば、モデルで定義したキャストが自動的に適用される。

foreach ($testData as $data) {
    TestModel::create($data);
}

メリット

  • Eloquent のキャストが自動適用される
  • モデルのイベント(creating, created 等)が発火する
  • より「Eloquent 的」な書き方

デメリット

  • バルクインサートと比較してパフォーマンスが低下
  • 大量データの挿入には不向き

小規模なデータであれば差は小さいが、数百件・数千件のデータを扱う場合は、insert()+明示的キャストの方が有利である。

実装時の推奨事項

私の意見としては、以下の方針を推奨する。

  1. バルクインサート時は解決策①を採用

    • パフォーマンスが重要な場面ではinsert()+明示的キャスト
    • コードレビュー時に型キャストの漏れをチェック
  2. 単一レコード作成時は解決策②を採用

    • 1 件ずつの作成であればcreate()を使用
    • Eloquent の機能を最大限活用

学んだこと

データベースごとの特性を理解する重要性

MySQL や PostgreSQL では問題なく動作するコードが、SQL Server では動作しない場合がある。クロスプラットフォーム対応を考える場合、各データベースの型システムの違いを理解する必要がある。

PDO の型推論に頼りすぎない

PDO は便利だが、自動型推論に頼りすぎると予期しない動作につながる。重要なデータ操作では、明示的な型指定を行うべきである。

まとめ

Laravel と SQL Server の組み合わせで DECIMAL 型を扱う際は、PHP の型の混在に注意が必要である。特にDB::table()->insert()を使用する場合、Eloquent のキャストがバイパスされるため、明示的な型キャストが不可欠である。

同様の問題に遭遇した際は、本記事で紹介した解決策を参考にしていただければ幸いである。

参考文献

脚注
  1. Microsoft SQL Server Documentation: "Data Type Precedence (Transact-SQL)" ↩︎

  2. Laravel Framework Issue #28923 ↩︎

  3. Stack Overflow "Error converting data type nvarchar to numeric" ↩︎

GitHubで編集を提案

Discussion