کوئری های پیشرفته، Error Handling و Data Loader در GraphQL
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: یازده دقیقه

در قسمت قبل یادگرفتیم که چگونه GraphQL را با ASP.NET Core یکپارچه کنیم و اولین GraphQL query را ایجاد و داده‌ها را از سرور بازیابی کردیم. البته ما به این query ‌های ساده بسنده نخواهیم کرد. در این قسمت می‌خواهیم یاد بگیریم که چگونه query ‌های پیشرفته‌ی GraphQL را بنویسیم و در زمان انجام این کار، نمایش دهیم که چگونه خطا‌ها را  مدیریت کنیم و علاوه بر این با queries, aliases, arguments, fragments نیز کار خواهیم کرد.

Creating Complex Types for GraphQL Queries 
اگر نگاهی به owners و query (در پایان قسمت قبل)  بیندازیم، متوجه خواهیم شد که یک لیست از خصوصیات مدل Owner که در OwnerType معرفی شده‌اند، نسبت به کوئری برگشت داده می‌شود. OwnerType شامل فیلد‌های Id , Name  و Address می‌باشد. یک Owner می‌تواند چندین account مرتبط با خود را داشته باشد. هدف این است که در owners ،query لیست account ‌های مربوط به هر owner را بازگشت دهیم. 
قبل از اضافه کردن فیلد Accounts در کلاس OwnerType نیاز است کلاس AccountType را ایجاد کنیم. در ادامه یک کلاس را به نام AccountType در پوشه GraphQLTypes ایجاد می‌کنیم. 
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object.");
        Field(x => x.Description).Description("Description property from the account object.");
        Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object.");
    }
}

 همانطور که مشخص است، خصوصیت Type را از کلاس Account، معرفی نکرده‌ایم (در ادامه اینکار را انجام خواهیم داد). در ادامه، واسط IAccountRepository و کلاس AccountRepository را باز کرده و آن را مطابق زیر ویرایش می‌کنیم: 
public interface IAccountRepository
{
    IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId);
}

public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
       _context = context;
    }
 
    public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts
        .Where(a => a.OwnerId.Equals(ownerId))
        .ToList();
}

اکنون می‌توان لیست account‌ها را به نتیجه owners ، query اضافه کنیم. پس کلاس OwnerType را باز کرده و آن را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository)
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id)
        );
    }
}

چیز خاصی در اینجا وجود ندارد که ما تا کنون ندیده باشیم. به همان روش که یک فیلد را در کلاس AppQuery  ایجاد کردیم، یک فیلد را با نام  accounts در کلاس OwnerType ایجاد می‌کنیم. همچنین متد GetAllAccountsPerOwner نیاز به پارامتر id را دارد و این پارامتر را از طریق context.Source.Id فراهم می‌کنیم. زیرا context شامل خصوصیت Source است که در این حالت مشخص نوع Owner می‌باشد.

اکنون پروژه را اجرا کنید و به آدرس زیر بروید:
https://localhost:5001/ui/playground
سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید که نتیجه آن علاوه بر owner‌ها، لیست account ‌های مربوط به هر owner هم می‌باشد:  
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      ownerId
    }
  }
}

Adding Enumerations in GraphQL Queries 
در کلاس AccountType  فیلد Type را اضافه نکرده‌ایم و این کار را عمدا انجام داده‌ایم. اکنون زمان انجام این کار می‌باشد. برای اضافه کردن گونه شمارشی به کلاس AccountType نیاز است تا در ابتدا یک کلاس تعریف شود که نسبت به type ‌های معمول در GraphQL متفاوت است. یک کلاس را به نام AccountTypeEnumType  در پوشه GraphQLTypes  ایجاد کرده و آن را مطابق زیر ویرایش می‌کنیم:  
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount>
{
    public AccountTypeEnumType()
    {
        Name = "Type";
        Description = "Enumeration for the account type object.";
    }
}

کلاس AccountTypeEnumType باید از نوع جنریک کلاس EnumerationGraphType ارث بری کند و پارامتر جنریک آن، یک گونه شمارشی را دریافت می‌کند (که در قسمت قبل آن را ایجاد کردیم؛ TypeOfAccount).  همچنین مقدار خصوصیت Name نیز باید همان نام خصوصیت گونه شمارشی در کلاس Account باشد (نام آن در کلاس Account مساوی Type می‌باشد). سپس گونه شمارشی را در کلاس AccountType به صورت زیر اضافه می‌کنیم:
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        ...
        Field<AccountTypeEnumType>("Type", "Enumeration for the account type object.");
    }
}

