GraphQL Part VIII: 使用一对多查询
今天,我们引入两个新的实体来处理客户与订单。客户与订单之间是一对多的关系,一个客户可以拥有一个或者多个订单,反过来,一个订单只能被某个客户所拥有。
可以按照 Engity Framework 的约定配置实体之间的关系。 如果某个实体拥有一个第二个实体的集合属性,Entity Framework 会自动创建一对多的关系。该属性被称为导航属性。
在 Customer 实体中有一个 Orders 属性,作为集合类型的导航属性。
Customer.cs
public class Customer { public int CustomerId { get; set; } public string Name { get; set; } public string BillingAddress { get; set; } public IEnumerable<Order> Orders { get; set; } }
大多情况下,定义一个导航属性就已经足够了,但是,还是建议定义完全的关系。在第二个实体上定义一个引用导航属性的外键属性。
Order.cs
public class Order { public int OrderId { get; set; } public string Tag { get; set;} public DateTime CreatedAt { get; set;} public Customer Customer { get; set; } public int CustomerId { get; set; } }
一旦定义了所有需要的关系,就可以使用 dotnet CLI 创建一个 migration 然后更新数据库。
dotnet ef migrations add OneToManyRelationship
dotnet ef database update
我们还需要创建两个新的 ObjectGraphType 。
OrderType.cs
public class OrderType: ObjectGraphType <Order> { public OrderType(IDataStore dataStore) { Field(o => o.Tag); Field(o => o.CreatedAt); Field <CustomerType, Customer> () .Name("Customer") .ResolveAsync(ctx => { return dataStore.GetCustomerByIdAsync(ctx.Source.CustomerId); }); } }
以及 CustomerType.cs
public class CustomerType: ObjectGraphType <Customer> { public CustomerType(IDataStore dataStore) { Field(c => c.Name); Field(c => c.BillingAddress); Field <ListGraphType<OrderType> , IEnumerable <Order>> () .Name("Orders") .ResolveAsync(ctx => { return dataStore.GetOrdersByCustomerIdAsync(ctx.Source.CustomerId); }); } }
为了暴露两个新的端点,还需要在 InventoryQuery 中注册这两个类型。
InventoryQuery.cs
Field<ListGraphType<OrderType>, IEnumerable<Order>>() .Name("Orders") .ResolveAsync(ctx => { return dataStore.GetOrdersAsync(); }); Field<ListGraphType<CustomerType>, IEnumerable<Customer>>() .Name("Customers") .ResolveAsync(ctx => { return dataStore.GetCustomersAsync(); });
在后台的数据仓库中,还需要提供相应的数据访问方法。
DataStore.cs
public async Task <IEnumerable<Order>> GetOrdersAsync() { return await _applicationDbContext.Orders.AsNoTracking().ToListAsync(); } public async Task <IEnumerable<Customer>> GetCustomersAsync() { return await _applicationDbContext.Customers.AsNoTracking().ToListAsync(); } public async Task <Customer> GetCustomerByIdAsync(int customerId) { return await _applicationDbContext.Customers.FindAsync(customerId); } public async Task <IEnumerable<Order>> GetOrdersByCustomerIdAsync(int customerId) { return await _applicationDbContext.Orders.Where(o => o.CustomerId == customerId).ToListAsync(); }
同时,我们还增加两个方法用于创建 Customer 和 Order,
public async Task<Order> AddOrderAsync(Order order) { var addedOrder = await _applicationDbContext.Orders.AddAsync(order); await _applicationDbContext.SaveChangesAsync(); return addedOrder.Entity; } public async Task<Customer> AddCustomerAsync(Customer customer) { var addedCustomer = await _applicationDbContext.Customers.AddAsync(customer); await _applicationDbContext.SaveChangesAsync(); return addedCustomer.Entity; }
在上一篇 Blog 中,我们创建过 InputObjectGraphType 用于 Item 的创建,与其类似,我们也需要为 Customer 和 Order 创建对应的 InputObjectGraph。
OrderInputType.cs
public class OrderInputType : InputObjectGraphType { public OrderInputType() { Name = "OrderInput"; Field<NonNullGraphType<StringGraphType>>("tag"); Field<NonNullGraphType<DateGraphType>>("createdAt"); Field<NonNullGraphType<IntGraphType>>("customerId"); } }
CustomerInputType.cs
public class CustomerInputType : InputObjectGraphType { public CustomerInputType() { Name = "CustomerInput"; Field<NonNullGraphType<StringGraphType>>("name"); Field<NonNullGraphType<StringGraphType>>("billingAddress"); } }
最后,我们需要注册所有新的的类型到 DI 系统中。在 Startup 的 ConfigureServices 方法中如下注册。
public void ConfigureServices(IServiceCollection services) { .... .... services.AddScoped<CustomerType>(); services.AddScoped<CustomerInput>(); services.AddScoped<OrderType>(); services.AddScoped<OrderInputType>(); }
现在,如果您运行应用,将会看到如下的错误信息:
"No parameterless constructor defined for this object."
通过调查 graphql-dotnet,我发现了这个问题:issue
对于当前的方案, Schema 的构造函数注入不能工作。DI 系统可以获取类型一次,但是不能对该对象链的子级再次获取。简单来说,如果在 InventoryQuery 中通过构造函数注入了 IDataSource 一次 ,但是,你不能在其他的 Graph Type 构造函数中注入;例如 CustomerType。但是,这不是我们期望的行为,因此,使用 IDependencyResolver,在 DI 系统中注册 IDependencyResolver,并确保提供一个有限的生命周期。
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
需要在 InventorySchemacs 中做一点修改,修改代码,在构造函数中注入 IDependencyResolver。
InventorySchema.cs
public class InventorySchema: Schema { public InventorySchema(IDependencyResolver resolver): base(resolver) { Query = resolver.Resolve < InventoryQuery > (); Mutation = resolver.Resolve < InventoryMutation > (); } }
现在,重新运行应用,并验证你可以访问新增加的字段。