PostgreSQL で Entity Framework Core を使っていて、PostgreSQL の money 型の列を C# の decimal 型のプロパティにマップしたら、プロパティに格納される値がなんか変でハマった。テーブルに格納されている値の 100 分の 1 になってる。
試したのは下記のコード。
using Microsoft.EntityFrameworkCore; using System; using System.Threading.Tasks; namespace PgSample { public class Product { public long Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } public class ApplicationDbContext : DbContext { public ApplicationDbContext() : base() { Products = Set<Product>(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql( "Host=localhost;Username=postgres;Password=p@ssword;Database=sample"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>(b => { b.Property(e => e.Id) .UseNpgsqlSerialColumn() .HasColumnName("id"); b.Property(e => e.Name) .IsRequired() .HasColumnName("name"); b.Property(e => e.Price) .IsRequired() .HasColumnName("price") .HasColumnType("money"); b.ToTable("products"); b.HasKey(e => e.Id); }); } public DbSet<Product> Products { get; } } class Program { static void Main(string[] args) { MainAsync(args).GetAwaiter().GetResult(); Console.WriteLine("Press Enter Key."); Console.ReadLine(); } static async Task MainAsync(string[] args) { // テストデータを作りなおす using (var context = new ApplicationDbContext()) { await context.Database.EnsureDeletedAsync(); await context.Database.EnsureCreatedAsync(); context.Products.Add(new Product() { Name = "foo", Price = 12345, }); context.Products.Add(new Product() { Name = "bar", Price = 67890 }); await context.SaveChangesAsync(); } // Entity Framework Core で普通に取得 Console.WriteLine("EF Normal"); using (var context = new ApplicationDbContext()) { var products = await context.Products.ToListAsync(); foreach (var p in products) { Console.WriteLine( $"id:{p.Id}\tname:{p.Name}\tprice:{p.Price}"); } } // Entity Framework Core で SQL を指定して取得 Console.WriteLine("FromSql"); using (var context = new ApplicationDbContext()) { var products = await context.Products .FromSql("SELECT id, name, price FROM products") .ToListAsync(); foreach (var p in products) { Console.WriteLine( $"id:{p.Id}\tname:{p.Name}\tprice:{p.Price}"); } } // Entity Framework Core で money を numeric にキャストする SQL を指定して取得 Console.WriteLine("FromSql with convert"); using (var context = new ApplicationDbContext()) { var products = await context.Products .FromSql("SELECT id, name, price::money::numeric FROM products") .ToListAsync(); foreach (var p in products) { Console.WriteLine( $"id:{p.Id}\tname:{p.Name}\tprice:{p.Price}"); } } // NpgsqlConnection を直に使って取得 Console.WriteLine("ExecuteReaderAsync"); using (var context = new ApplicationDbContext()) { using (var connection = context.Database.GetDbConnection()) { await connection.OpenAsync(); using (var command = connection.CreateCommand()) { command.CommandText = "SELECT id, name, price FROM products"; using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { Console.WriteLine( $"id:{reader[0]}\tname:{reader[1]}\tprice:{reader[2]}"); } } } } } // NpgsqlConnection を直に使い、money を numeric にキャストする SQL を指定して取得 Console.WriteLine("ExecuteReaderAsync with convert"); using (var context = new ApplicationDbContext()) { using (var connection = context.Database.GetDbConnection()) { await connection.OpenAsync(); using (var command = connection.CreateCommand()) { command.CommandText = "SELECT id, name, price::money::numeric FROM products"; using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { Console.WriteLine( $"id:{reader[0]}\tname:{reader[1]}\tprice:{reader[2]}"); } } } } } } } }
実行結果がこちら。
Entity Framework Core の問題かと思ってたら、Npgsql を直に使った場合も同じだった。 Npgsql 内で PostgreSQL の money 型を C# の decimal 型にマップしているところに原因がありそう。 GitHub の Npgsql リポジトリで MoneyHandler のソースコードを見てみた。
短いのでクラスを引用。
[TypeMapping("money", NpgsqlDbType.Money, dbType: DbType.Currency)] class MoneyHandler : NpgsqlSimpleTypeHandler<decimal> { public override decimal Read(NpgsqlReadBuffer buf, int len, FieldDescription fieldDescription = null) => buf.ReadInt64() / 100m; public override int ValidateAndGetLength(decimal value, NpgsqlParameter parameter) => value < -92233720368547758.08M || value > 92233720368547758.07M ? throw new OverflowException($"The supplied value ({value}) is outside the range for a PostgreSQL money value.") : 8; public override void Write(decimal value, NpgsqlWriteBuffer buf, NpgsqlParameter parameter) => buf.WriteInt64((long)(Math.Round(value, 2, MidpointRounding.AwayFromZero) * 100m)); }
Read 時に 100m で割っている。怪しい。こいつが犯人か。 なぜこんな実装になっているのか調べたけど、ちょっと時間切れ。 money ではなく numeric を使った方が良かったかもな。