Blog จะเขียนต่อจากตอนที่แล้ว [DOTNET] GraphQL บน NET8 ด้วย HotChocolate Library #01 (Query) ที่จะเน้นไปส่วนของการดึงข้อมูล (Query) โดยใช้ GraphQL HotChocolate เหมือนเดิมครับ แต่ไปเน้นในส่วยของ Create Update Delete แทนครับ ซึ่งใน GraphQL เค้าจะเรียกว่า mutation และมีการลองเจ้า TiDB ด้วยครับ (รู้จากงาน CodeMania ได้เอามาลองพอดี)
เนื่องจาก Blog ตอนที่แล้ว ผมมีแค่ Code อย่างเดียว ไม่มีได้มี Dependency อะไรเพิ่ม แต่เนื่องจากลองในส่วน mutation (Create / Update / Delete) มันต้องมี Database และ โดยใน Blog นี้ ผมใช้ TiDB เค้ามี Free Tier และลอง Table เดียว เล็กๆ ครับ
หัวข้อ
ภาพรวม
สำหรับภาพรวมของระบบใน Blog นี้หลักจะมี 2 ส่วน ตัว
- WebAPI ที่เปิดรับทั้งตัว GraphQL / REST API เดิมด้วย
- Database อันนี้เรียกว่าเป็นการลองของ ใช้ TiDB Cloud ลองใน Blog นี้ครับ
การทดสอบ ผมใช้ไฟล์ .http ลองเป็น Client ทำ Sample Request เพื่อทดสอบครับ
โครงสร้าง Project
ผมมีปรับจาก Blog ตอนที่แล้วนิดนึง มีลักษณะดังนี้
GraphQLAPI -> Controllers -> DTOs ---> AppMapperProfile.cs ---> SuppilerCreateDTO.cs ---> SupplierDTO.cs SuppilerDTO.cs -> GraphQL ---> Query ---> Type ---> Mutation ------> SupplierGraphQLMutation.cs -> Services -> Infra ---> Models ------> SupplierModel.cs ---> Repositories ---> DataDBContext.cs
TiDB
อันนี้ง่ายเลยครับ สมัครก่อนตาม Link ของ TiDB Cloud ผมลองใช้ Google Account สมัครเข้าไป สร้าง Cluster + Database ของผมชื่อ test
หน้าจอมันจะมี Connection Generate มาให้ด้วยครับ และ Gen Password ด้วยนะ มันจะมีรูปแบบ Connection String หลายแบบ ตอนที่ผมลอง C# ไม่มีครับ เอาแบบ .env มาแทน เก็บข้อมูลตรงนี้เอาไว้นะครับ
Code ส่วนที่เกี่ยวกับ DB
ตัว Project ผมเลือกใช้ Lib ดังนี้ครับ
- ORM ในที่นี้ใช่ตัว EntityFrameworkCore ครับ
- ตัว TiDB ใช้ Driver MySQL ในการเชื่อมต่อครับ กลับมาใช้ MySQL Driver ในรอบหลายปีเลย เจอแปลกเหมือนมี 2 ค่ายที่ทำครับ ได้แก่ MySql.EntityFrameworkCore / Pomelo.EntityFrameworkCore.MySql ผมเลือกตัว Pomelo ครับ
- AutoMapper เอาช่วง Map Field DTO กับ Model ครับ
สำหรับ Command จะได้ ดังนี้ครับ
dotnet add package Microsoft.EntityFrameworkCore --version 8.0.8 dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.8 dotnet add package Pomelo.EntityFrameworkCore.MySql --version 8.0.2 dotnet add package AutoMapper --version 13.0.0
เพิ่ม DTO จริงๆคล้ายๆ Entity นะ แต่อยากบังคับ Structure ให้แยกกันส่วน DB / Data ของ API ส่วน DTO แยกสำหรับ Create / อื่นๆด้วย ถ้าสงสัยว่าทำไมต้องแยกลองดู Blog นี้ได้ครับ
- SupplierModel.cs ส่วนที่แทน Table ของ Database
- SupplierDTO.cs สำหรับส่งข้อมูล ในแต่ละ Layer Service / Repository
- SuppilerCreateDTO.cs สำหรับ Create ข้อมูล
จากนั้นมาเพิ่ม DBContext ของ EF ครับ อย่างผมเอา Default + เพิ่มกำหนดให้มัน Auto Create Table ให้เลย / Seed Data
using GraphQLAPI.Infra.Models; using Microsoft.EntityFrameworkCore; namespace GraphQLAPI.Infra { public class DataDBContext : DbContext { public DataDBContext(DbContextOptions<DataDBContext> options) : base(options) { } public DbSet<SupplierModel> Suppliers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { //อันนี้ ถ้าไม่อยากให้มันสร้า่งComment ออก base.OnModelCreating(modelBuilder); //Seed Data modelBuilder.Entity<SupplierModel>().HasData( new SupplierModel { Id = 1, FirstName = "John", LastName = "Doe", Address = "123 Main St", Email = "John@exam,ple.com", Phone = "123-456-7890" }, new SupplierModel { Id = 2, FirstName = "Jane", LastName = "Doe", Address = "123 Main St", Email = "Jane@exam,ple.com", Phone = "123-456-7890" }); } } }
ขยับขึ้นมาปรับส่วน Repository ตรงนี้ใช้ Automapper มาช่วย Map Enitity / DTO Code Mapping ผมเอาง่ายๆ ดังนี้
- ISupplierRepository.cs / SupplierRepository .cs สำหรับ Code ด้านล่าง แปะ Logic บางส่วนครับ
namespace GraphQLAPI.Infra.Repositories; using AutoMapper; using GraphQLAPI.DTOs; using GraphQLAPI.Infra.Models; using System.Collections.Generic; public class SupplierRepository: ISupplierRepository { private readonly DataDBContext _context; private readonly IMapper _mapper; public SupplierRepository(DataDBContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task <IEnumerable <SupplierDTO>> GetSuppliers() { IList<SupplierModel> supplierModels = _context.Suppliers.ToList(); return await Task.FromResult(_mapper.Map<IEnumerable<SupplierDTO>>(supplierModels)); } public async Task <SupplierDTO> GetSupplier(int Id) { SupplierModel supplierModel = _context.Suppliers.FirstOrDefault(x => x.Id == Id); return await Task.FromResult(_mapper.Map<SupplierDTO>(supplierModel)); } public async Task<SupplierDTO> AddSupplier(SupplierDTO supplier) { SupplierModel supplierModel = _mapper.Map<SupplierModel>(supplier); _context.Suppliers.Add(supplierModel); _context.SaveChanges(); return await Task.FromResult(_mapper.Map<SupplierDTO>(supplierModel)); } //Other Logic }
- AutoMapper (DTO <-> Entity) ไฟล์ DTOs/AppMapperProfile.cs
using AutoMapper; using GraphQLAPI.Infra.Models; namespace GraphQLAPI.DTOs; public class AppMapperProfile : Profile { public AppMapperProfile() { CreateMap<SupplierModel, SupplierDTO>().ReverseMap(); CreateMap<SuppilerCreateDTO, SupplierDTO>() .ForMember(dest => dest.Id, opt => opt.Ignore()) // Ignore the Id property .ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); // Apply the global condition } }
ขยับชึ้นมาในส่วนของ Service ที่เตรียมไว้ครับ จะมี 2 ไฟล์ ISuppilerService.cs / SuppilerService.cs โดยมี Code สำหรับส่วน Create ตามด้านล่าง
namespace GraphQLAPI.Services; using AutoMapper; using GraphQLAPI.DTOs; using GraphQLAPI.Infra.Repositories; public class SupplierService : ISupplierService { private readonly ISupplierRepository _supplierRepository; private readonly IMapper _mapper; public SupplierService(ISupplierRepository supplierRepository, IMapper mapper) { _supplierRepository = supplierRepository; _mapper = mapper; } public async Task<IEnumerable<SupplierDTO>> GetSuppliers() { return await _supplierRepository.GetSuppliers(); } public async Task<SupplierDTO> GetSupplier(int id) { return await _supplierRepository.GetSupplier(id); } public async Task<SupplierDTO> AddSupplier(SuppilerCreateDTO supplierCreate) { //map SuppilerCreateDTO to SupplierDTO SupplierDTO supplier = _mapper.Map<SupplierDTO>(supplierCreate); supplier = await _supplierRepository.AddSupplier(supplier); return supplier; } //Other Operation }
จากนั้นมาที่ส่วนของ program.cs โดยมี Step การเพิ่มของลงไปดังนี้
- เรียกใช้ Automapper
builder.Services.AddAutoMapper(typeof(Program));
- เพิ่มการเชื่อมต่อ DB
String connectionString = builder.Configuration.GetConnectionString("TiDBConnn"); builder.Services.AddDbContext<DataDBContext>(opt => opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
สุดท้ายในส่วน appsettings.json เพิ่ม Connnection String แก้ Server / Port / User และ Password ให้ครับ
"ConnectionStrings": { "TiDBConnn": "Server=gateway01.ap-southeast-1.prod.aws.tidbcloud.com;port=4000;database=<YOUR-DB>;user=<YOUR_USER_WITH.ROOT>;password=<YOUR_PASSWORD>;" }
หลังจากทำเสร็จแล้ว ลอง Build สักรอบด้วยคำสั่ง dotnet build
สร้าง Migration File
เมื่อตรวจสอบว่า Build Success แล้ว ให้ลองสร้าง Migration File (Base Path project root)
dotnet ef migrations add intial
List และ Apply
--List out pending migrations dotnet ef migrations list --project .\GraphQLAPI.csproj --Update database with pending migrations dotnet ef database update --project .\GraphQLAPI.csproj
ลองไปดู TiDB มี Table และ
จากนั้น dotnet watch run ลองทดสอบครับ
เพิ่มในส่วน Mutation (Create Update Delete)
สำหรับส่วนนี้ Pattern คล้ายๆกันครับ เพิ่ม Logic ในส่วน repository และไล่มาส่วน service สุดท้ายที่ส่วน Mutation
- repository map ข้อมูลระหว่าง dto กับ entity
- service ตอนนี้ไม่มี Logic พิเศษ เอาไว้ให้มัน Call repository
- mutation ผมแยกไฟล์ SupplierGraphQLMutation.cs และเพิ่ม Logic เข้าไป สังเกคุว่า ผมจะมีดัก Try Catch และ มา Rewrite Exception ด้วย ตรง GraphQLException
using GraphQLAPI.DTOs; using GraphQLAPI.Services; namespace GraphQLAPI.GraphQL.Mutation { public class SupplierGraphQLMutation { public async Task<SupplierDTO> AddSupplier([Service] ISupplierService supplierService, SuppilerCreateDTO newSupplier) { try { return await supplierService.AddSupplier(newSupplier); } catch (Exception ex) { throw new GraphQLException(new ErrorBuilder() .SetMessage(ex.Message) .SetCode("SUPPLIER_ADD_ERROR") .Build()); } } } }
- program.cs เพิ่มให้ GraphQL รู้จักกับส่วน Mutation ตอน Start มันจะได้เอาไป Generate GraphQL Schema ใหม่ครับ
builder.Services.AddGraphQLServer() .AddType<SupplierType>() .AddQueryType<SupplierGraphQLQuery>() .AddMutationType<SupplierGraphQLMutation>(); // Register the mutation type
ถ้า Run ขึ้นมา พบว่าตัว Create Register ใน Schema ในแล้วครับ
Test Create
หลังจากสร้างเสร็จแล้ว มาลองกันครับ ของผมเอาง่ายๆ เขียนไฟล์ .http ไว้แล้ว ลองยิง Create แล้วดู result ที่ได้ครับ
### Create Supplier ### POST {{GraphQLAPI_HostAddress}}/graphQL Content-Type: application/json X-REQUEST-TYPE: GraphQL mutation { addSupplier(newSupplier: { firstName: "John" lastName: "Sunma" address: "1234 Main St" phone: "123-456-7890" email: "faii@adc.com" }) { id firstName lastName address phone email } }
ส่วน Update / Delete คล้ายกับ Create โดย Code ตัวเต็ม GraphQL/Mutation/SupplierGraphQLMutation.cs
อ๋อ ส่วน Service / Repository จะเคสพิเศษตอน Update มาตรวจว่า Id ที่ส่งเข้ามา มีอะไรบ้างลองไปแกะจาก Code ใน Repo ได้ครับ
Test Update / Delete
ไฟล์ .http ไว้แล้ว ลองยิง Update / Delete ตัวเต็มจะอยู่ที่นี่ครับ GraphQLAPI.http
### Update Supplier ### POST {{GraphQLAPI_HostAddress}}/graphQL Content-Type: application/json X-REQUEST-TYPE: GraphQL mutation { updateSupplier( id: 30002, supplier: { id: 30002 firstName: "John" lastName: "John" address: "1234 Main St" phone: "123-456-7890" email: "faii@adc.com" }) { id firstName lastName address phone email } } ### Update Supplier Id Not Exist ### POST {{GraphQLAPI_HostAddress}}/graphQL Content-Type: application/json X-REQUEST-TYPE: GraphQL mutation { updateSupplier( id: 99999, supplier: { id: 99999 firstName: "John" lastName: "John" address: "1234 Main St" phone: "123-456-7890" email: "faii@adc.com" }) { id firstName lastName address phone email } } ### Delete Supplier (true-deleted / false-not found)### POST {{GraphQLAPI_HostAddress}}/graphQL Content-Type: application/json X-REQUEST-TYPE: GraphQL mutation { deleteSupplier(id: 30003) }
อันนี้เป็นอย่างของ GraphQL หลักผมจะไปใช้ในมุม Query มากกว่า ที่เหลือยังเป็น REST API เหมือนเดิมครับ
ว่าจะเดี๋ยว Blog หน้าอาจจะลองแยก Project นี้มาเป็นหลายๆ ตาม Pattern Clean Architecture ดูครับ สำหรับ Code เต็มๆ อยู่ที่นี้ครับ GitHub - pingkunga/net8_graphql_HotChocolate_sample at feature/mutation_sample
Discover more from naiwaen@DebuggingSoft
Subscribe to get the latest posts sent to your email.