Zuvor habe ich im folgenden Artikel eine Testimplementierung von Relay-Stil implementiert.
Die Kombination der Anforderungen "Wechseln zur vorherigen / nächsten Seite" und "Sortieren in aufsteigender / absteigender Reihenfolge nach Elementen" verursachte jedoch eine kompliziertere Implementierung als ich es mir vorgestellt hatte und war ziemlich unverdaulich. Dieses Mal habe ich versucht, die Implementierung auf der Back-End-Seite (aus der vorherigen Zeit) zu vereinfachen, indem ich eine architektonische Einschränkung auferlegte, dass die zu verwendende RDB PostgreSQL ist.
Außerdem werden wir diese einzelnen Sprachen und Bibliotheken nicht erklären.
Entspricht Letzter Front-End-Artikel.
Es hat die folgenden Funktionen auf der Seite, die einige Informationen auflisten (diesmal "Kunde" (Kunde)).
Holen Sie sich einfach alle Elemente, wenn die erste Seite angezeigt wird, und suchen Sie jedes Mal (so viel wie für eine Seite erforderlich) anstelle der vorherigen Seite und des nächsten Seitenübergangs im Speicher. Wenn Folgendes ausgeführt wird, kehrt die Anzeige zur ersten Seite zurück, auch wenn die Seite angezeigt wird (z. B. wird die zweite Seite angezeigt).
--Sortieren Sie in aufsteigender oder absteigender Reihenfolge nach Listenanzeigeelementen --Ändern Sie die Anzahl der in der Liste angezeigten Elemente
Seite
Seite
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
$ go version
go version go1.15.2 linux/amd64
v0.13.0
IDE - Goland
GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020
https://github.com/sky0621/study-graphql/tree/v0.10.0/try01
DB Führen Sie PostgreSQL v13 mit Docker Compse aus. (Da es nur lokal verwendet wird, schreiben Sie das Passwort usw. in fester Form)
docker-compose.yml
version: '3'
services:
db:
restart: always
image: postgres:13-alpine
container_name: study-graphql-postgres-container
ports:
- "25432:5432"
environment:
- DATABASE_HOST=localhost
- POSTGRES_DB=study-graphql-local-db
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=yuckyjuice
- PGPASSWORD=yuckyjuice
volumes:
- ./local/data:/docker-entrypoint-initdb.d/
Erstellen Sie eine "Kunden" -Tabelle in der obigen Datenbank.
CREATE TABLE customer (
id bigserial NOT NULL,
name varchar(64) NOT NULL,
age int NOT NULL,
PRIMARY KEY (id)
);
Die Datensätze in der Tabelle "Kunde" sind unten aufgeführt.
Eigentlich ist es nicht so kompliziert, wenn es nur der Relay-Teil ist, aber diesmal ist es eine Kombination aus "Suchfilter für Zeichenketten" und "Sortierung nach aufsteigend / absteigend nach jedem Element", daher ist die Definition etwas kompliziert.
$ tree schema/
schema/
├── connection.graphql
├── customer.graphql
├── order.graphql
├── pagination.graphql
├── schema.graphql
└── text_filter.graphql
■schema.graphql
# Global Object Identification ...Alle Daten mit einer gemeinsamen ID eindeutig
interface Node {
id: ID!
}
schema {
query: Query
}
type Query {
node(id: ID!): Node
}
■customer.graphql
extend type Query {
"Holen Sie sich die TODO-Liste durch Relay-kompatible Paging-kompatible Suche"
customerConnection(
"Paging-Bedingungen"
pageCondition: PageCondition
"Sortierbedingungen"
edgeOrder: EdgeOrder
"String-Filterbedingung"
filterWord: TextFilterCondition
): CustomerConnection
}
Dies ist die Abfrage, die diesmal vom Frontend aufgerufen wird. Eine Beschreibung jedes Elements wird später beschrieben. Es hat die folgenden Felder entsprechend den Anforderungen.
Stringfilterbedingung
"für String-Suchfilter (teilweise Übereinstimmung)Der Rückgabewert der Abfrage liegt in einem Relay-kompatiblen Verbindungsformat vor (wird auch später beschrieben).
CustomerConnection
"Für die Rückgabe von Ergebnissen mit Paging"
type CustomerConnection implements Connection {
"Seiteninformationen"
pageInfo: PageInfo!
"Suchergebnisliste (* einschließlich Cursorinformationen)"
edges: [CustomerEdge!]!
"Gesamtzahl der Suchergebnisse"
totalCount: Int64!
}
Zum Speichern des Ausführungsergebnisses der customerConnection-Abfrage. Konform mit Relaisspezifikationen (möglicherweise nicht auf der Ebene, Referenzstufe).
Die "Verbindungs" -Schnittstelle (später beschrieben) wird implementiert, damit sie für allgemeine Zwecke verwendet werden kann.
Seiteninformationen (PageInfo
) werden später beschrieben.
CustomerEdge
"Suchergebnisse (* einschließlich Cursorinformationen)"
type CustomerEdge implements Edge {
node: Customer!
cursor: Cursor!
}
Die "Edge" -Schnittstelle (später beschrieben) ist so implementiert, dass sie für allgemeine Zwecke verwendet werden kann. Zeigt die Suchergebnisse für einen Artikel an. Es enthält Informationen, die als "Cursor" zur Datenidentifikation bezeichnet werden. Der Typ "Cursor" wird später beschrieben.
Customer
type Customer implements Node {
"ID"
id: ID!
"Name"
name: String!
"Alter"
age: Int!
}
Repräsentiert einen Kunden.
■pagination.graphql PageCondition Ein Typ, der die an die Abfrage übergebene "Paging-Bedingung" darstellt.
"Paging-Bedingungen"
input PageCondition {
"Vorherige Seitenübergangsbedingung"
backward: BackwardPagination
"Übergangsbedingung für die nächste Seite"
forward: ForwardPagination
"Aktuelle Seitenzahl (Stand vor diesem Paging)"
nowPageNo: Int64!
"Anzahl der pro Seite angezeigten Elemente"
initialLimit: Int64!
}
BackwardPagination Paging-Bedingung zum Zeitpunkt des Übergangs "vorherige Seite" übergeben.
"Vorherige Seitenübergangsbedingung"
input BackwardPagination {
"Anzahl der Akquisitionen"
last: Int64!
"Cursor zur Erfassung des Erfassungsziels (* Datensätze vor diesem Cursor zum Zeitpunkt des Übergangs zur vorherigen Seite sind Erfassungsziele)"
before: Cursor!
}
ForwardPagination Die Paging-Bedingung wurde während des Übergangs "Nächste Seite" übergeben.
"Übergangsbedingung für die nächste Seite"
input ForwardPagination {
"Anzahl der Akquisitionen"
first: Int64!
"Cursor zur Erfassung des Erfassungsziels (* Datensätze hinter diesem Cursor werden beim Übergang zur nächsten Seite erfasst.)"
after: Cursor!
}
Cursor Im Cursor wird der URL-codierte Wert gespeichert, nachdem die beim Durchsuchen der Datenbank zugewiesene "ROW_NUMBER" mit dem Tabellennamen kombiniert wurde. Siehe unten.
"Cursor (Kennung, die einen Datensatz eindeutig identifiziert)"
scalar Cursor
■order.graphql EdgeOrder Ein Typ, der die an die Abfrage übergebene "Sortierbedingung" darstellt.
"Sortierbedingungen"
input EdgeOrder {
"Schlüsselelement sortieren"
key: OrderKey!
"Sortierrichtung"
direction: OrderDirection!
}
OrderKey
"""
Sortierschlüssel
[Betrachtungsprozess]
Ich wollte es zu einer universellen Struktur und typsicher machen, also habe ich versucht, es mit Eingabe oder Aufzählung für jede Funktion zu implementieren, nachdem ich es mit der Schnittstelle definiert habe.
Ich gab jedoch auf, weil die Eingabe eine Spezifikation war, die keine Schnittstelle implementieren konnte.
Ich wünschte, Enum hätte eine Vererbungsfunktion, aber das tat es nicht.
CustomerOrderKey und (falls vorhanden) Sortierschlüssel für andere Funktionen in Union|Ich habe auch darüber nachgedacht, wie ich mich verbinden soll
Ich habe auch aufgegeben, weil es eine Spezifikation war, dass Union nicht als Element in die Eingabe einbezogen werden konnte.
Ich wollte jedoch die Sortierung als gemeinsamen Mechanismus bereitstellen und habe daher die Aufzählungsfelder für jede Funktion in einer gemeinsamen Eingabe aufgelistet.
"""
input OrderKey {
"Sortierschlüssel für Benutzerliste"
customerOrderKey: CustomerOrderKey
}
OrderDirection
"Sortierrichtung"
enum OrderDirection {
"aufsteigende Reihenfolge"
ASC
"absteigende Reihenfolge"
DESC
}
■text_filter.graphql TextFilterCondition Ein Typ, der die "Zeichenfolgenfilterbedingung" darstellt, die an die Abfrage übergeben werden soll.
"String-Filterbedingung"
input TextFilterCondition {
"Filterzeichenfolge"
filterWord: String!
"Übereinstimmendes Muster"
matchingPattern: MatchingPattern!
}
MatchingPattern
"Passender Mustertyp (* Je nach Anforderung "Match starten" oder "Match beenden" hinzufügen)"
enum MatchingPattern {
"Teilweise Übereinstimmung"
PARTIAL_MATCH
"Perfekte Übereinstimmung"
EXACT_MATCH
}
■connection.graphql
scalar Int64
"Für die Rückgabe von Ergebnissen mit Paging"
interface Connection {
"Seiteninformationen"
pageInfo: PageInfo!
"Ergebnisliste (* einschließlich Cursorinformationen)"
edges: [Edge!]!
"Gesamtzahl der Suchergebnisse"
totalCount: Int64!
}
"Seiteninformationen"
type PageInfo {
"Mit oder ohne nächste Seite"
hasNextPage: Boolean!
"Mit oder ohne vorherige Seite"
hasPreviousPage: Boolean!
"1. Datensatz der Seite"
startCursor: Cursor!
"Letzte Aufzeichnung auf der Seite"
endCursor: Cursor!
}
"Suchergebnisliste (* einschließlich Cursorinformationen)"
interface Edge {
"Eine Ersetzung ist möglich, wenn der Typ die Knotenschnittstelle implementiert"
node: Node!
cursor: Cursor!
}
Dies ist nicht das Thema dieser Zeit, also präsentieren Sie einfach die Quelle.
server.go
package main
import (
"log"
"net/http"
"time"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/rs/cors"
"github.com/sky0621/study-graphql/try01/src/backend/graph"
"github.com/sky0621/study-graphql/try01/src/backend/graph/generated"
"github.com/volatiletech/sqlboiler/v4/boil"
)
func main() {
// MEMO:Da es nur lokal verwendet wird, ist es fest
dsn := "host=localhost port=25432 dbname=study-graphql-local-db user=postgres password=yuckyjuice sslmode=disable"
db, err := sqlx.Connect("postgres", dsn)
if err != nil {
log.Fatal(err)
}
boil.DebugMode = true
var loc *time.Location
loc, err = time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
boil.SetLocation(loc)
r := chi.NewRouter()
r.Use(corsHandlerFunc())
r.Handle("/", playground.Handler("GraphQL playground", "/query"))
r.Handle("/query",
handler.NewDefaultServer(
generated.NewExecutableSchema(
generated.Config{
Resolvers: &graph.Resolver{
DB: db,
},
},
),
),
)
if err := http.ListenAndServe(":8080", r); err != nil {
panic(err)
}
}
func corsHandlerFunc() func(h http.Handler) http.Handler {
return cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}).Handler
}
Dies ist die Quelle, die das Thema dieser Zeit trägt. Grob gesagt,
go:graph/customer.resolvers.go(Auszug)
func (r *queryResolver) CustomerConnection(ctx context.Context, pageCondition *model.PageCondition, edgeOrder *model.EdgeOrder, filterWord *model.TextFilterCondition) (*model.CustomerConnection, error) {
/*
*Zum Halten verschiedener Elemente, die für die SQL-Konstruktion erforderlich sind
*/
params := searchParam{
//Name der Zielerfassungstabelle für die Informationserfassung
tableName: boiled.TableNames.Customer,
//Die Standardreihenfolge ist absteigende ID
orderKey: boiled.CustomerColumns.ID,
orderDirection: model.OrderDirectionDesc.String(),
}
/*
*Einstellungen für Suchzeichenfolgenfilter
* TODO:Wenn Sie einen Filter auf mehrere Spalten anwenden möchten, verbinden Sie sich hier mit AND oder buildSearchQueryMod()Muss berücksichtigt werden, um zu erweitern
*/
filter := filterWord.MatchString()
if filter != "" {
params.baseCondition = fmt.Sprintf("%s LIKE '%s'", boiled.CustomerColumns.Name, filter)
}
/*
*Paging-Einstellungen
*/
if pageCondition.IsInitialPageView() {
//Erste Seitenansicht ohne Paging
params.rowNumFrom = 1
params.rowNumTo = pageCondition.InitialLimit
} else {
//Übergangsanweisung zur vorherigen Seite
if pageCondition.Backward != nil {
key, err := decodeCustomerCursor(pageCondition.Backward.Before)
if err != nil {
log.Print(err)
return nil, err
}
params.rowNumFrom = key - pageCondition.Backward.Last
params.rowNumTo = key - 1
}
//Übergangsanweisung zur nächsten Seite
if pageCondition.Forward != nil {
key, err := decodeCustomerCursor(pageCondition.Forward.After)
if err != nil {
log.Print(err)
return nil, err
}
params.rowNumFrom = key + 1
params.rowNumTo = key + pageCondition.Forward.First
}
}
/*
*Reihenfolge angeben
*/
if edgeOrder.CustomerOrderKeyExists() {
params.orderKey = edgeOrder.Key.CustomerOrderKey.String()
params.orderDirection = edgeOrder.Direction.String()
}
/*
*Suchausführung
*/
var records []*CustomerWithRowNum
if err := boiled.Customers(buildSearchQueryMod(params)).Bind(ctx, r.DB, &records); err != nil {
log.Print(err)
return nil, err
}
/*
*Erforderlich, um festzustellen, ob die nächste Seite und die vorherige Seite nach dem Paging vorhanden sind
*Zum Speichern der Anzahl der Ergebnisse nach Anwendung des Suchzeichenfolgenfilters
*/
var totalCount int64 = 0
{
var err error
if filter == "" {
totalCount, err = boiled.Customers().Count(ctx, r.DB)
} else {
totalCount, err = boiled.Customers(qm.Where(boiled.CustomerColumns.Name+" LIKE ?",
filterWord.MatchString())).Count(ctx, r.DB)
}
if err != nil {
log.Print(err)
return nil, err
}
}
/*
*Relaisrückgabeformat
*/
result := &model.CustomerConnection{
TotalCount: totalCount,
}
/*
*Konvertieren Sie Suchergebnisse in das Edge-Slice-Format
*/
var edges []*model.CustomerEdge
for _, record := range records {
edges = append(edges, &model.CustomerEdge{
Node: &model.Customer{
ID: strconv.Itoa(int(record.ID)),
Name: record.Name,
Age: record.Age,
},
Cursor: createCursor("customer", record.RowNum),
})
}
result.Edges = edges
//Berechnen Sie die Gesamtzahl der Seiten dieser Suche aus der Gesamtzahl der Suchergebnisse und der Anzahl der pro Seite angezeigten Elemente.
totalPage := pageCondition.TotalPage(totalCount)
/*
*Informationen, die für die Bildschirmanzeige und das nächste Paging auf der Clientseite erforderlich sind
*/
pageInfo := &model.PageInfo{
HasNextPage: (totalPage - pageCondition.MoveToPageNo()) >= 1, //Ist nach dem Übergang noch eine Seite vor Ihnen?
HasPreviousPage: pageCondition.MoveToPageNo() > 1, //Gibt es nach dem Übergang noch eine vorherige Seite?
}
if len(edges) > 0 {
pageInfo.StartCursor = edges[0].Cursor
pageInfo.EndCursor = edges[len(edges)-1].Cursor
}
result.PageInfo = pageInfo
return result, nil
}
params := searchParam{
//Name der Zielerfassungstabelle für die Informationserfassung
tableName: boiled.TableNames.Customer,
//Die Standardreihenfolge ist absteigende ID
orderKey: boiled.CustomerColumns.ID,
orderDirection: model.OrderDirectionDesc.String(),
}
Die obige Entität ist unten. Grundsätzlich überschreibt es mit den vom GraphQL-Client übergebenen Bedingungen, aber wenn es nicht angegeben ist, wird es zu Beginn initialisiert, wenn es einen Standard benötigt. (Selbst in der Funktion, die die SQL-Anweisung durch Übergabe von "searchParam" erstellt, wird sie tatsächlich initialisiert.)
search.go
type searchParam struct {
orderKey string
orderDirection string
tableName string
baseCondition string
rowNumFrom int64
rowNumTo int64
}
/*
*Einstellungen für Suchzeichenfolgenfilter
* TODO:Wenn Sie einen Filter auf mehrere Spalten anwenden möchten, verbinden Sie sich hier mit AND oder buildSearchQueryMod()Muss berücksichtigt werden, um zu erweitern
*/
filter := filterWord.MatchString()
if filter != "" {
params.baseCondition = fmt.Sprintf("%s LIKE '%s'", boiled.CustomerColumns.Name, filter)
}
Die Zeichenfolge für die Suche wird durch die folgende Funktion erstellt.
model/expansion.go
func (c *TextFilterCondition) MatchString() string {
if c == nil {
return ""
}
if c.FilterWord == "" {
return ""
}
matchStr := "%" + c.FilterWord + "%"
if c.MatchingPattern == MatchingPatternExactMatch {
matchStr = c.FilterWord
}
return matchStr
}
Für das Übereinstimmungsmuster werden vorerst nur exakte Übereinstimmungen und Teilübereinstimmungen vorbereitet. Sie können jedoch die Präfixübereinstimmung und die Suffixübereinstimmung nach Bedarf erhöhen.
model/models_gen.go
//Passender Mustertyp (* Je nach Anforderung "Match starten" oder "Match beenden" hinzufügen)
type MatchingPattern string
const (
//Teilweise Übereinstimmung
MatchingPatternPartialMatch MatchingPattern = "PARTIAL_MATCH"
//Perfekte Übereinstimmung
MatchingPatternExactMatch MatchingPattern = "EXACT_MATCH"
)
Wenn die Startseite angezeigt wird (kurz gesagt, wenn der Bildschirm zum ersten Mal geöffnet wird, werden die Sortierelemente geändert oder die Anzahl der in der Liste angezeigten Elemente geändert), gilt Folgendes.
if pageCondition.IsInitialPageView() {
//Erste Seitenansicht ohne Paging
params.rowNumFrom = 1
params.rowNumTo = pageCondition.InitialLimit
} else {
〜〜〜
}
Ob es sich um die erste Seite handelt oder nicht, wird wie folgt beurteilt.
model/expansion.go
func (c *PageCondition) IsInitialPageView() bool {
if c == nil {
return true
}
return c.Backward == nil && c.Forward == nil
}
Als nächstes sieht die Flusslinie zum Zeitpunkt des Übergangs zur vorherigen Seite oder zur nächsten Seite wie folgt aus.
〜〜〜
} else {
//Übergangsanweisung zur vorherigen Seite
if pageCondition.Backward != nil {
key, err := decodeCustomerCursor(pageCondition.Backward.Before)
if err != nil {
log.Print(err)
return nil, err
}
params.rowNumFrom = key - pageCondition.Backward.Last
params.rowNumTo = key - 1
}
//Übergangsanweisung zur nächsten Seite
if pageCondition.Forward != nil {
key, err := decodeCustomerCursor(pageCondition.Forward.After)
if err != nil {
log.Print(err)
return nil, err
}
params.rowNumFrom = key + 1
params.rowNumTo = key + pageCondition.Forward.First
}
}
Das Wichtigste dabei ist, den Cursor zu dekodieren.
Der Cursor ist URL-codiert in Form von " customer
+ ROW_NUMBER ".
ROW_NUMBER setzt voraus, dass dem Ergebnis unabhängig vom Inhalt der Suche eine Seriennummer zugewiesen wird (unabhängig davon, ob sie eingegrenzt oder in aufsteigender oder absteigender Reihenfolge ist).
decodeCustomerCursor(~~~~)
Dekodieren Sie wie folgt.
graph/customer.go
func decodeCustomerCursor(cursor string) (int64, error) {
modelName, key, err := decodeCursor(cursor)
if err != nil {
return 0, err
}
if modelName != "customer" {
return 0, errors.New("not customer")
}
return key, nil
}
Die Definition von "decodeCursor (~~~~)" lautet wie folgt.
graph/util.go
const cursorSeps = "#####"
func decodeCursor(cursor string) (string, int64, error) {
byteArray, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
return "", 0, err
}
elements := strings.SplitN(string(byteArray), cursorSeps, 2)
key, err := strconv.Atoi(elements[1])
if err != nil {
return "", 0, err
}
return elements[0], int64(key), nil
}
In der Abbildung unten sehen Sie, wie Sie die Datensätze für die Seite abrufen, die dieses Mal mit der obigen Logik angezeigt werden soll.
Derzeit ist der Zustand wie folgt.
・ Die Anzahl der pro Seite angezeigten Elemente beträgt 5
・ In absteigender Reihenfolge der ID angeordnet
・ Der Status, in dem die zweite Seite angezeigt wird
1. Seite, 2. Seite, 3. Seite
ROW_NUMBER: [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]
■ Bei Anweisung zum Übergang zur "vorherigen Seite"
Ich möchte die Datensätze 1 bis 5 auf der ersten Seite.
pageCondition.Backward.ROW zuvor dekodiert_In NUMBER (gibt den ersten Datensatz auf der zweiten Seite an)[6]Ist enthalten.
Auch pageCondition.Backward.Die Anzahl der Elemente, die pro Seite in Last angezeigt werden[5 Fälle]Ist enthalten.
Bestimmen Sie daher den Bereich, den Sie erfassen möchten, anhand der folgenden Berechnung.
From:6 - 5 = 1
To :6 - 1 = 5
■ Im Falle einer Anweisung zum Übergang zur "nächsten Seite"
Ich möchte die Datensätze 11 bis 15 auf der dritten Seite.
pageCondition.Forward.REIHE nach dekodiert_In NUMBER (gibt den letzten Datensatz auf der zweiten Seite an)[10]Ist enthalten.
Auch pageCondition.Forward.Anzahl der Elemente, die pro Seite in First angezeigt werden[5 Fälle]Ist enthalten.
Bestimmen Sie daher den Bereich, den Sie erfassen möchten, anhand der folgenden Berechnung.
From:10 + 1 = 11
To :10 + 5 = 15
if edgeOrder.CustomerOrderKeyExists() {
params.orderKey = edgeOrder.Key.CustomerOrderKey.String()
params.orderDirection = edgeOrder.Direction.String()
}
Die Definition von "CustomerOrderKeyExists ()" lautet wie folgt.
model/expansion.go
func (o *EdgeOrder) CustomerOrderKeyExists() bool {
if o == nil {
return false
}
if o.Key == nil {
return false
}
if o.Key.CustomerOrderKey == nil {
return false
}
return o.Key.CustomerOrderKey.IsValid()
}
Die wichtigsten Kandidaten für die Sortierung in Bezug auf "Kunden" -Informationen sind wie folgt.
model/modege_gen.go
type CustomerOrderKey string
const (
// ID
CustomerOrderKeyID CustomerOrderKey = "ID"
//Nutzername
CustomerOrderKeyName CustomerOrderKey = "NAME"
//Alter
CustomerOrderKeyAge CustomerOrderKey = "AGE"
)
var records []*CustomerWithRowNum
if err := boiled.Customers(buildSearchQueryMod(params)).Bind(ctx, r.DB, &records); err != nil {
log.Print(err)
return nil, err
}
Erstens ist die Struktur von "CustomerWithRowNum", dem Slice-Typ als "Datensätze", wie folgt.
boiled.Customer
ist eine Struktur, die automatisch von SQL Boiler aus der DB-Tabellendefinition generiert wird.
Schließen Sie dies ein und behalten Sie die ROW_NUMBER, die Sie als row_num
erhalten, in Ihrer SQL-Anweisung mit dem Namen RowNum
bei.
Auf diese Weise muss beim Empfang des Ausführungsergebnisses der SQL-Anweisung nicht nacheinander eine Struktur erstellt werden, die der Tabellendefinition entspricht, und es können nur die Elemente hinzugefügt werden, die Sie hinzufügen möchten.
graph/customer.go
type CustomerWithRowNum struct {
RowNum int64 `boil:"row_num"`
boiled.Customer `boil:",bind"`
}
Als nächstes lautet die Definition von "buildSearchQueryMod (params)" wie folgt.
graph/search.go
// TODO:Ich habe es vorerst grob gemacht. Wie vielseitig es sein kann, beispielsweise mehrere Tabellen zu unterstützen, hängt von den Anforderungen ab.
func buildSearchQueryMod(p searchParam) qm.QueryMod {
if p.baseCondition == "" {
p.baseCondition = "true"
}
q := `
SELECT row_num, * FROM (
SELECT ROW_NUMBER() OVER (ORDER BY %s %s) AS row_num, *
FROM %s
WHERE %s
) AS tmp
WHERE row_num BETWEEN %d AND %d
`
sql := fmt.Sprintf(q,
p.orderKey, p.orderDirection,
p.tableName,
p.baseCondition,
p.rowNumFrom, p.rowNumTo,
)
return qm.SQL(sql)
}
Verwenden Sie die PostgreSQL-Fensterfunktion (ROW_NUMBER ()
), um den Ergebnissen der Anwendung des angegebenen String-Suchfilters und der Sortierung Seriennummern zuzuweisen.
Extrahieren Sie aus dem Ergebnis den gewünschten Bereich von ROW_NUMBER.
Unabhängig vom Sortierelement, der aufsteigenden oder absteigenden Reihenfolge können Sie jetzt denselben Mechanismus für "vorherige Seite" und "nächste Seite" erhalten, indem Sie den Bereich von ROW_NUMBER angeben.
Wie im Kommentar angegeben, wird die Anzahl der Ergebnisse der verfeinerten Suche durch den Suchzeichenfolgenfilter erfasst und nach dem Seitenübergang, ob die vorherige (nächste) Seite noch vorhanden ist (* Durch die Rückgabe dieser Informationen befindet sich das Front-End im UI-Design , Sie können die Aktivierung / Inaktivität der Tasten [Zurück] und [Weiter] steuern.
/*
*Erforderlich, um festzustellen, ob die nächste Seite und die vorherige Seite nach dem Paging vorhanden sind
*Zum Speichern der Anzahl der Ergebnisse nach Anwendung des Suchzeichenfolgenfilters
*/
var totalCount int64 = 0
{
var err error
if filter == "" {
totalCount, err = boiled.Customers().Count(ctx, r.DB)
} else {
totalCount, err = boiled.Customers(qm.Where(boiled.CustomerColumns.Name+" LIKE ?",
filterWord.MatchString())).Count(ctx, r.DB)
}
if err != nil {
log.Print(err)
return nil, err
}
}
Schreiben Sie mit SQL Boiler einfach "boiled.Customers (). Count (ctx, r.DB)" unter Verwendung der automatisch generierten Quelle " Sie können die Gesamtzahl der Kundentische abrufen. Wenn Sie eine Suchbedingung hinzufügen möchten, schreiben Sie sie einfach in den Teil "xxxx" von "gekocht". Kunden (xxxx) "wie in der obigen Quelle, und verwenden Sie dabei die von SQL Boiler vorbereitete Beschreibungsmethode.
In dem von Relay benötigten Rückgabeformat benötigen Sie lediglich " Kanten
"und" pageInfo
". Aufgrund des UI-Designs möchten Sie jedoch normalerweise die Anzahl der Fälle, sodass auch "totalCount" definiert wird.
https://relay.dev/graphql/connections.htm#sec-Connection-Types
/*
*Relaisrückgabeformat
*/
result := &model.CustomerConnection{
TotalCount: totalCount,
}
edges
Die Decodierung des Cursors erfolgt wie oben beschrieben, die Codierung erfolgt jedoch hier. Generieren Sie für jedes Suchergebnis einen Cursor aus "ROW_NUMBER". Wenn Sie dies an das Front-End zurückgeben, können Sie Paging auf der Front-End-Seite realisieren, indem Sie dem Parameter beim nächsten Seitenübergang einfach einen Cursor hinzufügen (ohne insbesondere den Erfassungsbereich anzugeben).
/*
*Konvertieren Sie Suchergebnisse in das Edge-Slice-Format
*/
var edges []*model.CustomerEdge
for _, record := range records {
edges = append(edges, &model.CustomerEdge{
Node: &model.Customer{
ID: strconv.Itoa(int(record.ID)),
Name: record.Name,
Age: record.Age,
},
Cursor: createCursor("customer", record.RowNum),
})
}
result.Edges = edges
"CustomerEdge" hat die folgende Struktur.
model/models_gen.go
//Suchergebnisliste (* einschließlich Cursorinformationen)
type CustomerEdge struct {
Node *Customer `json:"node"`
Cursor string `json:"cursor"`
}
Die Definition von "createCursor (modelName, key)" lautet wie folgt.
graph/util.go
const cursorSeps = "#####"
func createCursor(modelName string, key int64) string {
return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s%s%d", modelName, cursorSeps, key)))
}
pageInfo
Da hier Informationen berechnet und zurückgegeben werden müssen, wird die Verarbeitung am Frontend erleichtert. Folgendes ist als Seiteninformation erforderlich.
model/models_gen.go
//Seiteninformationen
type PageInfo struct {
//Mit oder ohne nächste Seite
HasNextPage bool `json:"hasNextPage"`
//Mit oder ohne vorherige Seite
HasPreviousPage bool `json:"hasPreviousPage"`
//1. Datensatz der Seite
StartCursor string `json:"startCursor"`
//Letzte Aufzeichnung auf der Seite
EndCursor string `json:"endCursor"`
}
Berechnen Sie zunächst die "Gesamtzahl der Seiten", um das "Vorhandensein oder Fehlen der nächsten Seite" zu bestimmen.
//Berechnen Sie die Gesamtzahl der Seiten dieser Suche aus der Gesamtzahl der Suchergebnisse und der Anzahl der pro Seite angezeigten Elemente.
totalPage := pageCondition.TotalPage(totalCount)
Die Definition von "TotalPage (~~)" lautet wie folgt.
model/expansion.go
func (c *PageCondition) TotalPage(totalCount int64) int64 {
if c == nil {
return 0
}
var targetCount int64 = 0
if c.Backward == nil && c.Forward == nil {
targetCount = c.InitialLimit
} else {
if c.Backward != nil {
targetCount = c.Backward.Last
}
if c.Forward != nil {
targetCount = c.Forward.First
}
}
return int64(math.Ceil(float64(totalCount) / float64(targetCount)))
}
Unter Verwendung des Obigen kann "Vorhandensein oder Nichtvorhandensein der nächsten Seite" wie folgt bestimmt werden.
/*
*Informationen, die für die Bildschirmanzeige und das nächste Paging auf der Clientseite erforderlich sind
*/
pageInfo := &model.PageInfo{
HasNextPage: (totalPage - pageCondition.MoveToPageNo()) >= 1, //Ist nach dem Übergang noch eine Seite vor Ihnen?
HasPreviousPage: pageCondition.MoveToPageNo() > 1, //Gibt es nach dem Übergang noch eine vorherige Seite?
}
Die Definition von "MoveToPageNo ()", die auch in der obigen Beurteilung "Vorhandensein / Fehlen einer vorherigen Seite" verwendet wird, lautet wie folgt.
model/expansion.go
func (c *PageCondition) MoveToPageNo() int64 {
if c == nil {
return 1 //Erste Seite wegen unerwarteten
}
if c.Backward == nil && c.Forward == nil {
return c.NowPageNo //Weil es nicht nach vorne oder hinten übergeht
}
if c.Backward != nil {
if c.NowPageNo <= 2 {
return 1
}
return c.NowPageNo - 1
}
if c.Forward != nil {
return c.NowPageNo + 1
}
return 1 //Erste Seite wegen unerwarteten
}
Danach werden der erste und der letzte Cursor getrennt aus dem Datensatz der von dieser Suche angezeigten Seite extrahiert.
if len(edges) > 0 {
pageInfo.StartCursor = edges[0].Cursor
pageInfo.EndCursor = edges[len(edges)-1].Cursor
}
result.PageInfo = pageInfo
Dieser Cursor verwendet " StartCursor
"beim Übergang zur" vorherigen Seite "beim nächsten Seitenübergang im Frontend und" EndCursor
" beim Übergang zur "nächsten Seite".
PageCondition
Backward
Vor ・ ・ ・ StartCursor
Forward
Nach ・ ・ ・ EndCursor
Die Quelle ist unten. https://github.com/sky0621/study-graphql/tree/v0.10.0/try01/src/frontend
Dies ist die gleiche Struktur wie in dem Artikel, den ich zuvor geschrieben habe, daher wird die Erklärung weggelassen. Bitte beachten Sie Folgendes. Paging-Implementierung nach Relay-Stil in GraphQL (Teil 2: Front-End)
GraphQL-Antwortdaten auf Seite 3
k
".Damit wurde vorerst das Paging (und das Sortieren nach Element und Kombination von Zeichenketten-Suchfiltern) auf der Seite "Kundenliste" realisiert. Selbst als Backend-Implementierung hat SQL einheitlich dasselbe Format, ohne den Wert des Sortierelements im Cursor wie [vorherige] zu haben (https://qiita.com/sky0621/items/1e8823200633f2c46013). Ich kann jetzt schlagen. Die Massenproduktion für jede Funktion ist jedoch zu viel für die Kesselplatte, sodass bei der tatsächlichen Verwendung so viel wie möglich eine Vorlage erstellt werden muss. Außerdem schreibe ich TODO in die Kommentare in der Quelle, aber es gibt verschiedene Probleme.
Recommended Posts