اکنون پروژه را اجرا کنید و سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید:
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
}
که نتیجه آن اضافه شدن type  به هر account می‌باشد:


Implementing a Cache in the GraphQL Queries with Data Loader  
دیدم که query، نتیجه دلخواهی را برای ما بازگشت می‌دهد؛ اما این query هنوز به اندازه کافی بهینه نشده‌است. مشکل چیست؟
query  ایجاد شده به حالتی کار می‌کند که در ابتدا همه owner ‌ها را بازیابی می‌کند. سپس به ازای هر owner، یک Sql Query  را به سمت بانک اطلاعاتی ارسال می‌کند تا Account ‌های مربوط به آن Owner را بازگشت دهد که می‌توان log  آن را در Terminal مربوط به VS Code مشاهده کرد.
 


البته زمانیکه چند موجودیت owner را داشته باشیم، این مورد یک مشکل نمی‌باشد؛ ولی وقتی تعداد موجودیت‌ها زیاد باشد چطور؟
 owners ، query را می‌توان با استفاده از DataLoader که توسط GraphQL فراهم شده‌است، بهینه سازی کرد. جهت انجام اینکار در ابتدا واسط IAccountRepository و کلاس AccountRepository را همانند زیر ویرایش می‌کنیم:
public interface IAccountRepository
{
    ...
    Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds);
}
public class AccountRepository : IAccountRepository
{
    ...
    public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds)
    {
        var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync();
        return accounts.ToLookup(x => x.OwnerId);
    }
}

 نیاز است که یک متد داشته باشیم که <<Task<ILookup<TKey, T  را برگشت می‌دهد؛ زیرا DataLoader نیازمند یک متد با نوع برگشتی که در امضایش عنوان شده است می‌باشد .
در ادامه کلاس OwnerType را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader)
    {
       ...
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context =>
            {
                var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds);
                return loader.LoadAsync(context.Source.Id);
            });
    }
}

در کلاس OwnerType، واسط IDataLoaderContextAccessor را در سازنده کلاس تزریق می‌کنیم و سپس متد Context.GetOrAddCollectionBatchLoader را فراخوانی می‌کنیم که در پارامتر اول آن، یک کلید و در پارامتر دوم آن، متد GetAccountsByOwnerIds را از IAccountRepository معرفی می‌کنیم.
سپس باید DataLoader را در متد ConfigureServices موجود در کلاس Startup ثبت کنیم. در ادامه services.AddGraphQL را مطابق زیر ویرایش می‌کنیم: 
services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped)
        .AddDataLoader(); 
اکنون پروژه را با دستور زیر اجرا کنید و سپس query قبلی را در UI.Playground اجرا کنید.
 اگر log  موجود در Terminal مربوط به  VS Code را مشاهده کنید، متوجه خواهید شد که در این حالت یک query برای تمام owner ‌ها و یک query برای تمام account ‌ها داریم.


Using Arguments in Queries and Handling Errors 
تا کنون ما  یک query را اجرا می‌کردیم که نتیجه آن بازیابی تمام owner ‌ها به همراه تمام account ‌های مربوط به هر owner بود. اکنون می‌خواهیم  براساس id، یک owner  مشخص را بازیابی کنیم. برای انجام این کار نیاز است که یک آرگومان را در query شامل کنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    ...
    Owner GetById(Guid id);
}

