例
例えばこういう感じのid
が重複するパターンのデータをapolloを使って受け取った場合、
{
"data": {
"results": [
{
"id": 1,
"className": "ClassA",
"value": 42
},
{
"id": 1,
"className": "ClassB",
"value": 24
}
]
}
}
通常通りこういう感じで表示すると
const client = useApolloClient()
client.query({
query: QUERY,
})
.then(({ data }) => {
console.log(JSON.stringify(data, null, 2))
})
こんな感じになってしまう。(ClassAがClassBのデータに上書きされてる)
{
"results": [
{
"id": 1,
"className": "ClassB",
"value": 24
},
{
"id": 1,
"className": "ClassB",
"value": 24
}
]
}
原因
ApolloClientを作る時に特段事情がなければInMemoryCacheをこういう風にデフォルトのままで使ってると思う。
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
const cache = new InMemoryCache();
const client = new ApolloClient({
link: new HttpLink(),
cache
});
このままだと、GraphQLで返ってくるデータにid
(もしくは_id
)があると、これをキーとしてキャッシュしてしまう。この為、1つの問い合わせの中に複数テーブルのデータが混ざる場合、id
が重複してしまう可能性があり、これによりGraphQL自体は正しいが(Networkタブでは期待通りのデータが返ってきてる)、ApolloClientで取れるデータが期待通りにならない。
https://www.apollographql.com/docs/react/advanced/caching/#normalization
By default, InMemoryCache will attempt to use the commonly found primary keys of id and __id for the unique identifier if they exist along with __typename on an object.
解決方法
- uuidを使う
- ulidを使う
- InMemoryCacheでnormalizationをする
-
id
を架空のランダムな物にして(例えばその場でuuidをつくって使う)、_id
に本来入れるプリマリーキーを入れる
1の場合、グローバルにユニークになるのでこのcachingと相性が良くなるが、新規プロジェクトじゃないとかなりやっかい。その上uuidは基本ソート出来ないのでRailsなどで使うModel.first
みたいな物が期待した物にはならない。(idが登録順序にならないので)
2の場合、cachingとの相性、ソート可と便利だが、やはり新規プロジェクトじゃないとかなりやっかい。
3の場合、面倒。上の例だとApolloClientを作る際にこのような形でInMemoryCache
を設定する必要がある。似たようなクエリーがあればその数ほどやらないといけない。
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
...
...
const cache = new InMemoryCache({
dataIdFromObject: object => {
switch (object.__typename) {
case 'Result': return `${object.className}-${object.id}`;
default: return defaultDataIdFromObject(object);
}
}});
...
4の場合、React側でid
は使い物にならないが、とりあえず期待と違うキャッシングはされない。React側ではid
の代わりに_id
を使う(_id
にこだわる必要は無く名前は何でも良いが...)。
query {
results{
id
_id
className
value
}
}
# 例えばRuby側がサーバーで、データを返すとして
def ...
class_a = ClassA.find(id)
results = [
{
id: SecureRandom.uuid, # 例えばuuidを使う
_id: class_a.id, # React側ではこちらを使う
...
...
}
...
]
{ results: results }
end