emberjs / data

A lightweight reactive data library for web applications. Designed over composable primitives.
https://api.emberjs.com/ember-data/release
Other
3.03k stars 1.33k forks source link

[Question] Get an already fetched record from cache #8543

Closed Jopie01 closed 1 year ago

Jopie01 commented 1 year ago

This is more of a question / thought experiment then an issue. Since PR #8539 everything is working, data is fetched and put into cache. The next time data is taken from cache instead from the backend. I had to make a small adjustment because the data is nested a level deeper (see my repo for what changed).

Imagine the following: I store icon data in my backend and fetch that on application start so I can use those icons throughout my application. I do a call to the backend and get a list of icons back with id, name, and icon_data. This data is then put into the cache where the unique identifier is the hash of the query and the data is the list of icons.

content: { 
  data: [{ 
    type: 'icon', 
    id: '1', 
    attributes: { 
      name: 'my_icon_name',
      icon-data: '<svg>...</svg>'
    } 
   }, {
    type: 'icon', 
    id: '2', 
    attributes: { 
      name: 'my_second_icon_name' ,
      icon-data: '<svg>...</svg>'
    }
  }, {
    type: 'icon', 
    id: '3', 
    attributes: { 
      name: 'my_n_icon_name',
      icon-data: '<svg>...</svg>'
    } 
  }]
} 

This works pretty well, the next the query is called and the hash is the same, data is taken from the cache instead of the backend.

Now I want to use an icon and I only know the name, so I do a this.store.query('icon', [['name', '=', 'my_icon_name']]); because I already have the query working and in the end I only take the first record. Because I already fetched the icons, I would expect that this request doesn't reach to the backend to get the icon. But the hash of this query is different and therefore it reaches out to the backend to fetch the data for that icon (which already exists in cache). Also to note, there is no relationship.

So I basically want one record out of the list of data but that document has the wrong key, so the fetch doesn't find anything and asks the backend for the data. Instead of the this.store.query I can create a this.store.queryRecord which then first tries to find something from the cache and then reaches out to the backend.

Is the above possible? Are there some functions / hooks I can call?

runspired commented 1 year ago

There are a few approaches here

1) if name is a unique key for the icon, then this is essentially the same as "the username problem", which is how we describe the common issue where folks use a username for the route's url, mapping it to id in the request to get data, but get back a record that has the actual id. In effect username is a secondary unique-key.

The username problem is solved by configuring the identifiers cache. (Lots of things are solved by configuring this cache, it's why we coordinate identity of opaque objects in this way). You'll want to review these tests for an example implementation and setup that addresses the username problem https://github.com/emberjs/data/blob/a7f521572f55e1845f694d07c6db8d9287b479b3/tests/main/tests/integration/identifiers/scenarios-test.ts#L39

2) if name is not unique but you want to be able to query local data in the cache, there are 4 approaches

a. use `peekAll` and filter. Ultimately if EmberData ever builds in any sort of sync-client-side-query capability it'll probably be built over `peekAll` and use filter itself too. We've spent a lot of time optimizing live-arrays that would be hard to beat for building up query indeces and whatnot.

b. build secondary request caches in your Cache implementation that populate additional queries, or do so dynamically. E.g. `Cache.peekRequest` could dynamically create a fake request based on data it already has.

c. RFC something akin to `Cache.query` to be a sync method that caches may implement to respond to client-side queries. We're intending to experiment on something like this at some point, but even with it would encourage only using this via (d)

d. create a request handler that uses a combo of [(a) and (b)] or (c) to generate a response. This has the benefit of working with any async storage, so could even work with an indexdb or worker approach.
Jopie01 commented 1 year ago

Thanks for the thorough answer! I'm first going to look into 2a which should be sufficient at first. Then see if I can use handlers and secondary request caches.

BTW, I only have a few @ember-data packages installed, not the whole thing.

"dependencies": {
  "@babel/core": "^7.21.4",
  "@ember-data/debug": "4.12.0-alpha.19",
  "@ember-data/graph": "4.12.0-alpha.19",
  "@ember-data/json-api": "4.12.0-alpha.19",
  "@ember-data/request": "4.12.0-alpha.19",
  "@ember-data/store": "4.12.0-alpha.19",
  "@ember-data/tracking": "4.12.0-alpha.19",
  "crypto-js": "^4.1.1",
  "ember-simple-auth": "^5.0.0",
  "pnpm": "8.1.0"
}
runspired commented 1 year ago

Closing as nothing more to do here (quest linked will be responsible for ensuring the answer here is eventually codified into guides and docs)

Jopie01 commented 1 year ago

To make this complete, I ended up using this.peekAll and find to get the record. If there is no result I hand the request over to the query method which then fetches the data from the backend.

queryRecord(modelName, query, options) {
  const data = this.peekAll(modelName).find(d => {
    query.domain.forEach(part => {
      const [key, operator, value] = part;
      if (d[key] === value) {
        return d;
      }
    });
  });
  if (data !== undefined) {
    return Promise.resolve(data);
  }

  const promise = this.query(modelName, query, options);
  return promise.then(document => document[0]);
}

Maybe this can be moved into a request handler at some point, but that is going to be more advanced. I'm now returning one record, but it should be possible then to return multiple records. Also the question then pops up if the cache has all the records or not?. So I'm keeping it this way for the moment.