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.75→double型 -
5→integer型 -
6.5→double型
問題の本質
この問題は、以下の 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 つの要因が組み合わさると、以下のような問題が発生する。
- PDO が PHP の
integer型を検出 - SQL Server の型優先順位により、
int型として処理しようとする - しかし他の値(
7.75など)はintに変換できない - 結果として nvarchar(文字列)として解釈される
- 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()+明示的キャストの方が有利である。
実装時の推奨事項
私の意見としては、以下の方針を推奨する。
-
バルクインサート時は解決策①を採用
- パフォーマンスが重要な場面では
insert()+明示的キャスト - コードレビュー時に型キャストの漏れをチェック
- パフォーマンスが重要な場面では
-
単一レコード作成時は解決策②を採用
- 1 件ずつの作成であれば
create()を使用 - Eloquent の機能を最大限活用
- 1 件ずつの作成であれば
学んだこと
データベースごとの特性を理解する重要性
MySQL や PostgreSQL では問題なく動作するコードが、SQL Server では動作しない場合がある。クロスプラットフォーム対応を考える場合、各データベースの型システムの違いを理解する必要がある。
PDO の型推論に頼りすぎない
PDO は便利だが、自動型推論に頼りすぎると予期しない動作につながる。重要なデータ操作では、明示的な型指定を行うべきである。
まとめ
Laravel と SQL Server の組み合わせで DECIMAL 型を扱う際は、PHP の型の混在に注意が必要である。特にDB::table()->insert()を使用する場合、Eloquent のキャストがバイパスされるため、明示的な型キャストが不可欠である。
同様の問題に遭遇した際は、本記事で紹介した解決策を参考にしていただければ幸いである。
参考文献
- Laravel Framework Issue #28923: "MSSQL conversion error"
- Microsoft SQL Server Documentation: "Data Type Precedence (Transact-SQL)"
- Stack Overflow: "Error converting data type nvarchar to numeric when trying to create a record using Eloquent"
- PHP Manual: "PDO::prepare"
Discussion