5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#Advent Calendar 2024

Day 22

Hot Chocolate?何それ美味しいのって美味しいに決まってるだろ!

Last updated at Posted at 2024-12-21

はじめに

これは、C# Advent Calendar 2024 の22日目の記事です。

忘れかけていたGraphQLを思い出すためにこの記事を書きました。頭の整理となる良い機会でした:writing_hand:ありがとうアドベントカレンダー:bow:

GraphQLの大雑把なまとめ

GraphQLとは、ユーザーと様々なデータソースとを橋渡しするクエリ言語であり、そのための実行環境です。GraphQLサーバーは、どのようなデータをどのように処理するかをサーバー構築者が予め定義します。

データ構造をスキーマ、データ取得(SELECT)の定義をクエリ、データ変更(INSERT、UPDATE、DELETE)の定義をミューテーション、データ処理(関数)をリゾルバと言います。

最近仲良くなったGeminiくんがまとめた表
概念 GraphQL用語 SQLでの対応例 説明
データ構造 スキーマ CREATE TABLE データの型(例:Int, String, Boolean, ID)やオブジェクトの構造(フィールドとその型)を定義します。クライアントとサーバー間のデータ交換のルールを定める契約のような役割を果たします。型安全性を提供し、開発効率を高めます。
データ取得 クエリ SELECT サーバーからデータを取得するための操作を定義します。取得したいデータのフィールドをクライアントが自由に指定できるため、必要なデータだけを取得できます(オーバーフェッチ/アンダーフェッチの解消)。GraphQLサーバーに送信するリクエストそのものでもあります。
データ変更 ミューテーション INSERT, UPDATE, DELETE サーバー上のデータを変更(作成、更新、削除)するための操作を定義します。クエリと同様に、操作名と必要な引数、返り値の型を定義します。データ変更に加えて、キャッシュの更新や他の副作用を伴う場合もあります。
データ処理 リゾルバ (SQLの実行、ストアドプロシージャなど) クエリまたはミューテーションで定義された操作に対応する処理(関数)を実装します。データベースへのアクセス、外部APIへのリクエスト、データの変換・加工、ビジネスロジックの実装、エラーハンドリングなど、データ処理の中核を担います。クエリやミューテーションが「何をしたいか」を宣言するのに対し、リゾルバは「どのようにそれを実現するか」を記述します。

当初はクエリやミューテーションに関数宣言(シグネチャ)が記載されているため、リゾルバと何が違うのかと混乱していました。定義に目を向けるとクエリ/ミューテーション、処理(実装)に目を向けるとリゾルバ、と言うことですね。

Hot Chocolateの飲み方

さて、C#でGraphQLサーバーを構築する場合、有名どころと言えばHot Chocolateです。Geminiくんに聞くとHot Chocolateには二つのアプローチがあるそうです。

  • コードファースト
    C#のコードから直接GraphQLスキーマを構築する(EF Coreを使用)
  • スキーマファースト
    SDL (Schema Definition Language) を使用してスキーマを記述

