0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

一覧をランダム順に返す API でページネーションする時に順序を維持する

Last updated at Posted at 2021-06-13

背景

  • あるリソースの一覧をランダム順に返却する API でページネーションを行う必要があった
  • 単純に順序をランダム化するだけだとリクエストの度にランダム順に返ってしまう
  • 1ページ目をリクエストした時と2ページ目以降をリクエストした時で同じ順番に並んでいてほしい

環境

  • Ruby 2.7.3
  • Rails 6.1.3.1
  • PostgreSQL 12.5-alpine
  • React 17.0.2
  • TypeScript 3.9.9

設計

  • 要素をランダムに並べる時に seed という数値を指定すれば、 seed が同じである限り同じ順番にランダム化してくれることを利用する
  • フロント側で順序を維持したい範囲に seed を保持して、リクエストの度にパラメータとして送信する
  • サーバー側では seed が同じであれば同じ順番でランダム化された一覧を返すように実装する

サーバー側

実装

  • 今回は諸事情により Ruby でランダムに並べ替えていたので、下記のように実装した。
  • SQL でランダム化したい場合の実装は後述。
class StaffsController < ApplicationController
  def index
    page = params[:page].to_i
    per_page = params[:per_page].to_i
    seed = params[:randomize_seed].to_i
    staffs = Staff.all.shuffle(random: Random.new(seed)).slice((page - 1) * per_page, per_page) || []
    render json: { staffs: staffs }
  end
end

解説

shuffle(random: Random.new(seed)) のところが肝だと思うので解説していく。

  • Array#shuffle で配列をランダムに並び替える時に、引数として Random オブジェクトを渡すことができる。
  • Random::new で Random オブジェクトを初期化する際に seed として数値を渡すことができる。
  • params[:randomize_seed] でフロントから受け取った seed を渡すことで順番を維持できる。

フロント側

実装

const StaffListPage: React.FC = () => {
  const [staffs, setStaffs] = useState<Staff[]>([]);
  const [page, setPage] = useState(1);
  const seed = useMemo(() => Math.floor(Math.random() * 100), []);

  useEffect(() => {
    const fetchStaffs = async () => {
      const response = await fetch(`/api/staffs?page=${page}&per_page=10&randomize_seed=${seed}`, {
        method: 'GET',
      });
      const data = await response.json();
      setStaffs(data.staffs);
    };
  }, [setStaffs, page, seed]);

  return <StaffList staffs={staffs} onChangePage={setPage} />;
};

解説

  • Math.random()Math.floor() を用いて0から100までのランダムな数値を取得している
  • ランダムな数値がレンダリングの度に変わらないように useMemo でキャッシュしている
  • ページが切り替わっても同一の randomize_seed がサーバーに送信されるので順番が保持される

余談

  • 普通は SQL 側でランダム化・ページネーションを行いたいと思うので、その時の実装も検討。

MySQL

  • RAND() が利用できそう。
class StaffsController < ApplicationController
  def index
    page = params[:page].to_i
    per_page = params[:per_page].to_i
    seed = params[:randomize_seed].to_i
    staffs = Staff.order("RAND(#{seed})").page(page).per(per_page)
    render json: { staffs: staffs }
  end
end

PostgreSQL

class StaffsController < ApplicationController
  def index
    page = params[:page].to_i
    per_page = params[:per_page].to_i
    seed = params[:randomize_seed].to_i
    Staff.connection.execute("SELECT setseed(#{seed});")
    staffs = Staff.order("RANDOM()").page(page).per(per_page)
    render json: { staffs: staffs }
  end
end

備考

  • 要点を絞って伝えるために本番で動作しているコードの一部を簡略化して掲載したので、実際に実行してみて動かなかったらごめんなさい。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?