色んな事を書く

色んな事を書く

シンプルさを極めたエンジニアになりたい

EF の再試行

Entity Framework 6 からリトライストラテジーを使って再試行の振る舞いを設定できるようになりました。

こちらの記事を読むと DbConfiguration を使って再試行ストラテジーを仕込む方法が紹介されています。DbConfiguration も EF 6 から使えるようになったみたいですね。

https://github.com/dotnet/EntityFramework.Docs/blob/main/entity-framework/ef6/fundamentals/connection-resiliency/retry-logic.md

SQL Server でのリトライ

SQL Server と Entity Framework を使う時にこんなコードを書くことがあると思います。

builder.Services.AddDbContext<MyDbContext>(options =>
{
    options.UseSqlServer("");
});

DI コンテナに DbContext を登録する際に、SQL Server の接続文字列を渡してます。UseSqlServer メソッドの第二引数には Action<SqlServerDbContextOptionsBuilder> を渡せます。なのでこういう風に書くと、割とシンプルに再試行ストラテジーを設定できます。

builder.Services.AddDbContext<ChangeFeedDbContext>(options =>
{
    options.UseSqlServer("", sqlServerOptions =>
    {
        // 再試行ストラテジーに SqlServerRetryingExecutionStrategy を使う
        // https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.sqlserverretryingexecutionstrategy?view=efcore-8.0
        sqlServerOptions.EnableRetryOnFailure();
    });
});

SqlServerRetryingExecutionStrategyを使って再試行をしてくれるようです。このクラスのコンストラクタを見ると、再試行回数や再試行のディレイを受け取ってくれるみたいですね。これだけ見ると Exponecial Backoff でやってくれるのかしら?と思っちゃいます。

EnableRetryOnFailure にはいくつかオーバーロードがあって、再試行回数やディレイの時間を渡せます。これで良しなに再試行ストラテジーを作れます。

ExecutionStrategy を使えばカスタムの再試行ストラテジーを使えます。

builder.Services.AddDbContext<ChangeFeedDbContext>(options =>
{
    options.UseSqlServer("", sqlServerOptions =>
    {
        sqlServerOptions.ExecutionStrategy(d => new CustomRetryStrategy());
    });
});


class CustomRetryStrategy: IExecutionStrategy
{

}

もちろんこのコードはビルドエラーが起きます。

ユーザがトランザクションを明示的に開始したい場合は工夫が必要

例えば以下のようなコードを書いていたとします。※普段はこんなコード書きませんが、よい例が浮かばなかったので。

async Task RetryWithImplicitTransaction()
{

    var option = new DbContextOptionsBuilder().UseSqlServer("").Options;
    var context = new MyContext(option);

    using var transaction = await context.Database.BeginTransactionAsync();

    var goods = new Goods { Name = "Drink", Price = 100 };
    var order = new Order { Goods = new List<Goods> { goods } };
    
    await context.Orders.AddAsync(order);
    await context.SaveChangesAsync();

    order.Goods.Add(new Goods
    {
        Name = "Food",
        Price = 1000
    });
    
    var orders = await context.Orders.ToArrayAsync();
    
    var totalPrice = orders.Sum(o => o.Goods.Sum(g => g.Price));
    var user = new User { TotalPurchasePrice = totalPrice };
    
    await context.Users.AddAsync(user);
    
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}

Food を作成し TotalPurchasePrice を更新するタイミングでデータベース操作が失敗しても再試行は行われません

そもそも一貫性を保つ必要があるので、2 度目の SaveChanges だけをやり直すわけにはいきません。2 度目の SaveChangesAsync は 1. FoodOrderに追加、2. TotalPurchasePriceの更新、をやっています。この時に 1 は成功して 2 は失敗し、2度目の SaveChangesAsync を丸ごとやり直すとどうなるでしょうか?本来はFoodを1つ追加できればよかったのに2つ目が追加され、その結果TotalPurchasePriceも増えて整合性が取れなくなってしまいます。

