获取数据
管理到数据库和其他数据源的连接
希望从 REST API 获取数据?查看从 REST 获取.
Apollo Server可以获取您所需的任何来源的数据,例如一个REST API或数据库。您的服务器可以使用任意数量的不同数据源:
因为服务器可以使用多种不同的数据源,所以保持您的解析器整洁变得更加重要。
因此,我们建议为从特定来源获取数据创建单独的数据源类,提供解析器可以使用的方法来整洁地访问数据。您还可以自定义数据源类来帮助缓存、去重或解析数据源时处理错误。
创建数据源类
您的数据源类可以根据您的需求简单或复杂。您知道服务器需要什么数据,可以将这些数据作为您类中包含方法的指南。
以下是一个连接到存储预约的数据库的数据源类示例:
export class ReservationsDataSource {private dbConnection;private token;private user;constructor(options: { token: string }) {this.dbConnection = this.initializeDBConnection();this.token = options.token;}async initializeDBConnection() {// set up our database details, instantiate our connection,// and return that database connectionreturn dbConnection;}async getUser() {if (!this.user) {// store the user, lookup by tokenthis.user = await this.dbConnection.User.findByToken(this.token);}return this.user;}async getReservation(reservationId) {const user = await this.getUser();if (user) {return await this.dbConnection.Reservation.findByPk(reservationId);} else {// handle invalid user}}//... more methods for finding and creating reservations}
export class ReservationsDataSource {constructor(options) {this.dbConnection = this.initializeDBConnection();this.token = options.token;}async initializeDBConnection() {// set up our database details, instantiate our connection,// and return that database connectionreturn dbConnection;}async getUser() {if (!this.user) {// store the user, lookup by tokenthis.user = await this.dbConnection.User.findByToken(this.token);}return this.user;}async getReservation(reservationId) {const user = await this.getUser();if (user) {return await this.dbConnection.Reservation.findByPk(reservationId);} else {// handle invalid user}}//... more methods for finding and creating reservations}
Apollo的RestDataSource
类是一个关于数据源类如何处理缓存、去重和错误的建设性示例。
批量处理和缓存
如果你想在数据源类中添加批量处理、去重或缓存,我们建议使用 DataLoader软件包。使用DataLoader这样的软件包特别有助于解决 著名的N+1查询问题。
DataLoader提供了一个缓存机制,可以避免在单个 GraphQL 请求中多次加载相同的对象(类似于 RESTDataSource
的一个缓存层)。它还可以在单次事件循环中合并批量请求,一次性获取多个对象。
DataLoader实例是针对每个请求的,因此如果你在数据源中使用DataLoader,请确保 每次请求都使用该类创建一个新的实例:
import DataLoader from 'dataloader';class ProductsDataSource {private dbConnection;constructor(dbConnection) {this.dbConnection = dbConnection;}private batchProducts = new DataLoader(async (ids) => {const productList = await this.dbConnection.fetchAllKeys(ids);// Dataloader expects you to return a list with the results ordered just like the list in the arguments were// Since the database might return the results in a different order the following code sorts the results accordinglyconst productIdToProductMap = productList.reduce((mapping, product) => {mapping[product.id] = product;return mapping;}, {});return ids.map((id) => productIdToProductMap[id]);});async getProductFor(id) {return this.batchProducts.load(id);}}// In your server file// Set up our database, instantiate our connection,// and return that database connectionconst dbConnection = initializeDBConnection();const { url } = await startStandaloneServer(server, {context: async () => {return {dataSources: {// Create a new instance of our data source for every request!// (We pass in the database connection because we don't need// a new connection for every request.)productsDb: new ProductsDataSource(dbConnection),},};},});
import DataLoader from 'dataloader';class ProductsDataSource {constructor(dbConnection) {this.dbConnection = dbConnection;}batchProducts = new DataLoader(async (ids) => {const productList = await this.dbConnection.fetchAllKeys(ids);// Dataloader expects you to return a list with the results ordered just like the list in the arguments were// Since the database might return the results in a different order the following code sorts the results accordinglyconst productIdToProductMap = productList.reduce((mapping, product) => {mapping[product.id] = product;return mapping;}, {});return ids.map((id) => productIdToProductMap[id]);});async getProductFor(id) {return this.batchProducts.load(id);}}// In your server file// Set up our database, instantiate our connection,// and return that database connectionconst dbConnection = initializeDBConnection();const { url } = await startStandaloneServer(server, {context: async () => {return {dataSources: {// Create a new instance of our data source for every request!// (We pass in the database connection because we don't need// a new connection for every request.)productsDb: new ProductsDataSource(dbConnection),},};},});
将数据源添加到你的 context
函数中
您可以将 data sources 添加到服务器 context
初始化函数中,如下所示:
interface ContextValue {dataSources: {dogsDB: DogsDataSource;catsApi: CatsAPI;};token: string;}const server = new ApolloServer<ContextValue>({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async ({ req }) => {const { cache } = server;const token = req.headers.token;return {// We create new instances of our data sources with each request.// We can pass in our server's cache, contextValue, or any other// info our data sources require.dataSources: {dogsDB: new DogsDataSource({ cache, token }),catsApi: new CatsAPI({ cache }),},token,};},});console.log(`🚀 Server ready at ${url}`);
const server = new ApolloServer({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async ({ req }) => {const { cache } = server;const token = req.headers.token;return {// We create new instances of our data sources with each request.// We can pass in our server's cache, contextValue, or any other// info our data sources require.dataSources: {dogsDB: new DogsDataSource({ cache, token }),catsApi: new CatsAPI({ cache }),},token,};},});console.log(`🚀 Server ready at ${url}`);
Apollo Server在每次接收到操作时都会调用 初始化 context
函数。这意味着:
- 对于每个操作,
context
返回一个包含您数据源类(在本例中是DogsDataSource
和CatsAPI
)的对象的新实例。 - 如果你的数据源是状态相关的(例如,使用一个内存缓存),则
context
函数应为每个操作创建一个新的数据源类实例。这可以确保你的数据源不会意外地在请求之间缓存结果。
然后,您的解析器可以访问共享的 contextValue
对象中的数据源并使用它们来检索数据:
const resolvers = {Query: {dog: async (_, { id }, { dataSources }) => {return dataSources.dogsDB.getDog(id);},popularDogs: async (_, __, { dataSources }) => {return dataSources.dogsDB.getMostLikedDogs();},bigCats: async (_, __, { dataSources }) => {return dataSources.catsApi.getCats({ size: 10 });},},};
开源实现
Apollo Server 3包含名为DataSource
的抽象类,您的每个数据源都可以从该抽象类派生。然后,您会使用特殊的 dataSources
函数初始化每个 DataSource
子类,这个函数会在幕后将您的数据源附加到 context
上。
在Apollo Server 4中,现在您可以在与请求其他设置相同的 context
函数中创建数据源,完全避免了 DataSource
超类。我们推荐为每个数据源创建一个自定义类,每个类最适合特定数据源。
现代数据源
Apollo为Apollo Server 4维护以下开源数据源:
类 | 示例 | 用于与……结合使用 |
---|---|---|
RESTDataSource | 查看获取Rest | HTTP/REST API |
社区维护以下为Apollo Server 4的开源数据源:
类 | 源 | 用于与……结合使用 |
---|---|---|
BatchedSQLDataSource | 社区 | SQL数据库(通过Knex.js)& 批量处理(通过DataLoader) |
FirestoreDataSource | 社区 | Cloud Firestore |
旧数据源类
⚠️ 注意:社区为Apollo Server 3构建了以下每个数据源包。如图所示,您仍然可以通过额外的设置在Apollo Server 4中使用这些包。如下,您仍然可以在Apollo Server 4中使用这些包,只需进行一些额外的设置。
以下数据源实现扩展了从已弃用的DataSource
抽象类派生出的。DataSource类的子类定义了与特定存储或API通信所需的逻辑。
更大的社区维护以下开源实现
类 | 源 | 用于与……结合使用 |
---|---|---|
HTTPDataSource | 社区 | HTTP/REST API |
SQLDataSource | 社区 | SQL数据库(通过Knex.js) |
MongoDataSource | 社区 | MongoDB |
CosmosDataSource | 社区 | Azure Cosmos DB |
Apollo不会为社区维护的库提供官方支持。我们无法保证社区维护的库遵循最佳实践,或者它们将得到持续维护。
使用 DataSource
子类
在 Apollo Server 3 中,在构建每个 DataSource
子类之后,服务器会暗中调用每个新的 DataSource
的 initialize({ cache, context })
方法。
要在 Apollo Server 4 中重现此操作,您可以在每个 DataSource
子类的构造函数中手动调用 initialize
方法,如下所示:
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { KeyValueCache } from '@apollo/utils.keyvaluecache';import { Pool } from 'undici';import { HTTPDataSource } from 'apollo-datasource-http';class MoviesAPI extends HTTPDataSource {override baseURL = 'https://movies-api.example.com/';constructor(options: { cache: KeyValueCache<string>; token: string }) {// the necessary arguments for HTTPDataSourceconst pool = new Pool(baseURL);super(baseURL, { pool });// We need to call the initialize method in our data source's// constructor, passing in our cache and contextValue.this.initialize({ cache: options.cache, context: options.token });}async getMovie(id: string): Promise<Movie> {return this.get<Movie>(`movies/${encodeURIComponent(id)}`);}}interface MyContext {dataSources: {moviesApi: MoviesAPI;};token?: string;}const server = new ApolloServer<MyContext>({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async ({ req }) => {const { cache } = server;const token = req.headers.token;return {dataSources: {moviesApi: new MoviesAPI({ cache, token }),},token,};},});
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { Pool } from 'undici';import { HTTPDataSource } from 'apollo-datasource-http';class MoviesAPI extends HTTPDataSource {baseURL = 'https://movies-api.example.com/';constructor(options) {// the necessary arguments for HTTPDataSourceconst pool = new Pool(baseURL);super(baseURL, { pool });// We need to call the initialize method in our data source's// constructor, passing in our cache and contextValue.this.initialize({ cache: options.cache, context: options.token });}async getMovie(id) {return this.get(`movies/${encodeURIComponent(id)}`);}}const server = new ApolloServer({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async ({ req }) => {const { cache } = server;const token = req.headers.token;return {dataSources: {moviesApi: new MoviesAPI({ cache, token }),},token,};},});