MichalLytek / type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
https://typegraphql.com
MIT License
8.03k stars 676 forks source link

[doc] DI vs Apollo Context/@Ctx #601

Open desmap opened 4 years ago

desmap commented 4 years ago

Early on in the docs in the resolvers chapter you introduce DI as a way to access data without re-initializing new classes. I used this with typedi to access the DB connection.

However, I realized that Apollo Context and injecting with it with @Ctx might be better for this use case. It's shorter, quicker and you don't have to check if the connection is already established. I'd just mention it in the docs as alternative to DI, so people don't enter the rabbit hole of DI like I did (btw re our prior discussion, typedi is the best lib, I was wrong).

MichalLytek commented 4 years ago

The simplest answer is: @Ctx can be injected only to resolvers, while dependency injection works also in services and other layers 😉

desmap commented 4 years ago

Just tested it: Once a pure class method which is not exposed/decorated as GraphQL API in a @Resolver decorated class, the @Ctx injection in that method doesn't work anymore, right. Ok and good to know!

So, once I would go deeper in the business logic DI comes handy. Do you have some concrete examples in mind?

desmap commented 4 years ago

What would be nice though: a way to easily inject Apollo's context into the entire class without the need to inject it manually in every class method. Is this possible somehow?

MichalLytek commented 4 years ago

a way to easily inject Apollo's context into the entire class without the need to inject it manually in every class method

See the scoped containers example - there you create a fresh container for each "request", you put the context in the container and then you can inject that to the scoped services.

So, once I would go deeper in the business logic DI comes handy. Do you have some concrete examples in mind?

Dependency injection is for injecting dependencies, context is for placing request-related objects like decoded user from JWT.

wonbyte commented 4 years ago

@desmap check this example from Apollo

https://github.com/apollographql/fullstack-tutorial/blob/master/final/server/src/datasources/user.js#L19

You can Access the context in your Datasource directly for whateve but @MichalLytek is right. It’s mainly for Request related things

desmap commented 4 years ago

See the scoped containers example - there you create a fresh container for each "request", you put the context in the container and then you can inject that to the scoped services.

Ok but then I would reconnect on every request to the DB (Mongo) and open another DB connection which is also not what I should do, right?

Dependency injection is for injecting dependencies, context is for placing request-related objects like decoded user from JWT.

Then I should clearly get the DB connect via DI but I just struggle (offtopic now but fwiw) with creating an async constructor. So, I have a DbClient class with a async connect method and accessing the class instance is cumbersome because I have to await the connect every time and first (because the dependent doesn't know if it is the first connect) before I can access the DB. Or is there an easier way?

desmap commented 4 years ago

Example code to the previous post: here the dependency I inject:

@Service()
export default class DbClient {
  connection

  async connect(dbName: string) {
    if (!this.connection) {
      this.connection = await MongoClient.connect(
        `mongodb://root:${process.env.MONGO_INITDB_ROOT_PASSWORD}@db:27017`,
      )
    }
    return this.connection.db(dbName)
  }
}

Before I can do anything with the db I have to const db = await.this.dbClient.connect("db") in the class where I inject DbClient. When using context I can skip this await.

wonbyte commented 4 years ago

Maybe look at TypeORM. You can instantiate the connection on application start and then just get a connection instance when you need it. Maybe something like

import {getConnection} from "typeorm";

// can be used once createConnection is called and is resolved
const connection = getConnection();

from here https://typeorm.io/#/connection

desmap commented 4 years ago

@wonbyte awesome and simple idea, thanks! It's working and is quite elegant.

Now I realize that the db-name I use depends on the hostname of the request. I could now pass @Ctx to the DIed dbClient methods and this would probably the correct method. But it would be still easier to send everything pre-configured & host-dependent with Apollo context. If you guys have any further architectural advice pls let me know.

desmap commented 4 years ago

Ok, fyi, my current design: It features DRY, DI and Apollo Context:

  1. Everything is declared in the to be DIed class DbClient. Everything means connection but also shortcuts to all collections which is handy but also avoids spelling errors of eg collection names.
  2. You can DI 1 in any class but it's also initialized in the main function and also placed into Apollo's context.
  3. Now you can choose how to get to the DB instance either via DI or via @Ctx depending on your local use case, all DRY and it's always the same instance. If DIed you need to pass Ctx' hostname which makes the DI version a bit longer but yeah.

Feedback welcome!

:-)

backbone87 commented 4 years ago

general stuff on db connections and DIs: the thing what you normally do with relational databases is to create a connection pool (which is a service in your app/DIC). a connection pool keeps a configurable number of db connections around (usually a few dozen). the connection pool also deals with closing broken connection and creating new connections. in your resolver classes you inject the pool and when you want to do a query, you get a connection from it (which may be an existing connection). this is an async operation (because the pool might need to establish a new connection or it has to wait for an exclusive connection to be returned and the pool is at the connection limit). its also important to return the connection to the pool, if you requested an exclusive connection (see RAII pattern), which you would want to do if you do transactions or streaming results. a good example of how a pool is implemented can be seen in the pg npm package.

for graphql: you normally dont want to share connections directly between resolvers (let the pool handle the sharing) and you usually dont want cross resolver transactions (which would be the only time you would need direct connection sharing between resolvers).

on typeorm: in typeorm you usually inject the entitymanager, which itself wraps the typeorm connection, which is a wrapper around the platform specific connections, where the latter are usually the connection pool implementations of each platform specific client lib.

ByBogon commented 3 years ago

@desmap Hi desmap, I'm thinking about DI and Apollo Context for my project. If you don't mind, could you give me some example about.. how to use DI and Apollo Context for db connection?

Thanks.

oleh-kolinko commented 2 years ago

Apollo recommends using Data Sources, they are automatically added to the context and Apollo creates a new instance of each data source for each operation. type-graphql doesn't mention in the documentation about it, would be great to add examples using them. I understand that I can pass them around with @Ctx but I'm still confused if using DI makes that obsolete or how do I elegantly combine them to gather benefits of both

  1. Is Apollo's Data Sources implementation work similarly to scoped DI containers?
  2. Can I still use Apollo's RESTDataSource caching with basic DI or do I have to configure scoped containers?
  3. For example, I have 3 layers : 1)API Resolvers 2)Domain (Business logic) 3)Data Source What's the best way to do this? a) Initialize Data sources with Apollo and pass around with @Ctx (resolver -> domain) b) use DI (data -> domain -> resolver) in case of b) How can I use Apollo caching?