Entity Framework Core < - > PostgreSQL 間の MAX/MIN 値のやりとり (数値データ編) 1
はじめに
Entity Framework Core (以下 EF Core)を使って PostgreSQL に数値データを追加したり取得したりしているときに、エラーが発生することがありました。
値を丸めたりして対処したりはしたものの、どういうデータでエラーが出るのか、ということが気になったので試してみることにしました。
数値データ編としましたが、他のデータ型については気が向いたらやります。
どちらかというと、 PostgreSQL に比較的最近?追加された JSON 型や Address 型などが扱えるのか?というのが気になるところではあります。
準備
とりあえず PostgreSQL のドキュメントを参考に、数値データを持つテーブルを作ります。
CREATE Table "NumberSample" ( "Id" serial PRIMARY KEY, "BigIntValue" bigint, "BigSerialValue" bigserial, "DoubleValue" double precision, "IntegerValue" integer, "NumericValue" numeric, "SerialValue" serial, "RealValue" real, "SmallIntValue" smallint, "SmallSerialValue" smallserial, "MoneyValue" money )
検証は、
- DB に各データ型の MAX/MIN 値を持つレコードを追加し、 EF Core でそれを検索
- C# の MAX/MIN 値の幅が DB 側より広い場合はその値を含むレコードを追加
- C# の MAX/MIN 値が DB 側より狭い場合は、 C# での MAX/MIN 値を含むレコードを追加
これでエラーになるかを調べ、エラーを抑える方法(数値を丸めるなど)も調べてみたいと思います。
なお C# 側の MAX/MIN 値は下記を参考にしています。
bigint
PostgreSQL における bigint の取り得る範囲は -9,223,372,036,854,775,808 ~ +9,223,372,036,854,775,807 。
C# でこれに近い範囲を持つ型は、 long 型です (-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807)。
INSERT INTO "NumberSample" ("BigIntValue") VALUES (-9223372036854775808); INSERT INTO "NumberSample" ("BigIntValue") VALUES (9223372036854775807);
BigSerialValue などのカラムは、 MIN 値( Id = 2 )、 MAX 値( Id = 3 )のレコードを更新する形で値を入れていくことにします。
NumberSample.cs
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EfCoreDataTypeSample.Models { [Table("NumberSample")] public class NumberSample { [Key] public int Id { get; set; } public long BigIntValue { get; set; } } }
HomeController.cs
using System; using System.Collections.Generic; using System.Threading.Tasks; using EfCoreDataTypeSample.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; namespace EfCoreDataTypeSample.Controllers { public class HomeController : Controller { private readonly EfCoreDataTypeSampleContext _context; public HomeController(EfCoreDataTypeSampleContext context) { _context = context; } [Route("/")] [Route("/Home")] [Produces("application/json")] public async Task< List< NumberSample>> Index() { return await _context.NumberSamples .ToListAsync(); } [Route("/Create")] public async Task Create() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { NumberSample newMinNumber = new NumberSample { BigIntValue = -9223372036854775808, }; _context.NumberSamples.Add(newMinNumber); NumberSample newMaxNumber = new NumberSample { BigIntValue = 9223372036854775807, }; _context.NumberSamples.Add(newMaxNumber); await _context.SaveChangesAsync(); transaction.Commit(); } } [Route("/Update")] public async Task< bool> Update() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { // Min NumberSample minNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 4); minNumber.BigIntValue = GetOppositeBigIntValue(minNumber.BigIntValue); NumberSample maxNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 5); maxNumber.BigIntValue = GetOppositeBigIntValue(maxNumber.BigIntValue); int result = await _context.SaveChangesAsync(); transaction.Commit(); return result > 0; } } private long GetOppositeBigIntValue(long originalValue) { if (originalValue <= 0) { return 9223372036854775807; } return -9223372036854775808; } } }
REST はいずこという感じですが、今回は気にしない方向で。。。
結果としては、検索、追加、更新ともに値が変化することなくやり取りできました。
ただ一点、 Index() で返される JSON を Firefox で見たところ、下記のように表示されていました。
生データをみると正しい値になっていたため、 JSON を整形して表示する上で発生しているだけのようですが、注意が必要かもしれません。
bigserial
PostgreSQL における bigserial の取り得る範囲は 1 ~ +9,223,372,036,854,775,807 。
C# でこれに近い範囲を持つ型は、 ulong 型です (0 ~ 9,223,372,036,854,775,807)。
UPDATE "NumberSample" SET "BigSerialValue" = 1 WHERE "Id" = 2; UPDATE "NumberSample" SET "BigSerialValue" = 9223372036854775807 WHERE "Id" = 3
HomeController.cs
~省略~ public async Task Create() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { NumberSample newMinNumber = new NumberSample { ~省略~ BigSerialValue = 0, }; _context.NumberSamples.Add(newMinNumber); NumberSample newMaxNumber = new NumberSample { ~省略~ BigSerialValue = 9223372036854775807, }; _context.NumberSamples.Add(newMaxNumber); await _context.SaveChangesAsync(); transaction.Commit(); } } [Route("/Update")] public async TaskUpdate() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { // Min NumberSample minNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 4); ~省略~ minNumber.BigSerialValue = GetOppositeBigSerialValue(minNumber.BigSerialValue); // Max NumberSample maxNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 5); ~省略~ maxNumber.BigSerialValue = GetOppositeBigSerialValue(maxNumber.BigSerialValue); int result = await _context.SaveChangesAsync(); transaction.Commit(); return result > 0; } } ~省略~ private ulong GetOppositeBigSerialValue(ulong originalValue) { if (originalValue <= 0) { return 9223372036854775807; } return 0; } } }
結果は、値が変化することなく検索、追加、更新されました。
あれ?
ulong は最小値が 0 のため、範囲から外れているのに。。。
と思ったら、カラムのデータ型としては bigint で設定されるため、明示的に指定して入れる分には先ほどと同じ範囲の値が扱える、ということのようです。
bigserial の範囲というのは、値が空であった場合に自動でインクリメントした値をセットする、シーケンスの取り得る範囲のようです。
double precision
double precision ですが、範囲は 15桁精度 となっています。
実際どの程度の桁数まで登録できるのかを確認したところ、整数部分はドキュメント通り 1E-307 ~ 1E+308 程度であり、私の環境で確認したところ、308 桁を超えるとエラーとなっていました。 小数点以下は 1000 桁ぐらいでもエラーにはならないのですが、インサート後は小数点第 15 位で四捨五入され(これが 15桁精度 ということなのかと)、それ以下は反映されません。
また 1E+308 のように指数表記が使われている場合、小数点以下が丸められてしまうようです。
C# で doule が持つ MAX/MIN 値として、 double.MaxValue/double.MinValue があります。
それぞれ +1.79769313486232e+308 / -1.79769313486232e+308 であり、これも合わせて試すことにします。
あと、小数点第 15 位以下を持つ値を使った場合も見てみたいと思います。
HomeController.cs
~省略~ [Route("/Create")] public async Task Create() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { NumberSample newMinNumber = new NumberSample { ~省略~ DoubleValue = double.MinValue }; _context.NumberSamples.Add(newMinNumber); NumberSample newMaxNumber = new NumberSample { ~省略~ DoubleValue = double.MaxValue }; _context.NumberSamples.Add(newMaxNumber); NumberSample newMinNumber2 = new NumberSample { ~省略~ DoubleValue = -99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999, }; _context.NumberSamples.Add(newMinNumber2); NumberSample newMaxNumber2 = new NumberSample { ~省略~ DoubleValue = 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999, }; _context.NumberSamples.Add(newMaxNumber2); NumberSample newMinNumber3 = new NumberSample { ~省略~ DoubleValue = -3.33333333333333333333333333333333333333, }; _context.NumberSamples.Add(newMinNumber3); NumberSample newMaxNumber3 = new NumberSample { ~省略~ DoubleValue = 3.33333333333333333333333333333333333333, }; _context.NumberSamples.Add(newMaxNumber3); await _context.SaveChangesAsync(); transaction.Commit(); } } [Route("/Update")] public async TaskUpdate() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { // Min NumberSample minNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 4); ~省略~ minNumber.DoubleValue = GetOppositeDoubleValue(minNumber.DoubleValue); // Max NumberSample maxNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 5); ~省略~ maxNumber.DoubleValue = GetOppositeDoubleValue(maxNumber.DoubleValue); // Min NumberSample minNumber2 = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 6); ~省略~ minNumber2.DoubleValue = GetOppositeDoubleValue2(minNumber2.DoubleValue); // Max NumberSample maxNumber2 = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 7); ~省略~ maxNumber2.DoubleValue = GetOppositeDoubleValue2(maxNumber2.DoubleValue); // Min NumberSample minNumber3 = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 8); ~省略~ minNumber3.DoubleValue = GetOppositeDoubleValue3(minNumber3.DoubleValue); // Max NumberSample maxNumber3 = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 9); ~省略~ maxNumber3.DoubleValue = GetOppositeDoubleValue3(maxNumber3.DoubleValue); int result = await _context.SaveChangesAsync(); transaction.Commit(); return result > 0; } } ~省略~ private double GetOppositeDoubleValue(double originalValue) { if (originalValue <= 0) { return double.MaxValue; } return double.MinValue; } private double GetOppositeDoubleValue2(double originalValue) { if (originalValue <= 0) { return 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999; } return -99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999; } private double GetOppositeDoubleValue3(double originalValue) { if (originalValue <= 0) { return 3.33333333333333333333333333333333333333; } return -3.33333333333333333333333333333333333333; } } }
結果は下記の通りです( maxNumber, minNumber などの名前は Create() のものです)。
- maxNumber: C#, DB ともに 1.79769313486232e+308
- minNumber: C#, DB ともに -1.79769313486232e+308
- maxNumber2: C#, DB ともに 1e+308
- minNumber2: C#, DB ともに -1e+308
- maxNumber3: C# では 3.3333333333333335 、 DB では 3.33333333333333
- minNumber3: C# では -3.3333333333333335 、 DB では -3.33333333333333
C# と PostgreSQL では、小数点の丸め方が異なるようです。
maxNumber3 、 minNumber3 の違いは、 C# から値を入れた場合も DB から取得した場合も両方同じ値になります。
Double なので正確性はないとはいえ、場合によっては注意が必要かもしれません。
NaN 、 Infinity
思い付きでこれらも確認してみることにしました。
HomeController.cs
~省略~ public async Task Create() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { ~省略~ NumberSample newMinNumber4 = new NumberSample { ~省略~ DoubleValue = double.NaN, }; _context.NumberSamples.Add(newMinNumber4); NumberSample newMinNumber5 = new NumberSample { ~省略~ DoubleValue = double.NegativeInfinity, }; _context.NumberSamples.Add(newMinNumber5); NumberSample newMaxNumber5 = new NumberSample { ~省略~ DoubleValue = double.PositiveInfinity, }; _context.NumberSamples.Add(newMaxNumber5); await _context.SaveChangesAsync(); transaction.Commit(); } } [Route("/Update")] public async TaskUpdate() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { ~省略~ // NaN NumberSample nanNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 10); ~省略~ nanNumber.DoubleValue = double.NaN; // Min NumberSample minNumber5 = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 11); ~省略~ minNumber5.DoubleValue = GetOppositeDoubleValue5(minNumber5.DoubleValue); // Max NumberSample maxNumber5 = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 12); ~省略~ maxNumber5.DoubleValue = GetOppositeDoubleValue5(maxNumber5.DoubleValue); int result = await _context.SaveChangesAsync(); transaction.Commit(); return result > 0; } } ~省略~ private double GetOppositeDoubleValue5(double originalValue) { if (originalValue <= 0) { return double.PositiveInfinity; } return double.NegativeInfinity; } } }
結果は、検索、追加、更新ともに値が変化することなく値が渡されていました。
ただし JSON では文字列として渡されていました。
おそらく変換自体はできるものと思いますが。
integer
PostgreSQL における integer の範囲は -2147483648 ~ +2147483647 。
C# でこれに近い範囲を持つ型は、int です。 ( -2147483648 ~ +2147483647 )
UPDATE "NumberSample" SET "IntegerValue" = -2147483648 WHERE "Id" = 2; UPDATE "NumberSample" SET "IntegerValue" = 2147483647 WHERE "Id" = 3
HomeController.cs
~省略~ public async Task Create() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { NumberSample newMinNumber = new NumberSample { ~省略~ IntegerValue = int.MinValue, }; _context.NumberSamples.Add(newMinNumber); NumberSample newMaxNumber = new NumberSample { ~省略~ IntegerValue = int.MaxValue, }; _context.NumberSamples.Add(newMaxNumber); await _context.SaveChangesAsync(); transaction.Commit(); } } [Route("/Update")] public async TaskUpdate() { using (IDbContextTransaction transaction = _context.Database.BeginTransaction()) { // Min NumberSample minNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 4); ~省略~ minNumber.IntegerValue = GetOppositeIntValue(minNumber.IntegerValue); // Max NumberSample maxNumber = await _context.NumberSamples .FirstOrDefaultAsync(n => n.Id == 5); ~省略~ maxNumber.IntegerValue = GetOppositeIntValue(maxNumber.IntegerValue); ~省略~ int result = await _context.SaveChangesAsync(); transaction.Commit(); return result > 0; } } ~省略~ private int GetOppositeIntValue(int originalValue) { if (originalValue <= 0) { return int.MaxValue; } return int.MinValue; } } }
結果は、検索、追加、更新ともに値が変化することなく値が渡されていました。
範囲外の値
ここまで C# 、 PostgreSQL のデータ型の取り得る値の範囲が同じであったこともあり、範囲内の値だけを用いて確認していました。
では、範囲外の値が入るとどうなるのでしょうか。
C# 側の値が大きすぎ・小さすぎた場合
IntegerValue の値を long に変更し、long.Max / long.Min を入れてみたところ、下記の例外が発生しました。
PostgresException: 22003: integer out of range
PostgreSQL 側の値が大きすぎ・小さすぎた場合
IntegerValue の値を short に変更し、検索を行ったところ、下記の例外が発生しました。
OverflowException: Arithmetic operation resulted in an overflow.
当然といえば当然ですが、エラーが起きる場所が異なりますね。
いったん切ります。