public class OwnerRepository : IOwnerRepository
{
    ...
    Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id));
}
سپس کلاس AppQuery  را مطابق زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        ...

        Field<OwnerType>(
            "owner",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
            resolve: context =>
            {
                var id = context.GetArgument<Guid>("ownerId");
                return repository.GetById(id);
            }
        );
    }
}
در اینجا یک فیلد را ایجاد کرده‌ایم که مقدار برگشتی آن یک OwnerType می‌باشد. نام query را owner تعیین می‌کنیم و از بخش arguments، برای ایجاد کردن آرگومان‌های این query استفاده می‌کنیم. آرگومان این query نمی‌تواند NULL باشد و باید از نوع IdGraphType و با نام ownerId باشد و در نهایت بخش resolve است که کاملا گویا می‌باشد. 
اگر پارامتر id، از نوع Guid نباشد، بهتر است که یک پیام را به سمت کلاینت برگشت دهیم. جهت انجام این کار یک اصلاح کوچک در بخش resolve انجام میدهیم:  
Field<OwnerType>(
    "owner",
    arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
    resolve: context =>
    {
        Guid id;
        if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id))
        {
            context.Errors.Add(new ExecutionError("Wrong value for guid"));
            return null;
        }
 
         return repository.GetById(id);
     }
);

 اکنون پروژه را اجرا کنید و سپس یک query جدید را در  UI.Playground به صورت زیر ارسال  کنید: 
{
  owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
که نتیجه آن بازیابی یک owner  با ( Id=6f513773-be46-4001-8adc-2e7f17d52d83 ) می‌باشد.

نکته: 
در صورتیکه قصد داشته باشیم علاوه بر id، یک name را هم ارسال کنیم، در بخش resolve به صورت زیر آن را دریافت می‌کنیم: 
 string name = context.GetArgument<string>("name");
و در زمان ارسال query:
{
  owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") {
   ...
  }
}


Aliases, Fragments, Named Queries, Variables, Directives 
می توانیم برای query ‌های ارسال شده از سمت کلاینت با معرفی aliases، یک سری تغییرات را داشته باشیم. وقتی‌که می‌خواهیم نام نتیجه دریافتی یا هر فیلدی را در نتیجه دریافتی تغییر دهیم، بسیار کاربردی می‌باشند. اگر یک query داشته باشیم که یک آرگومان را دارد و بخواهیم دو تا از این query داشته باشیم، برای ایجاد تفاوت بین query ‌ها می‌توان از aliases  استفاده کرد. 
جهت استفاده باید نام مورد نظر را در ابتدای query یا فیلد قرار دهیم:
{
  first:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  },
  second:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  }
}
اینبار در خروجی بجای ownerId ، id و بجای ownerName ، name و ... را مشاهده خواهید کرد.


همانطور که از مثال بالا مشخص است، دو query با فیلد‌های یکسانی را داریم. اگر بجای 2  query یکسان (مانند مثال بالا) ولی با آرگومان‌های متفاوت، اینبار 10 query یکسان با آرگومان‌های متفاوتی را داشته باشیم، در این حالت خواندن query ‌ها مقداری سخت می‌باشد. در این صورت می‌توان این مشکل را با استفاده از fragment‌ها برطرف کرد. Fragment‌‌ها این اجازه را به ما می‌دهند تا فیلد‌ها را با استفاده از کاما ( ، ) از یکدیگر جدا و تبدیل به یک بخش مجزا کنیم و سپس استفاده مجدد از آن بخش را در تمام query ‌ها داشته باشیم. Syntax آن به حالت زیر می‌باشد: 
fragment SampleName on Type{
  ...
}
تعریف یک fragment به نام ownerFields  و استفاده از آن : 
{
  first:owners{
    ...ownerFields
  },
  second:owners{
    ...ownerFields
  },
  ...
}


fragment ownerFields on OwnerType{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
}

برای ایجاد کردن یک named query، مجبور هستیم از کلمه کلیدی query در آغاز کل query استفاده کنیم؛ به همراه نام query، که بعد از کلمه کلیدی query قرار میگیرد. اگر نیاز داشته باشیم می‌توان آرگومان‌ها را به query ارسال کرد.
نکته مهمی که در رابطه با named query ‌ها وجود دارد این است که اگر یک query آرگومان داشته باشد نیاز است از پنجره QUERY VARIABLES برای تخصیص مقدار به آن آرگومان استفاده کنیم. 
query OwnerQuery($ownerId:ID!)
{
  owner(ownerId:$ownerId){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type
    }
  }
}
و سپس در قسمت QUERY VARIABLES 
{
  "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83"
}
اکنون اجرا کنید و خروجی را مشاهده کنید .

در نهایت می‌توان بعضی فیلد‌ها را از نتیجه دریافتی با استفاده از directive‌ها در query حذف یا اضافه کرد. دو directive وجود دارد که می‌توان از آن‌ها استفاده کرد (include  و skip). 


در قسمت بعد در رابطه با GraphQL Mutations صحبت خواهیم کرد.  
کد‌های مربوط به این قسمت را از اینجا دریافت کنید .  ASPCoreGraphQL_2.zip