vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

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
)

検証は、

  1. DB に各データ型の MAX/MIN 値を持つレコードを追加し、 EF Core でそれを検索
  2. C# の MAX/MIN 値の幅が DB 側より広い場合はその値を含むレコードを追加
  3. 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() で返される JSONFirefox で見たところ、下記のように表示されていました。

f:id:mslGt:20190329063723j:plain

生データをみると正しい値になっていたため、 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 Task Update()
        {
            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 Task Update()
        {
            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 Task Update()
        {
            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 Task Update()
        {
            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.

当然といえば当然ですが、エラーが起きる場所が異なりますね。

いったん切ります。

参照