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.