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 ");
で取得しようとするとヒットせず 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)")]
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}");
}
}
}
}
}
実行するとちゃんと更新に成功する。