ariga.io/entcache@v0.1.1-0.20230620164151-0eb723a11c40/README.md (about)

     1  # entcache
     2  
     3  An experimental cache driver for [ent](https://github.com/ent/ent) with variety of storage options, such as:
     4  
     5  1. A `context.Context`-based cache. Usually, attached to an HTTP request.
     6  
     7  2. A driver level cache embedded in the `ent.Client`. Used to share cache entries on the process level.
     8  
     9  4. A remote cache. For example, a Redis database that provides a persistence layer for storing and sharing cache
    10    entries between multiple processes.
    11  
    12  4. A cache hierarchy, or multi-level cache allows structuring the cache in hierarchical way. For example, a 2-level cache
    13     that composed from an LRU-cache in the application memory, and a remote-level cache backed by a Redis database.
    14  
    15  ## Quick Introduction
    16  
    17  First, `go get` the package using the following command.
    18  
    19  ```shell
    20  go get ariga.io/entcache
    21  ```
    22  
    23  After installing `entcache`, you can easily add it to your project with the snippet below:
    24  
    25  ```go
    26  // Open the database connection.
    27  db, err := sql.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
    28  if err != nil {
    29  	log.Fatal("opening database", err)
    30  }
    31  // Decorates the sql.Driver with entcache.Driver.
    32  drv := entcache.NewDriver(db)
    33  // Create an ent.Client.
    34  client := ent.NewClient(ent.Driver(drv))
    35  
    36  // Tell the entcache.Driver to skip the caching layer
    37  // when running the schema migration.
    38  if client.Schema.Create(entcache.Skip(ctx)); err != nil {
    39  	log.Fatal("running schema migration", err)
    40  }
    41  
    42  // Run queries.
    43  if u, err := client.User.Get(ctx, id); err != nil {
    44  	log.Fatal("querying user", err)
    45  }
    46  // The query below is cached.
    47  if u, err := client.User.Get(ctx, id); err != nil {
    48  	log.Fatal("querying user", err)
    49  }
    50  ```
    51  
    52  **However**, you need to choose the cache storage carefully before adding `entcache` to your project.
    53  The section below covers the different approaches provided by this package.
    54  
    55  
    56  ## High Level Design
    57  
    58  On a high level, `entcache.Driver` decorates the `Query` method of the given driver, and for each call, generates a cache
    59  key (i.e. hash) from its arguments (i.e. statement and parameters). After the query is executed, the driver records the
    60  raw values of the returned rows (`sql.Rows`), and stores them in the cache store with the generated cache key. This
    61  means, that the recorded rows will be returned the next time the query is executed, if it was not evicted by the cache store.
    62  
    63  The package provides a variety of options to configure the TTL of the cache entries, control the hash function, provide
    64  custom and multi-level cache stores, evict and skip cache entries. See the full documentation in
    65  [go.dev/entcache](https://pkg.go.dev/ariga.io/entcache).
    66  
    67  ### Caching Levels
    68  
    69  `entcache` provides several builtin cache levels:
    70  
    71  1. A `context.Context`-based cache. Usually, attached to a request and does not work with other cache levels.
    72  It is used to eliminate duplicate queries that are executed by the same request.
    73  
    74  2. A driver-level cache used by the `ent.Client`. An application usually creates a driver per database,
    75  and therefore, we treat it as a process-level cache.
    76  
    77  3. A remote cache. For example, a Redis database that provides a persistence layer for storing and sharing cache
    78    entries between multiple processes. A remote cache layer is resistant to application deployment changes or failures,
    79    and allows reducing the number of identical queries executed on the database by different process.
    80  
    81  4. A cache hierarchy, or multi-level cache allows structuring the cache in hierarchical way. The hierarchy of cache
    82  stores is mostly based on access speeds and cache sizes. For example, a 2-level cache that composed from an LRU-cache
    83  in the application memory, and a remote-level cache backed by a Redis database.
    84  
    85  #### Context Level Cache
    86  
    87  The `ContextLevel` option configures the driver to work with a `context.Context` level cache. The context is usually
    88  attached to a request (e.g. `*http.Request`) and is not available in multi-level mode. When this option is used as
    89  a cache store, the attached `context.Context` carries an LRU cache (can be configured differently), and the driver
    90  stores and searches entries in the LRU cache when queries are executed.
    91  
    92  This option is ideal for applications that require strong consistency, but still want to avoid executing duplicate
    93  database queries on the same request. For example, given the following GraphQL query:
    94  
    95  ```graphql
    96  query($ids: [ID!]!) {
    97      nodes(ids: $ids) {
    98          ... on User {
    99              id
   100              name
   101              todos {
   102                  id
   103                  owner {
   104                      id
   105                      name
   106                  }
   107              }
   108          }
   109      }
   110  }
   111  ```
   112  
   113  A naive solution for resolving the above query will execute, 1 for getting N users, another N queries for getting
   114  the todos of each user, and a query for each todo item for getting its owner (read more about the
   115  [_N+1 Problem_](https://entgo.io/docs/tutorial-todo-gql-field-collection/#problem)).
   116  
   117  However, Ent provides a unique approach for resolving such queries(read more in
   118  [Ent website](https://entgo.io/docs/tutorial-todo-gql-field-collection)) and therefore, only 3 queries will be executed
   119  in this case. 1 for getting N users, 1 for getting the todo items of **all** users, and 1 query for getting the owners
   120  of **all** todo items.
   121  
   122  With `entcache`, the number of queries may be reduced to 2, as the first and last queries are identical (see
   123  [code example](internal/examples/ctxlevel/main_test.go)).
   124  
   125  ![context-level-cache](https://github.com/ariga/entcache/blob/assets/internal/assets/ctxlevel.png)
   126  
   127  ##### Usage In GraphQL
   128  
   129  In order to instantiate an `entcache.Driver` in a `ContextLevel` mode and use it in the generated `ent.Client` use the
   130  following configuration.
   131  
   132  ```go
   133  db, err := sql.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
   134  if err != nil {
   135  	log.Fatal("opening database", err)
   136  }
   137  drv := entcache.NewDriver(db, entcache.ContextLevel())
   138  client := ent.NewClient(ent.Driver(drv))
   139  ```
   140  
   141  Then, when a GraphQL query hits the server, we wrap the request `context.Context` with an `entcache.NewContext`.
   142  
   143  ```go
   144  srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
   145  	if op := graphql.GetOperationContext(ctx).Operation; op != nil && op.Operation == ast.Query {
   146  		ctx = entcache.NewContext(ctx)
   147  	}
   148  	return next(ctx)
   149  })
   150  ```
   151  
   152  That's it! Your server is ready to use `entcache` with GraphQL, and a full server example exits in
   153  [examples/ctxlevel](internal/examples/ctxlevel).
   154  
   155  ##### Middleware Example
   156  
   157  An example of using the common middleware pattern in Go for wrapping the request `context.Context` with
   158  an `entcache.NewContext` in case of `GET` requests.
   159  
   160  ```go
   161  srv.Use(func(next http.Handler) http.Handler {
   162  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   163  		if r.Method == http.MethodGet {
   164  			r = r.WithContext(entcache.NewContext(r.Context()))
   165  		}
   166  		next.ServeHTTP(w, r)
   167  	})
   168  })
   169  ```
   170  
   171  #### Driver Level Cache
   172  
   173  A driver-based level cached stores the cache entries on the `ent.Client`. An application usually creates a driver per
   174  database (i.e. `sql.DB`), and therefore, we treat it as a process-level cache. The default cache storage for this option
   175  is an LRU cache with no limit and no TTL for its entries, but can be configured differently.
   176  
   177  ![driver-level-cache](https://github.com/ariga/entcache/blob/assets/internal/assets/drvlevel.png)
   178  
   179  ##### Create a default cache driver, with no limit and no TTL.
   180  
   181  ```go
   182  db, err := sql.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
   183  if err != nil {
   184  	log.Fatal("opening database", err)
   185  }
   186  drv := entcache.NewDriver(db)
   187  client := ent.NewClient(ent.Driver(drv))
   188  ```
   189  
   190  ##### Set the TTL to 1s.
   191  
   192  ```go
   193  drv := entcache.NewDriver(drv, entcache.TTL(time.Second))
   194  client := ent.NewClient(ent.Driver(drv))
   195  ```
   196  
   197  ##### Limit the cache to 128 entries and set the TTL to 1s.
   198  
   199  ```go
   200  drv := entcache.NewDriver(
   201      drv,
   202      entcache.TTL(time.Second),
   203      entcache.Levels(entcache.NewLRU(128)),
   204  )
   205  client := ent.NewClient(ent.Driver(drv))
   206  ```
   207  
   208  #### Remote Level Cache
   209  
   210  A remote-based level cache is used to share cached entries between multiple processes. For example, a Redis database.
   211  A remote cache layer is resistant to application deployment changes or failures, and allows reducing the number of
   212  identical queries executed on the database by different processes. This option plays nicely the multi-level option below. 
   213  
   214  #### Multi Level Cache
   215  
   216  A cache hierarchy, or multi-level cache allows structuring the cache in hierarchical way. The hierarchy of cache
   217  stores is mostly based on access speeds and cache sizes. For example, a 2-level cache that compounds from an LRU-cache
   218  in the application memory, and a remote-level cache backed by a Redis database.
   219  
   220  ![context-level-cache](https://github.com/ariga/entcache/blob/assets/internal/assets/multilevel.png)
   221  
   222  ```go
   223  rdb := redis.NewClient(&redis.Options{
   224      Addr: ":6379",
   225  })
   226  if err := rdb.Ping(ctx).Err(); err != nil {
   227      log.Fatal(err)
   228  }
   229  drv := entcache.NewDriver(
   230      drv,
   231      entcache.TTL(time.Second),
   232      entcache.Levels(
   233          entcache.NewLRU(256),
   234          entcache.NewRedis(rdb),
   235      ),
   236  )
   237  client := ent.NewClient(ent.Driver(drv))
   238  ```
   239  
   240  ### Future Work
   241  
   242  There are a few features we are working on, and wish to work on, but need help from the community to design them
   243  properly. If you are interested in one of the tasks or features below, do not hesitate to open an issue, or start a
   244  discussion on GitHub or in [Ent Slack channel](https://entgo.io/docs/slack).
   245  
   246  1. Add a Memcache implementation for a remote-level cache.
   247  2. Support for smart eviction mechanism based on SQL parsing.