じゃ1度目のSaveChangesAsync からやり直せばよいのですが、そのための情報がありません。なぜなら SaveChangesAsyncを呼び出したタイミングでChangeTrackerがリフレッシュされるからです。

という事で、トランザクションのうちどれか一つでもデータ操作が失敗するとそのトランザクション内の他の操作もロールバックして、もう一度最初からやり直したいわけですが、このままだと出来ません。と理解しています。間違ってたらごめんなさい。

なので、ユーザが明示的にトランザクションを開始する場合には、どの範囲の処理をリトライするのかも明示的にしてあげなければなりません。例えば以下のようにします。

async Task RetryWithImplicitTransaction()
{
    var option = new DbContextOptionsBuilder().UseSqlServer("").Options;
    var context = new MyContext(option);

    var strategy = context.Database.CreateExecutionStrategy();
    await strategy.Execute(async () =>
    {
        using var transaction = await context.Database.BeginTransactionAsync();

        var goods = new Goods { Name = "Drink", Price = 100 };
        var order = new Order { Goods = new List<Goods> { goods } };
        
        await context.Orders.AddAsync(order);
        await context.SaveChangesAsync();

        order.Goods.Add(new Goods
        {
            Name = "Food",
            Price = 1000
        });
        
        var orders = await context.Orders.ToArrayAsync();
        
        var totalPrice = orders.Sum(o => o.Goods.Sum(g => g.Price));
        var user = new User { TotalPurchasePrice = totalPrice };
        
        await context.Users.AddAsync(user);
        
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    });
}

strategy を作り、失敗した場合に再試行をするスコープを明示的にしてあげてください。これをすると2度目のSaveChangesAsyncで失敗した場合は、1度目のSaveChangesAsyncの内容もロールバックして、最初からトランザクションをやり直すという事になるはずですー。

DRY 原則

DRY ってなんだったっけ