コードファーストしか知らなかったので、スキーマファーストとはちょっとビックリしました。そんなアプローチも用意されていたんですね。(Geminiくんありがとう:blush:

ただ、C#を使っているほとんどの人はコードファーストを選択するのではないでしょうか。

そしてその場合、SDLのリゾルバを書く必要がありません。リゾルバはQueryクラスとMutationクラスの中のメソッドとなります。EF Coreで書くだけなので、その方面に強い方は迷わずコードファースト一択となるでしょう:laughing:

コードファーストによるリゾルバ実装例

以下はQueryクラスのリゾルバです。[UseFiltering]アノテーションがあることで、Hot Chocolateは自動的にフィルタリングのためのスキーマを生成します。そのため、GraphQLクエリ・リクエストでwhere引数を使ってidで絞り込むことができます。

Query.cs
// 別途Importモデル・クラスを書いています(後述)
public class Query
{
    [UseFiltering]
    public IQueryable<Import> Imports([Service] AppDbContext context) => context.Imports;
}
SDLの場合
# これはImportモデル・クラスに相当
type Import {
  id: ID!
  createDate: DateTime
  section: String
  tanto: String
  scm: String
  vessel: String
  voy: String
  # ... 他のフィールド
}

# こちらが関数宣言
type Query {
  imports: [Import!]! 
}
GraphQLリクエスト・テキスト
query GetImportById($id: ID!) {
  imports(where: { id: { eq: $id } }) { # eqはequalsの略
    id
    createDate
    section
    # ... 他のフィールド
  }
}

以下はMutationクラスのリゾルバです。BlNoSectionを埋め込んだリクエストを受けて、新規でImportを登録します。

Mutation.cs
public class Mutation
{
    public async Task<Import> CreateItemAsync(ImportInput input, [Service] AppDbContext context)
    {
        var item = new Import
        {
            BlNo = input.BlNo,
            Section = input.Section
        };
        context.Imports.Add(item);
        await context.SaveChangesAsync();
        return item;
    }
}
SDLの場合
input ImportInput {
  blNo: String
  section: String
  createDate: DateTime
  # ... 他のフィールド
}

# Queryの方に書いてあっても重複して書く必要があるらしい(Geminiくん談)
type Import {
  id: ID!
  blNo: String
  section: String
  createDate: DateTime
  # ... 他のフィールド
}

# 関数宣言
type Mutation {
  createItem(input: ImportInput!): Import!
}

GraphQLリクエスト・テキスト
mutation CreateNewImport {
  createItem(input: { blNo: "BL0123456789", section: "営業3部" }) {
    id # 作成されたImportのIDを取得可能
    blNo
    section
  }
}

以下は、データソースとなるデータベース設定とGraphQLサーバーの立ち上げです。ここに認証ロジックも書きますがこの記事では割愛します。

Program.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;

// データベースの設定(SQL Server)
builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .LogTo(message => Debug.WriteLine(message),
            new[] { DbLoggerCategory.Database.Name },
            LogLevel.Information,
            DbContextLoggerOptions.LocalTime)
        .UseSqlServer(configuration.GetConnectionString("DefaultConnection"))
);

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()        // Queryクラスでスキーマの一部(シグネチャ)とリゾルバを登録
    .AddMutationType<Mutation>()  // Mutationクラスでスキーマの一部(シグネチャ)とリゾルバを登録
    .AddType<ImportType>()        // Importクラスをスキーマの一部として登録
    .AddFiltering();
 
WebApplication app = builder.Build();

app.MapGraphQL();
app.Run();

その他必要なクラス
AppDbContext.cs
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Import> Imports { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Import>().ToTable("Import");
     }
 }
Import.cs
// Importモデル
public class Import
{
    public int Id { get; set; }
    public DateTime? CreateDate { get; set; }
    public string? Section { get; set; }
    public string? Tanto { get; set; }
    public string? Scm { get; set; }
    public string? Vessel { get; set; }
    public string? Voy { get; set; }
    public int? SitateruCode { get; set; }
    public string? Shipper { get; set; }
    public string? BlNo { get; set; }
    public DateTime? LoadingDate { get; set; }
    public DateTime? ArrivalDate { get; set; }
    public string? LoadingPort { get; set; }
    public string? ArrivalPort { get; set; }
    public string? IraiNo { get; set; }
    public string? Agent { get; set; }
    public float Qty { get; set; }
    public float Weight { get; set; }
    public float Volume { get; set; }
    public string? ContainerSize { get; set; }
    public string? Freight { get; set; }
    public string? Hds { get; set; }
    public string? Lss { get; set; }
    public string? Cic { get; set; }
    public string? Afr { get; set; }
}
その他
// ミューテーションの引数として使用するDTO (Data Transfer Object) 
public class ImportInput
{
    public int Id { get; set; }
    public DateTime? CreateDate { get; set; }
    public string? Section { get; set; }
    public string? Tanto { get; set; }
    public string? Scm { get; set; }
    public string? Vessel { get; set; }
    public string? Voy { get; set; }
    public string? Shipper { get; set; }
    public string? BlNo { get; set; }
    public DateTime? LoadingDate { get; set; }
    public DateTime? ArrivalDate { get; set; }
    public string? LoadingPort { get; set; }
    public string? ArrivalPort { get; set; }
    public string? IraiNo { get; set; }
}

// GraphQLのImport型を生成
public class ImportType : ObjectType<Import> { }

おわりに

GraphQLの参考書と言えばオライリーの『初めてのGraphQL』があり、わたしも購入しましたが、半分しか読んでません。後半からのサーバー実装の説明についてはHot Chocolateのコードファーストを使う場合、必要無いからです:grin:(いや、それぐらい読んどけよ:sweat_smile:

C#の型システムを最大限に活用して、型安全なGraphQL APIを提供します

Hot Chocolateによるコードファーストを高評価するGeminiくんのこの言葉通りだと思います:thumbsup:

:santa_tone2::christmas_tree::santa_tone2::christmas_tree::santa_tone2::christmas_tree::santa_tone2::christmas_tree::santa_tone2::christmas_tree:

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?