ファウナでページ抜きを取り扱う方法
導入
FQL(Foounaの照会言語)のレゾルバを書くことは、このタイプのプログラミングに精通していない場合は特に地球上で最も簡単なことではありません.一見したら変に見えるかもしれない.私が聞いている主なことは、「関数の中に関数の中に関数を持たなければならないことがあります.」
この記事ではget started with Fauna 既定のクエリを持つ基本的なセットアップ(スキーマをインポートするときに自動的に構築)からレゾルバを使用してクエリを作成するには、デフォルトのペグ化リゾルバを再作成します.最後に、複数のフィルタを受け入れる複雑なレゾルバに移動します.
スキーマの作成から始めましょう.我々のデータモデルは、レゾルバと実装からあなたを邪魔しないように簡単です.
第1章:スキーマ
データのスキーマを使用します.
type Student {
name: String
tests: [Test] @relation
}
type Project {
name: String
tests: [Test] @relation
}
type Test {
name: String
student: Student!
project: Project!
}
私たちは3つのモデルを持っている.学生、プロジェクト、テスト.それぞれのテストをプロジェクトに割り当てる必要があります.学生は、後で学生にリンクされているテストを受けることができます.
今のようにスキーマをアップロードする場合、ボックスからいくつかのクエリを受け取ります.スキーマをアップしましょう.
GraphSQLタブに移動し、インポートスキーマを押します.
スキーマをアップロードした後、クエリや突然変異のいくつかは自動的に私たちのために作成されます.我々は3クエリをご利用いただけます
findProjectByID
, findStudentByID
and findTestByID
.つのドキュメントプロジェクト、学生またはテストからのすべてのデータを受信します.
すべてのテストやプロジェクトを取得するには、スキーマに適切なクエリを追加する必要があります.
# ...
type Query {
projects: [Project]
tests: [Test]
}
スキーマのアップロード後、すべてのテストとプロジェクトを取得することもできます上記の手順の後、スキーマは次のようになります.
type Student {
name: String
tests: [Test] @relation
}
type Project {
name: String
tests: [Test] @relation
}
type Test {
name: String
student: Student
project: Project
}
type Query {
projects: [Project]
tests: [Test]
}
各ステップを自分で試してみたいなら、いくつかのダミーデータを持っているのが役に立つでしょう.これが使えますgist 上記のスキーマに一致する偽のデータを追加するには.<fauna_secret>
node generate.js
第2章:ページ付きクエリ
たった今、我々は3つのデフォルト質問を我々が一つの文書と我々の2つの質問からデータを得るのを許して、テストまたはプロジェクトを得ます.
どのような場合は、プロジェクトXからのみテストのような特定のデータを取得したいですか?
特定のプロジェクトを取得し、テストフィールドを使用して関連テストを取得できます.
query TestByProject {
findTestByID(id: <test_id>) {
project: {
data: {...}
}
}
}
それは常に簡単ではないが、カスタムレゾルバで新しいクエリを作成する理由です.新しいクエリをスキーマに追加します.グラフファイル
type Query {
...
getTestsByProject(id: ID): [Project] @resolver(name: "tests_by_project", paginated: true)
}
使用によって@resolver
ディレクティブは、このクエリのリゾルバを使用することを指定します.リクエストを処理する関数の名前を渡します.paginated: true
フラグは、新しいクエリが既定のものと同じように動作することを確認します.それは代わりにすべてを一度に送信するデータをページ化します.スキーマの更新後、新しい関数
tests_by_project
「機能」タブに表示されます.今すぐに新しいクエリを使用しようとすると、エラーが表示されます.それで、それをしましょう.このような問い合わせを処理できるインデックスがあるかどうかを確認する必要があります.指定したプロジェクトIDに一致するすべてのテストを取得したい場合は、インデックスタブに移動すると、既に作成された名前のインデックスが表示されます
project_tests_by_project
. それはまさに我々が必要とすることです.このインデックスを持っていない場合や、そのように作成する方法を知りたい場合は、シェルタブのコンソールで使用できるスクリプトがあります.
CreateIndex({
name: "project_tests_by_project",
source: Collection("Test"),
terms: [
{
field: ["data", "project"]
}
]
})
ここで、いくつかのコードをTestsCore Bytleプロジェクト関数に追加する必要があります.基本的に2つのことをする必要があります.最初の部分から始めましょう.Query(
Lambda(
["projectID"],
Let({
project: Ref(Collection("Project"), Var("projectID")),
match: Match(Index("project_tests_by_project"), Var("project")),
data: Paginate(Var("match"))
},
Map(Var("data"), Lambda("ref", Get(Var("ref"))))
)
)
)
ラムダが取る最初の引数は、我々の問い合わせが探す射撃です.次に、使用Let()
関数は、ラムダが段階的に行うものを明確にする変数のいくつかを定義します.プロジェクトの下に、プロジェクトのIDを表す文字列を格納します.実際のドキュメントでフィルタリングするには、ドキュメントにrefを必要とするので、「プロジェクト」変数の下に1つを作成します.
マッチの下にある変数は問い合わせを満たすすべてのドキュメントを探します、そして、最終的に「データ」変数は文書を保存します.match ()によって返される集合からドキュメントを"展開する"には、Paginate関数を使用する必要があります.次の手順では、見つかった各ドキュメントを反復処理してデータを取得します.
ページネーション.追加後
paginated
レゾルバラムダへのフラグは3つの追加引数を受け取ります.size -単一のクエリで返されるドキュメントの数を指定する
後/前-クエリを開始する場所を示します(両方のクエリで返されるので、我々は最後のクエリから“後”を使用することができますし、データの次のセットを取得する)
Paginate()
関数.これらの引数は、値が格納されているかどうかをスキップします.Query(
Lambda(
["projectID", "size", "after", "before"],
Let(
{
...
data: If(
And(IsNull(Var("after")), IsNull(Var("before"))),
Paginate(Var("match"), { size: Var("size") }),
If(
IsNull(Var("before")),
Paginate(Var("match"), { after: Var("after"), size: Var("size") }),
Paginate(Var("match"), { before: Var("before"), size: Var("size") })
)
)
},
...
)
)
)
第3章:データの表示
データを表示するにはreact-table 図書館.ページに表示するドキュメントの数だけを取得するには、ページ検索クエリを使用します.Faluna Graphical EdpointへのAPI呼び出しを実行するには、react-query ライブラリとgraphql-request .
つの基本構成から始めて、「すべてのプロジェクト」ページを作成しましょう.
// AllProjects.js
import React, { useContext } from "react";
import { useQuery } from "react-query";
import { gql } from "graphql-request";
import Table from "./Table";
import { GraphqlClientContext } from "./App";
export default function AllProjects() {
const { data, isLoading } = useProjects();
if (isLoading) {
return <span>Loading...</span>;
}
return <Table columns={columns} data={data} />;
}
function useProjects() {
const graphqlClient = useContext(GraphqlClientContext);
return useQuery("projects", async () => {
const {
projects: { data },
} = await graphqlClient.request(
gql`
query {
projects {
data {
_id
name
}
}
}
`
);
return projects;
});
}
const columns = [
{
Header: "ID",
accessor: "_id",
},
{
Header: "Name",
accessor: "name",
},
];
// Table.js
import { useTable } from "react-table";
import "./Table.scss";
export default function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({
columns,
data,
});
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps()}>{column.render("Header")}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
</tbody>
</table>
);
}
// App.js
import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { GraphQLClient } from "graphql-request";
import AllProjects from "./AllProjects";
const queryClient = new QueryClient();
const graphQLClient = new GraphQLClient(`https://graphql.fauna.com/graphql`, {
headers: {
authorization: "Bearer <fauna_secret>",
},
});
export const GraphqlClientContext = React.createContext();
function Main() {
return (
<Router>
<Switch>
<Route path="/projects">
<AllProjects />
</Route>
</Switch>
</Router>
);
}
function App() {
return (
<GraphqlClientContext.Provider value={graphQLClient}>
<QueryClientProvider client={queryClient}>
<Main />
</QueryClientProvider>
</GraphqlClientContext.Provider>
);
}
export default App;
これが基本的なセットアップです.フルリポジトリを見つけることができますhere .現在のセットアップでは、ページネーションをまったく処理しません.いくつかのケースでOKです.(例えば、私が確認できるならば、いくつかのプロジェクトを利用できるだけでしょう)
しかし、私たちのケースでは、私は確かにサーバー側のページ付けの利点を使用したいと思うので、多くのテストがあるでしょう.
PageMethodリクエストを送信することでページ化処理を行います.
// Table.js
import React from "react";
import { useTable, usePagination } from "react-table";
import "./Table.scss";
const pageSizeVariants = [50, 75, 100];
export default function Table({
columns,
data,
fetchData,
loading,
initialPageSize,
pageCount: controlledPageCount,
}) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
nextPage,
previousPage,
setPageSize,
// Get the state from the instance
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
initialState: { pageIndex: 0, pageSize: initialPageSize },
// We will be handling pagination by sending paginated request,
// not default client side, hence the manualPagination option
manualPagination: true,
pageCount: controlledPageCount,
},
usePagination
);
function changeSize(e) {
setPageSize(Number(e.target.value));
}
React.useEffect(() => {
fetchData({ pageIndex, pageSize });
}, [fetchData, pageIndex, pageSize]);
return (
<>
<table {...getTableProps()}>
<thead>{headerGroups.map(renderHeaderGroup)}</thead>
<tbody {...getTableBodyProps()}>
{page.map(renderPage(prepareRow))}
</tbody>
</table>
<div>
<button onClick={previousPage} disabled={!canPreviousPage}>
{"<"}
</button>{" "}
<button onClick={nextPage} disabled={!canNextPage}>
{">"}
</button>{" "}
<select value={pageSize} onChange={changeSize}>
{pageSizeVariants.map(renderOption)}
</select>
</div>
</>
);
}
function renderHeaderGroup(headerGroup) {
return (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps()}>{column.render("Header")}</th>
))}
</tr>
);
}
function renderPage(prepareRow) {
return function (row, i) {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
};
}
function renderOption(val) {
return (
<option key={val} value={val}>
Show {val}
</option>
);
}
テーブルコンポーネントに渡すための追加の小道具を必要としません.クエリから始めましょう.私たちは
getTestsByProject
. クエリ変数を定義する必要があります.query($id: ID, $size: Int, $cursor: String) {
getTestsByProject(id: $id, _size: $size, _cursor: $cursor) {
data {
id: _id
name
student {
id: _id
}
}
after
before
}
}
}
それが理論です.コードを書きましょう.
最初に新しいフック- usetest ()を作成します.
// useTests
function useTests(projectID) {
// react-table will send us the page index if user go back or next
const [page, setPage] = React.useState({ index: 0, cursor: null, size: 25 });
// we'll be using the GraphlClient to send requests
const graphqlClient = useContext(GraphqlClientContext);
const query = useQuery(
[key, page.size, page.cursor, projectID],
fetchProjects(graphqlClient)({ size: page.size, cursor: page.cursor, id: projectID })
);
return query
}
const fetchProjects = (client) => (variables) => async () => {
const { tests } = await client.request(
gql`
query($id: ID, $size: Int, $cursor: String) {
tests: getTestsByProject(id: $id, _size: $size, _cursor: $cursor) {
data {
id: _id
name
student {
name
}
}
after
before
}
}
`,
variables
);
return tests;
};
usequeryフックは、ページの状態を変更するたびに発生します.そして、使用されるメソッドのいくつかを加えた後に、ページネーションを扱います:
// useTests.js
function useTests(projectID) {
...
// under query.data we have all the results from `tests` query
// query.data -> { data, after, before }
const tests = query.data?.data || [];
const nextPageCursor = query.data?.after;
const prevPageCursor = query.data?.before;
const canNextPage = !!nextPageCursor;
function nextPage() {
if (!nextPageCursor) return;
setPage((page) => ({
...page,
index: page.index + 1,
cursor: nextPageCursor,
}));
}
const prevPageCursor = data?.before;
function prevPage() {
if (!prevPageCursor) return;
setPage((page) => ({
...page,
index: page.index - 1,
cursor: prevPageCursor,
}));
}
function changeSize(size) {
if (size === page.size) return;
setPage((page) => ({ index: page.index, cursor: null, size }));
}
function updateData({ pageIndex, pageSize }) {
if (pageSize !== page.size) changeSize(pageSize);
else if (pageIndex === page.index) return;
else if (pageIndex > page.index) nextPage();
else prevPage();
}
const canNextPage = !!nextPageCursor;
return {
...query,
data: tests,
size: page.size,
updateData,
// page + 1 gives actual number of pages (page is an index started from 0)
// Number(canNextPage) increase the pageCount by 1 if canNextPage == true
pageCount: page.index + 1 + Number(canNextPage),
};
}
ユーザーが次に行くことを決めた場合、nextPage ()メソッドを起動したい場合には、変更前のサイズ変更後にchangesize ()メソッドを返します.この論理はupdatedata ()の中で生きています.これはどんなページ/サイズ変更の後にも発射されます.「プロジェクト」コンポーネントで新しいメソッドを使用します
// Project.js
...
import { useParams } from "react-router-dom";
export default function Project() {
const { id } = useParams();
const { data, isLoading, pageCount, size, updateData } = useTests(id);
if (isLoading) {
return <span>Loading...</span>;
}
return (
<Table
columns={columns}
data={data}
fetchData={updateData}
pageCount={pageCount}
initialPageSize={size}
/>
);
}
const columns = [
{
Header: "ID",
accessor: "_id",
},
{
Header: "Name",
accessor: "name",
},
{
Header: "Student",
accessor: "student.name",
},
];
// App.js
...
<Router>
<Switch>
<Route path="/projects/:id">
<Project />
</Route>
<Route path="/projects">
<AllProjects />
</Route>
</Switch>
</Router>
...
これにより、各プロジェクトのページを入力することができます.ブラウザがヒットすると/project/<projectID>
ページプロジェクトコンポーネントは、useParams ()フックを使用してURLからIDを取得することができます.最後の変更は、すべてのプロジェクトテーブルのid列を変更して、特定のプロジェクトページへのリンクを作成することです.
// AllProjects.js
import { Link } from "react-router-dom";
...
const columns = [
{
Header: "ID",
accessor: ({ _id }) => <Link to={`/projects/${_id}`}>{_id}</Link>,
},
{
Header: "Name",
accessor: "name",
},
];
そして、今ではすべてのように見えます-私たちは完全にページ化されたクエリーを使用してページ化されたテーブルを機能させました🚀最終的な解決策をチェックしたいなら、ここでのリンクですrepository
でも.
別のクエリを書く代わりに、さらに一歩を踏み出したい
filter_by
あなたが使用したい、1つのクエリで複数のフィルタを受け入れる方法があります.複数の1つの目的質問の代わりに、あなたの質問にフィルタを使用したいと思う可能性が高いです.
query {
tests(filter: {
student: ["286712490662822407", "286712490702668289"],
project: ["286712490727835143"]
}) {
data {
id: _id
name
student {
id: _id
}
}
after
before
}
}
}
そのためには、各フィルタに対するインデックスを作成する必要があります.スキーマを使用した例# schema.graphql
#...
input TestFilters {
project: [ID]
student: [ID]
}
type Query {
# ...
tests(filter: TestFilters): [Test] @resolver(name: "get_tests", paginated: true)
#...
}
// get_tests.fql
Query(
Lambda(
["filters", "size", "after", "before"],
Let(
{
baseMatch: Match(Index("tests")),
// creates match for every id in in filter.project array
matchByProjects: Map(
Select("project", Var("filters"), []),
Lambda(
"id",
Match(
Index("project_tests_by_project"),
Ref(Collection("Project"), Var("id"))
)
)
),
// creates match for every id in in filter.student array
matchByStudents: Map(
Select("student", Var("filters"), []),
Lambda(
"id",
Match(
Index("student_tests_by_student"),
Ref(Collection("Student"), Var("id"))
)
)
),
// combines all matches into one array
// end up with [baseMatch, Union([projects]), Union([students])]
match: Reduce(
Lambda(
["acc", "curr"],
If(
IsArray(Var("curr")),
If(
// skips if empty
IsEmpty(Var("curr")),
Var("acc"),
Append(Union(Var("curr")), Var("acc"))
),
If(
IsNull(Var("curr")),
Var("acc"),
Append([Var("curr")], Var("acc")),
)
)
),
[],
[
Var("baseMatch"),
Var("matchByProjects"),
Var("matchByStudents")
]
),
intersectionMatch: Intersection(Var("match")),
item: If(
Equals(Var("before"), null),
If(
Equals(Var("after"), null),
Paginate(Var("intersectionMatch"), { size: Var("size") }),
Paginate(Var("intersectionMatch"), {
after: Var("after"),
size: Var("size")
})
),
Paginate(Var("intersectionMatch"), {
before: Var("before"),
size: Var("size")
})
)
},
Map(Var("item"), Lambda("ref", Get(Var("ref"))))
)
)
)
その場合は、同じクエリを使用して多くの要求をカバーすることができますし、以下の機能を維持する必要があります.私は個人的に1つの目的のリゾルバから起動し、マルチフィルタレゾルバには、同じコレクションの多くのフィルタリゾルバを持って切り替えます.
Reference
この問題について(ファウナでページ抜きを取り扱う方法), 我々は、より多くの情報をここで見つけました https://dev.to/aargrk/how-to-handle-pagination-with-fauna-3ehfテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol