はじめに
これは、C# Advent Calendar 2024 の22日目の記事です。
忘れかけていたGraphQLを思い出すためにこの記事を書きました。頭の整理となる良い機会でしたありがとうアドベントカレンダー
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くんありがとう)
ただ、C#を使っているほとんどの人はコードファーストを選択するのではないでしょうか。
そしてその場合、SDLのリゾルバを書く必要がありません。リゾルバはQuery
クラスとMutation
クラスの中のメソッドとなります。EF Coreで書くだけなので、その方面に強い方は迷わずコードファースト一択となるでしょう
コードファーストによるリゾルバ実装例
以下はQuery
クラスのリゾルバです。[UseFiltering]
アノテーションがあることで、Hot Chocolateは自動的にフィルタリングのためのスキーマを生成します。そのため、GraphQLクエリ・リクエストでwhere
引数を使ってid
で絞り込むことができます。
// 別途Importモデル・クラスを書いています(後述)
public class Query
{
[UseFiltering]
public IQueryable<Import> Imports([Service] AppDbContext context) => context.Imports;
}
# これはImportモデル・クラスに相当
type Import {
id: ID!
createDate: DateTime
section: String
tanto: String
scm: String
vessel: String
voy: String
# ... 他のフィールド
}
# こちらが関数宣言
type Query {
imports: [Import!]!
}
query GetImportById($id: ID!) {
imports(where: { id: { eq: $id } }) { # eqはequalsの略
id
createDate
section
# ... 他のフィールド
}
}
以下はMutation
クラスのリゾルバです。BlNo
とSection
を埋め込んだリクエストを受けて、新規でImport
を登録します。
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;
}
}
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!
}
mutation CreateNewImport {
createItem(input: { blNo: "BL0123456789", section: "営業3部" }) {
id # 作成されたImportのIDを取得可能
blNo
section
}
}
以下は、データソースとなるデータベース設定とGraphQLサーバーの立ち上げです。ここに認証ロジックも書きますがこの記事では割愛します。
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();
その他必要なクラス
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モデル
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のコードファーストを使う場合、必要無いからです(いや、それぐらい読んどけよ)
C#の型システムを最大限に活用して、型安全なGraphQL APIを提供します
Hot Chocolateによるコードファーストを高評価するGeminiくんのこの言葉通りだと思います