DRY (Don't Repeat Yourself) 原則は「達人プログラマー:熟達に向けたあなたの旅」にて初めて登場したプログラミング原則のようですね。この本では以下のように記載されています。

すべての知識はシステム内において、単一、かつ明確な、そして信頼出来る表現になっていなければならない。

通化し、システムの中で単一であるべきなのは「知識」であって「コード」ではないという事が大事なポイントだと思います。

そもそも「知識」とはいったいなんやねんという話なんですが、これは「ソフトウェアが課題を解決するために必要な知識」だと考えています。

具体例で考えていきましょう。EC サイトを運営しているとします。以下このサイトの仕様です。

  • 購入者は商品を買うとユーザランクに応じてポイントを獲得できる
  • ユーザランクはブロンズ、シルバー、ゴールド、プラチナ、の 4 段階ある
  • ユーザランク毎の獲得できるポイントの倍率は異なる
  • ポイントの計算は税込み前の価格に対して行われる

ポイントを還元させて、ユーザの購買意欲を高める効果を狙っているのでしょうかねー。そうすると、「商品購入数が下がった」といった課題があり、「ユーザの購買意欲の向上」を目指してこういう仕様を考えたのかもしれません。

ソフトウェアでこの仕様を果たすため (=課題を解決するするため) に必要な知識が「ポイント還元率」という事になりますね。まぁ、例としてこういうものを考えてみます。

さて、実際に購入者が商品を購入したときに得られるポイントを計算するには具体的に以下の知識が必要になるはずです。

  1. 購入者のユーザランクは何か
  2. ユーザランク毎のポイント還元率は何%か

じゃ、ユーザのランクをプラチナとして、ポイント還元率は10%としましょう。商品を買ってポイントと支払金額を計算するプログラムは以下のようになりそうです。

void Main()
{
    
    var goods = new Goods(1000);
    
    var consumer = new Consumer(ConsumerRank.Platinum);
    
    // 商品購入処理
    var tax = goods.Price * 0.1;
    var point = goods.Price * 0.1;
    
    var payment = goods.Price + tax;
}


class Consumer
{
    private ConsumerRank Rank { get; }
    
    public Consumer(ConsumerRank rank)
    {
        Rank = rank;
    }
}

enum ConsumerRank
{
    Bronze,
    Silver,
    Gold,
    Platinum
}

class Goods
{
    public int Price { get; }
    public Goods(int price)
    {
        Price = price;
    }
}

はい、適当ですかこんな感じですかね。夫、ここで注目してほしいのですが、「goods.Price * 0.1」というコードが二箇所に出てきていますね。重複しているし、同じことやってるんだからまとめてしまおう!

private int Calc(Goods goods)
{
    return (int)Math.Floor(goods.Price * 0.1);
}

...はい。これは DRY を誤解しています。なぜか。それは

  • ユーザランク毎のポイント還元率
  • 消費税

と本質的に異なる知識を同一なものとしてコード上で表現してしまっているからです。見かけ上は 10% で同じように見えるかもしれませんが、値が同じでも意味が異なります。

DRY を誤って使ってしまうとどうなるでしょうか?例えば「プラチナランクのポイント還元率を15%にする」という仕様変更があったとします。太っ腹ですね。

顧客的には嬉しいかもしれませんが、開発者的にはどうでしょうか?Calcメソッドを使って計算していた部分を検索し、一つずつ修正していかないといけませんね。漏れてしまえばポイント還元率が異なりビジネス的に大ダメージを受けてしまうかもしれません。少なからずユーザ体験は悪いでしょうね。

じゃ、DRY 原則を守れているとどうなるんでしょうね?例えばこんなコードを書いておきましょう。

void Main()
{
    
    var goods = new Goods(1000);
    
    var consumer = new Consumer(ConsumerRank.Platinum);
    
    // 商品購入処理
    var totalPrice = Casher.CalcTotalPrice(goods);
    var point = goods.Price * consumer.PoinReturRate;
}


class Consumer
{
    private ConsumerRank Rank { get; }
    
    public Consumer(ConsumerRank rank)
    {
        Rank = rank;
    }

    public double PoinReturRate => Rank switch
    {
        ConsumerRank.Bronze => 0.01,
        ConsumerRank.Silver => 0.03,
        ConsumerRank.Gold => 0.05,
        ConsumerRank.Platinum => 0.1,
        _ => throw new ArgumentOutOfRangeException(nameof(Rank), "")
    };
}

static class Casher
{
    private static readonly double TaxRate = 0.1;
    
    public static int CalcTotalPrice(Goods goods) => (int)Math.Floor(goods.Price * TaxRate);
    
    public static int CalcTotalPrice(Goods[] goods) => (int)Math.Floor(goods.Sum(g => g.Price) * TaxRate);
}

enum ConsumerRank
{
    Bronze,
    Silver,
    Gold,
    Platinum
}

class Goods
{
    public int Price { get; }
    public Goods(int price)
    {
        Price = price;
    }
}

これだとランク毎のポイント還元率と消費税が分離されていて、先ほどの仕様変更も楽に行えそうです。あっちこっちにポイント還元率が登場しているわけでもなく、プラチナに対応する還元率を変えれば他の部分はいじる必要もないですね。本当かな?

いやーこれは例が悪かったかもしれません。とにかく、

  • DRY は知識を繰り返し記述することを避けようね
  • 「見かけのコードが同じ」だからと言って「知識が同じ」というわけではないよ

と言っていることが伝われば幸いです。

Azure Function の Timer Trigger の色々

  • Timer Trigger Function とは
    • Blob に記録する実行状態
  • Timer Trigger Function はどうやって定期実行をやっているのか
  • TimerTrigger Attribute で指定出来る事
    • Schedule に CRON 式か TimeSpan を指定し Function の起動間隔を定義する
    • RunOnStartup=true にすると、Function Runtime の起動時に Function を実行できる
    • UseMonitor で Blob にスケジュールを記録するか切り替える
  • 何故スケールアウトされても単一のインスタンスで実行されるのか
続きを読む

イベント登壇振り返り

人生で初めてのイベント登壇を行いました。最初は社内のイベントだし、想定人数も少なめだったので軽い気持ちで臨んでいました。外部発信力を強めていきたかったので、その経験になればなという感じです。

しかし、イベントをオープンにしてからあれよあれよと人が集まり、界隈で超有名なトッププレイヤーの方までもが参加してくださいました (参加者が増えたのはそういった方達の Tweet 効果もあると思います。ありがとうございます)。

そういった経験を経て、自分がイベントまでの期間をどう過ごしたのか、その期間でどう変わったのかについて振り返ろうと思います。

  • ハードスキルの成長
    • 技術に対するアプローチの仕方
    • GraphQL
    • .NET, C#
    • プロダクトのアーキテクチャ・設計思想
  • ソフトスキルの成長
    • 登壇に対するスタンス
    • メンタル面の訓練
    • 朝活(?)
  • 今後やっていきたい事
続きを読む

値型とか参照型とかその他もろもろ

Span<T>Memory<T> の違いを言語化しようと思って勉強してたら ref 構造体とは何ぞ???となってしまったので基本から学びなおします。

  • 値型と参照型
    • 値型
      • GetHashCode と Boxing
      • 構造体のメモリ配置
    • 参照型
      • クラスのメモリ配置
  • 変数の値渡しと参照渡し
    • ref
    • in
    • out
  • 値型の値渡しと参照渡し
  • 参照型の値渡しと参照渡し
  • 参照戻り値と参照ローカル変数
続きを読む

GraphQL の色んな記事まとめ

自分用に GraphQL を学んだ際に参考にした記事たちとその内容をまとめて残しておきます。

  • GraphQLスキーマ設計の勘所
    • ページベースのページングの問題点
  • GraphQL実践ノウハウv2
  • GraphQL 成熟度モデルの紹介と、プロダクトに当てはめた事例 / GraphQL maturity model
    • node と egde の違いを正しく理解しよう
    • Relay Connection によるページネーション
  • GraphQLはどんな時に使うか
    • GraphQLを最速でマスターするための意識改革3ヶ条
    • GraphQLは何に向いているか
    • モニタリングとログ記録
    • Batching について、 GraphQL の文脈で
  • GraphQLクライアントの技術選定 2023冬
    • 型の自動生成の観点
  • 今こそ思い出すGraphQLの特徴
  • AWS AppSync入門(GraphQL、AWS CDK)
  • [AWS Dev Day 2021 A-1] ゼロから始めるAWS AppSync導入の軌跡と振り返り 〜課題も添えて〜
  • GraphQL 入門 ~ REST と比較しながら学ぶ ~
  • ソニー: GraphQLを活用したデータ配信プラットフォームの事例紹介
  • Hatena Engineer Seminar #14 GraphQL編
  • GraphQL「良さ」・「難しさ」再探訪 〜スタディサプリにおける実例〜 / StudySapuri with GraphQL
  • GraphQL と Prisma から考える次のN年を見据えた技術選定
  • GraphQLにおけるクライアントキャッシュ戦略
  • GraphQLにおけるエラーハンドリングの仕方
  • GraphQLは「オワコン」「流行らない」のか?
    • GraphQLが大注目のグラフAPIとは? 「REST API時代終了」後に注目すべきAPIの新潮流
  • 全網羅!Spring for GraphQLのエラーハンドリングを徹底攻略!!
  • GraphQLはいつ使うか、RESTとの比較
  • GraphQLのスキーマ定義やクエリから型定義、自動生成できまっせ
  • GraphQL で REST API を作る - 技術的な挑戦と、それを支える文化の話
  • GraphQLはサーバーサイド実装のベストプラクティスとなるか
続きを読む