PostgreSQL で character(n) 型の列を主キーにしていたら Entity Framework Core で酷い目にあった

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}");
                }
            }
        }
    }
}

このコードを実行すると、次のような例外が発生してしまう。

f:id:griefworker:20180522162315p:plain

原因

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");

で取得した bookCode プロパティには、後ろに空白が 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}");
                }
            }
        }
    }
}

実行すると、ちゃんと更新できる。

f:id:griefworker:20180522162324p:plain

「どうしても 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}");
                }
            }
        }
    }
}

実行するとちゃんと更新に成功する。

f:id:griefworker:20180522162333p:plain

魚忠

ランチで無性に魚が食べたくなったので、今泉にある『魚忠』に行ってみた。 ここは『田中田』や『わっぱ定食堂』の系列店で、同じく系列店『ナカタナカ』があった場所でもある。 ナカタナカは行こうとしたら運悪く閉店直後だったので、今回はリベンジみたいなものかな。

f:id:griefworker:20180522182653j:plain

『本日のお刺身と選べるメイン膳(1種類)』を注文。メインはひっじょ〜に悩みに悩んで、アジフライを選んだ。 トンカツや唐揚げにも惹かれたけど、いや待てと。魚を食べに来たんだろ?と。

本日のお刺身はゴマサバ。 ゴマサバは至高だな。 プリプリの食感がたまらない。 こっちでしか食べることができないと聞いたときはショックだった。

アジフライは外サクッと中はふっくら仕上がっていて、こちらも絶品。 アジフライにして正解だった。

ご飯のおかわりも無料でできて、 味ボリュームともに満足。 さすが田中田クオリティ。 ランチで魚が食べたくなった場合の候補として、今のところ1位2位を争うと思う。

関連ランキング:定食・食堂 | 西鉄福岡駅(天神)天神南駅薬院駅

『星野、目をつぶって。(11)』を読んだ

久しぶりに弓削(元)先生登場。以前はかっこいい感じの先生だった気がするが、 再登場ではダメな大人って感じでギャップが激しい。 こっちが素なんだろうけど。

小早川の「変わりたい」という願いは、ほぼ叶ったようなものかな。 星野の助力で変われた小早川が、今度は星野が変わるのを助けるという展開。 ジャージ女として活動してきたので、素顔の星野が受け入れられる土壌は既にあると思うから、 元いた榊のグループとの和解が課題になりそう。

それはそうと、松方と加納はこれで終戦なんだろうか。 個人的には加納が気に入ってただけに、このままフェードアウトっていうのは勘弁。

麺食堂 歩ごころ

会社帰りに、長浜にある『麺食堂 歩ごころ』に行ってみた。最近は福岡で非豚骨系のラーメンが増えていて、それは長浜ラーメンのお膝元でも例外ではないみたい。

『潮ラーメン』を注文。長浜という立地もあって、塩ではなく潮というのがお上品に感じた。

和食の経験を発揮して作られた、出汁を生かしたラーメン。出汁を味わうなら塩に限ると、どこのだれが言ったかは忘れたけど、出汁の風味と旨味をしっかり味わうことができた。ラーメンの他にもお酒や居酒屋メニューも提供していて、締めの一杯にもよさそう。

『金の彼女 銀の彼女(10)』を読んだ

ついに最終巻。 金の姫と銀の姫どちらが登郎と結ばれるかの勝負は、予想通りハーレムエンドで、 やっぱりこれしかないよなと思った。 綾乃峰は治外法権みたいなものだし。 まぁ厳密にはハーレムエンドとは違うんだけど。 むしろこれからって感じか。

登郎は二人のうちどちらか片方を選ぶことができなかったわけだけど、もとは同じ人物だったわけなので、選ぶのは酷というもの。 むしろ、二人とも手に入れるために命までかけていて、ここまで突き抜けると見ているほうも清々しい。 男の中の男。 あっぱれだと思う。

フルフル天神パン工房

天神周辺でランチと思ったら、めぼしい店がどこも多くてランチ難民に。昼休みも残り30分を切り、流石に店で食べる時間が無くなったので、たまたま目に入った『フルフル天神パン工房』へ。そういえばここのパンを食べたことなかった気がする。

明太フランスがイチオシみたいだったが、パンに明太子は個人的にナシなので、カツサンドを購入。

カツは程よく肉厚でいて、噛み切るのが容易なぐらい柔らかい。やはりカツはヒレに限る。そしてソースが、甘味と塩味と酸味のバランスが良い。

総合力の高いカツサンドだった。自分の中でカツサンドといったらまい泉だったけど、どちらを買うか毎度悩みそうなくらいに気に入った。

関連ランキング:パン | 天神駅西鉄福岡駅(天神)天神南駅

伊都きんぐ

5月とは思えない暑さで、長袖で外出したのを後悔するレベル。これは冷たいものを食べて身体の中から冷やさないと倒れかねない(大げさ)。冷たいものを求めて中央区今泉にある『伊都きんぐ』に向かった。

休日のランチタイムにスイーツを食べる人は少ないのか、すぐに席に案内されてホッと一息。前から気になっていた『あまゴリ』を注文した。

凍らせたイチゴだけで作られたかき氷。かき氷と呼ぶのはもはやふさわしくない。これでもかっていうイチゴの存在感に圧倒される。

お好みで練乳をかけてもいいと言われたが、既に結構かかっている。イチゴ自体が甘いので、そのままでもスプーンが止まらない。練乳をたっぷり追加するとさらに美味い。至福の時間。そしてお腹にたまった。1年分のイチゴを食べた感じ。このあまゴリを経験したら、そこらの店でかき氷を食べるなんて、お金が勿体無いと思えてしまう。