-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(backend): チャンネルフォロー一覧のsinceId/untilIdによる絞り込みが上手く動いていないのを修正 #13698
base: develop
Are you sure you want to change the base?
fix(backend): チャンネルフォロー一覧のsinceId/untilIdによる絞り込みが上手く動いていないのを修正 #13698
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #13698 +/- ##
===========================================
- Coverage 64.91% 64.90% -0.01%
===========================================
Files 989 989
Lines 113003 113018 +15
Branches 5777 5776 -1
===========================================
Hits 73352 73352
- Misses 38209 38224 +15
Partials 1442 1442 ☔ View full report in Codecov by Sentry. |
このPRによるapi.jsonの差分 差分はこちら |
q.orderBy(`${q.alias}.id`, 'DESC'); | ||
q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId }); | ||
q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId }); | ||
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SELECT
"MiChannelFollowing"."id" AS "MiChannelFollowing_id",
"MiChannelFollowing"."followeeId" AS "MiChannelFollowing_followeeId",
"MiChannelFollowing"."followerId" AS "MiChannelFollowing_followerId"
FROM
"channel_following" "MiChannelFollowing"
WHERE
"MiChannelFollowing"."followeeId" < $ 1
AND "MiChannelFollowing"."followerId" = $ 2
ORDER BY
"MiChannelFollowing"."followeeId" DESC
LIMIT
30
↑のような感じで、外から任意のIDカラムを条件として使用できるようにしました。
省略時は従来通りidを使います
channel.idをIDとして与えている箇所って具体的にどこかしら? |
見た感じバックエンドは現行の実装で何も問題なさそうだった |
(いまコメントに気が付いた) |
①フロントエンド側の
|
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] { | |
return entities.map(en => [en.id, en]); | |
} |
misskey/packages/frontend/src/components/MkPagination.vue
Lines 239 to 328 in c1514ce
const fetchMore = async (): Promise<void> => { | |
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; | |
moreFetching.value = true; | |
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | |
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { | |
...params, | |
limit: SECOND_FETCH_LIMIT, | |
...(props.pagination.offsetMode ? { | |
offset: offset.value, | |
} : { | |
untilId: Array.from(items.value.keys()).at(-1), | |
}), | |
}).then(res => { | |
for (let i = 0; i < res.length; i++) { | |
const item = res[i]; | |
if (i === 10) item._shouldInsertAd_ = true; | |
} | |
const reverseConcat = _res => { | |
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight(); | |
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY; | |
items.value = concatMapWithArray(items.value, _res); | |
return nextTick(() => { | |
if (scrollableElement.value) { | |
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); | |
} else { | |
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); | |
} | |
return nextTick(); | |
}); | |
}; | |
if (res.length === 0) { | |
if (props.pagination.reversed) { | |
reverseConcat(res).then(() => { | |
more.value = false; | |
moreFetching.value = false; | |
}); | |
} else { | |
items.value = concatMapWithArray(items.value, res); | |
more.value = false; | |
moreFetching.value = false; | |
} | |
} else { | |
if (props.pagination.reversed) { | |
reverseConcat(res).then(() => { | |
more.value = true; | |
moreFetching.value = false; | |
}); | |
} else { | |
items.value = concatMapWithArray(items.value, res); | |
more.value = true; | |
moreFetching.value = false; | |
} | |
} | |
offset.value += res.length; | |
}, err => { | |
moreFetching.value = false; | |
}); | |
}; | |
const fetchMoreAhead = async (): Promise<void> => { | |
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; | |
moreFetching.value = true; | |
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | |
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { | |
...params, | |
limit: SECOND_FETCH_LIMIT, | |
...(props.pagination.offsetMode ? { | |
offset: offset.value, | |
} : { | |
sinceId: Array.from(items.value.keys()).at(-1), | |
}), | |
}).then(res => { | |
if (res.length === 0) { | |
items.value = concatMapWithArray(items.value, res); | |
more.value = false; | |
} else { | |
items.value = concatMapWithArray(items.value, res); | |
more.value = true; | |
} | |
offset.value += res.length; | |
moreFetching.value = false; | |
}, err => { | |
moreFetching.value = false; | |
}); | |
}; |
②バックエンド側のQueryService.makePaginationQuery()
はid
というプロパティを期待しており、これをページネーションのsinceId
/untilId
に使用している
const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId) | |
.andWhere({ followerId: me.id }); |
misskey/packages/backend/src/core/QueryService.ts
Lines 43 to 68 in c1514ce
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> { | |
if (sinceId && untilId) { | |
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); | |
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); | |
q.orderBy(`${q.alias}.id`, 'DESC'); | |
} else if (sinceId) { | |
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); | |
q.orderBy(`${q.alias}.id`, 'ASC'); | |
} else if (untilId) { | |
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); | |
q.orderBy(`${q.alias}.id`, 'DESC'); | |
} else if (sinceDate && untilDate) { | |
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); | |
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) }); | |
q.orderBy(`${q.alias}.id`, 'DESC'); | |
} else if (sinceDate) { | |
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); | |
q.orderBy(`${q.alias}.id`, 'ASC'); | |
} else if (untilDate) { | |
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) }); | |
q.orderBy(`${q.alias}.id`, 'DESC'); | |
} else { | |
q.orderBy(`${q.alias}.id`, 'DESC'); | |
} | |
return q; | |
} |
ここで渡しているのはchannelFollowingsRepository
のクエリビルダーなので、channel_followings.idがsinceId
/untilId
として渡されることが想定されている
③バックエンドから返される時にpackされているが、この時のIDはchannel
のものになる
return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me))); |
id: channel.id, |
①+②+③
フロントエンドにはchannel.id
をキーとした一覧が返却され、MkPagination
によるページネーションもこのIDが使用される。channels/followedのsinceId
/untilId
にはchannel.id
が渡ってくることになるが、channels/followedそのものはchannel_followings.id
を使って絞り込みをしており、不正確な条件で絞り込みをしてしまう(IDの種類が違うので)
@syuilo |
ふぉろーIDを想定しているAPIにチャンネルIDを渡しているのを修正しないとダメそう |
互換性を維持したまま直すとしたら新しいエンドポイント作るしかなさそうね |
フォロー中ユーザー一覧ってどんなレスポンスだったっけ |
misskey/packages/backend/src/core/entities/FollowingEntityService.ts Lines 90 to 101 in 514a65e
followingのIDでした |
それと同じ形式で返すエンドポイントを作るしかないわね |
うーむ。 |
方針がよくなさそうなので、このPRは白紙にしますか。 |
個人的な考えとして…
という2つの問題があると捉えていて、前者と後者は方向性が違う(ので両方やって良さそう)と思っているのですが、syuiloさんとしては両方やるのは冗長…という感覚でしょうか?
|
自分の理解が間違ってるかもしれないけど、このPRで修正されるとは思えない(違う種類のデータ同士を比較しているから、期待したデータが返って来たとしても偶然と思われる) |
sinceId/untilIdをフォローした時間ベースのものであると捉えると正にそうなのですが、しかし「"v1(現行)は返ってきているものがChannelであり、ChannelFollowingではない"ので、v1(現行)におけるsinceId/untilIdがチャンネルが作成された時間ベースのものであってもおかしくはない」と思うのですが、いかがでしょうか…? |
あー |
それならおかしくないわね |
とはいえこのエンドポイントのためだけに makePaginationQuery を拡張したりするのは混乱が生まれそう |
makePaginationQueryを少し弄ったコピペ版を作成し(ただし、安易に複製しないでほしい旨と経緯、このissue/prをコメントで添える)、それを使ってページング処理をするようにすれば、すべてクリアになりますかね…? |
ページネーションが壊れているのを直すという目的を考えると、offsetを追加してsinceId/untilIdは非推奨にするのがシンプルなのではないかと思います 変更に追従しないサードパーティは壊れたままになりますがレスポンスが異なる別のエンドポイントが増える場合よりも追従は容易になります |
確かに、offset式に変えるとページネーションは直せるかもしれません…? しかし、正しいものが返ってきていないということが発覚した時点で、"このエンドポイントに手を付けるならv2を作る"ということ自体はほぼ確定しているのでは、と思っています。 |
Channelの代わりにChannelFollowingを返すようにすることにどのような意味があるのでしょうか(フロントエンドでフォローした日時を表示する?) |
一般的にフォロー中の何かを一覧で返す場合は、フォローした順に一覧したいことが多そう |
現在の実装でもフォローした順に返されているのではないかと思います |
そうですが、代わりにページネーションで間が抜けます |
offsetにすれば抜けないのでは? |
サードパーティクライアントが(追従するまで)壊れたままになります |
サードパーティクライアントで壊れないようにするにはv1も修正する必要があると思います |
offsetを使うか、v2を生やすかのどちらが良いかは諸説あるかもしれませんが、sinceId/untilIdをした時に間が抜けなくなる実装はその話とは別な気もします。
私はそれを意図していますね。 |
What
チャンネルのフォロー一覧を返すAPIにて、sinceId/untilIdの比較条件として与えているIDに間違いがあるのを直します(与えられているIDはchannel.idだが、比較先のIDはchannel_following.id)
これにより、チャンネルフォロー一覧から結果が抜け落ちる現象が改善されます。
Why
fix #12175
Additional info (optional)
実際にチャンネルを大量に作成&お気に入り登録し、歯抜けにならないことを確認。
Checklist