PostgreSQL で Entity Framework Core(Npgsql.EntityFrameworkCore.PostgreSQL) を使っていて、 テーブルの主キーの列に character(n) 型を使っていたら、 エンティティの更新に失敗して嵌った。
再現サンプルは下記の通り。
using Microsoft.EntityFrameworkCore; using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; namespace NpgsqlEFSample { [Table("books")] public class Book { [Key] [Required] [Column("code", TypeName = "character(20)")] public string Code { get; set; } [Column("title")] public string Title { get; set; } } public class ApplicationDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql("Host=localhost;Username=postgres;Password=p@ssword;Database=sample"); public DbSet<Book> Books => Set<Book>(); } class Program { static void Main(string[] args) { try { MainAsync(args).GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine(ex.Message); } 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.Books.Add(new Book() { Code = "12345", Title = "Foo", }); await context.SaveChangesAsync(); } using (var context = new ApplicationDbContext()) { var book = await context.Books .FirstOrDefaultAsync(b => b.Code == "12345"); book.Title = "Bar"; await context.SaveChangesAsync(); } using (var context = new ApplicationDbContext()) { var books = await context.Books.ToListAsync(); foreach (var book in books) { Console.WriteLine($"{book.Code}:{book.Title}"); } } } } }
このコードを実行すると、次のような例外が発生してしまう。
原因
context.Books.Add(new Book() { Code = "12345", Title = "Foo", }); await context.SaveChangesAsync();
を実行すると、Code
プロパティにマップしている code
列は character(20)
型なので、
テーブルには 12345
(後ろに空白が 15 個ある) が保存される。
var book = await context.Books
.FirstOrDefaultAsync(b => b.Code == "12345");
で取得した book
の Code
プロパティには、後ろに空白が 15 個ある 12345
が格納されている。
book.Title = "Bar";
await context.SaveChangesAsync();
を実行すると、Entity Framework Core は主キーである code
列の値が 12345
(後ろに空白が 15 個ある)
の行を更新しようとするが、後ろに空白を含んでいるとなぜか条件に引っかからず、更新できない。
そして 1 行更新しようとしたけど更新できなかったから例外発生。
試しに
var book = await context.Books .FirstOrDefaultAsync(b => b.Code == "12345 "); // 12345 の後ろに空白が 15 個ある
で取得しようとするとヒットせず null が返ってくる。
ちなみに、Npgsql.EntityFrameworkCore.PostgreSQL ではなく、Npgsql を直接使った場合も、 後ろに空白を含む文字列で検索してもヒットしなかった。
対策1 varchar に変更する
character ではなく varchar を使うのが手っ取り早いし、 根本的な解決になる。
using Microsoft.EntityFrameworkCore; using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; namespace NpgsqlEFSample { [Table("books")] public class Book { [Key] [Required] [Column("code", TypeName = "varchar(20)")] // varchar を使えば後ろに空白は入らない public string Code { get; set; } [Column("title")] public string Title { get; set; } } public class ApplicationDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql("Host=localhost;Username=postgres;Password=p@ssword;Database=sample"); public DbSet<Book> Books => Set<Book>(); } class Program { static void Main(string[] args) { try { MainAsync(args).GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine(ex.Message); } 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.Books.Add(new Book() { Code = "12345", Title = "Foo", }); await context.SaveChangesAsync(); } using (var context = new ApplicationDbContext()) { var book = await context.Books .FirstOrDefaultAsync(b => b.Code == "12345"); book.Title = "Bar"; await context.SaveChangesAsync(); } using (var context = new ApplicationDbContext()) { var books = await context.Books.ToListAsync(); foreach (var book in books) { Console.WriteLine($"{book.Code}:{book.Title}"); } } } } }
実行すると、ちゃんと更新できる。
「どうしても character でないといけない」という場合は使えないけど、 新規に作成するデータベースでそんなことあるかなぁ。
対策2 空白を除去してから保存する
スマートなやり方ではないけど、SaveChangesAsync
で保存する前に、
キーのプロパティに格納されている文字列から空白を除去してやれば、
無事保存される。
using Microsoft.EntityFrameworkCore; using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; namespace NpgsqlEFSample { [Table("books")] public class Book { [Key] [Required] [Column("code", TypeName = "character(20)")] public string Code { get; set; } [Column("title")] public string Title { get; set; } } public class ApplicationDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql("Host=localhost;Username=postgres;Password=p@ssword;Database=sample"); public DbSet<Book> Books => Set<Book>(); } class Program { static void Main(string[] args) { try { MainAsync(args).GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine(ex.Message); } 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.Books.Add(new Book() { Code = "12345", Title = "Foo", }); await context.SaveChangesAsync(); } using (var context = new ApplicationDbContext()) { // 変更を追跡しない var book = await context.Books .AsNoTracking() .FirstOrDefaultAsync(b => b.Code == "12345"); // 変更を追跡していないので、キーのプロパティを編集できる book.Code = book.Code.TrimEnd(); book.Title = "Bar"; context.Books.Update(book); await context.SaveChangesAsync(); } using (var context = new ApplicationDbContext()) { var books = await context.Books.ToListAsync(); foreach (var book in books) { Console.WriteLine($"{book.Code}:{book.Title}"); } } } } }
実行するとちゃんと更新に成功する。