[GO] Überprüfung der Paging-Implementierung nach Relay-Stil in GraphQL (Version mit Fensterfunktion)

Thema

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.

In dieser Beispielimplementierung verwendete Sprachen, Bibliotheken usw.

Außerdem werden wir diese einzelnen Sprachen und Bibliotheken nicht erklären.

Vorderes Ende

Entspricht Letzter Front-End-Artikel.

Backend

Andere

Angenommene Spezifikationen

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

Bildschirmbild

Wenn die Startseite angezeigt wird (Standardmäßig sind die Spezifikationen in absteigender Reihenfolge der ID angeordnet).

screenshot-localhost_3000-2020.11.15-23_36_44.png

Zum Zeitpunkt des Übergangs zur zweiten Seite

screenshot-localhost_3000-2020.11.16-00_59_48.png

Bei Verwendung eines Suchfilters

screenshot-localhost_3000-2020.11.16-01_01_41.png screenshot-localhost_3000-2020.11.16-01_02_42.png

Nach Namen in aufsteigender Reihenfolge sortieren

  1. Seite screenshot-localhost_3000-2020.11.16-01_03_40.png
  2. Seite screenshot-localhost_3000-2020.11.16-01_03_54.png
  3. Seite screenshot-localhost_3000-2020.11.16-01_04_06.png

Die Anzahl der in der Liste angezeigten Elemente wurde auf 10 geändert (+ "Alter" in absteigender Reihenfolge).

  1. Seite screenshot-localhost_3000-2020.11.16-01_06_09.png

  2. Seite screenshot-localhost_3000-2020.11.16-01_06_21.png

Zugehöriger Artikelindex

Entwicklungsumgebung

OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"

#Backend

#Language --Golang

$ go version
go version go1.15.2 linux/amd64

gqlgen

v0.13.0

IDE - Goland

GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020

Alle Quellen diesmal

https://github.com/sky0621/study-graphql/tree/v0.10.0/try01

Trainieren

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.

GraphQL-Schema

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

customerConnection-Abfrage

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.

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!
}

Backend

Hauptfunktion

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
}

Resolver zur Kundenlistenerfassung, der Paging unterstützt

Dies ist die Quelle, die das Thema dieser Zeit trägt. Grob gesagt,

    1. Definieren Sie die Struktur der Parameter, die zum Erstellen von SQL-Anweisungen für die Suche erforderlich sind
  1. Wenn im GraphQL-Client "Suchzeichenfolge" angegeben ist, wird dies in der obigen Struktur wiedergegeben.
    1. Wenn "Paging" vom GraphQL-Client angegeben wird (kurz gesagt, ob es sich um die erste Seitenanzeige, die vorherige Seite oder die nächste Seite handelt), wird dies in der obigen Struktur wiedergegeben.
  2. Wenn im GraphQL-Client "Sortierreihenfolge" angegeben ist, wird dies in der obigen Struktur wiedergegeben.
  3. SQL-Ausführung für die Suche
  4. Konvertieren Sie die Suchergebnisse in das Relaisformat und kehren Sie zurück

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
}

Struktur der Parameter, die zum Erstellen der SQL-Anweisung für die Suche 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(),
	}

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

	/*
	 *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"
)

Paging-Einstellungen

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

Reihenfolge angeben

	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"
)

Suchausführung

	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.

Konvertieren Sie die Suchergebnisse in das Relaisformat und kehren Sie zurück

Anzahl der Ergebnisse nach Anwendung des Suchzeichenfolgenfilters

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.

Relaisrückgabeformat

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

Vorderes Ende

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)

Funktionsprüfung

Wenn die erste Seite angezeigt wird (in absteigender Reihenfolge der ID)

DB-Status

Screenshot at 2020-11-16 23-12-21.png

Ergebnis des Bildschirmübergangs

1. Seite

screenshot-localhost_3000-2020.11.16-23_17_37.png

2. Seite

screenshot-localhost_3000-2020.11.16-23_17_55.png

3. Seite

screenshot-localhost_3000-2020.11.16-23_18_09.png

GraphQL-Antwortdaten auf Seite 3 Screenshot at 2020-11-16 23-21-30.png

Zurück zu Seite 2

screenshot-localhost_3000-2020.11.16-23_24_45.png

Änderung in aufsteigender Reihenfolge der ID

DB-Status

Screenshot at 2020-11-16 23-25-51.png

Ergebnis des Bildschirmübergangs

1. Seite

screenshot-localhost_3000-2020.11.16-23_26_42.png

2. Seite

screenshot-localhost_3000-2020.11.16-23_26_54.png

3. Seite

screenshot-localhost_3000-2020.11.16-23_27_05.png

Zurück zu Seite 2

screenshot-localhost_3000-2020.11.16-23_27_18.png

Änderung in absteigender Reihenfolge des Namens

DB-Status

Screenshot at 2020-11-16 23-29-36.png

Ergebnis des Bildschirmübergangs

1. Seite

screenshot-localhost_3000-2020.11.16-23_31_17.png

2. Seite

screenshot-localhost_3000-2020.11.16-23_31_29.png

3. Seite

screenshot-localhost_3000-2020.11.16-23_31_40.png

Zurück zu Seite 2

screenshot-localhost_3000-2020.11.16-23_32_22.png

Aufsteigende Reihenfolge des Alters und Änderung auf "10" pro Seitenanzeige

DB-Status

Screenshot at 2020-11-16 23-33-46.png

Ergebnis des Bildschirmübergangs

1. Seite

screenshot-localhost_3000-2020.11.16-23_35_09.png

2. Seite

screenshot-localhost_3000-2020.11.16-23_35_22.png

Zurück zu Seite 1

screenshot-localhost_3000-2020.11.16-23_35_34.png

Wechseln Sie zum Filter in aufsteigender Reihenfolge von Name und " k ".

DB-Status

Screenshot at 2020-11-16 23-39-52.png

Ergebnis des Bildschirmübergangs

screenshot-localhost_3000-2020.11.16-23_40_35.png

Zusammenfassung

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

Überprüfung der Paging-Implementierung nach Relay-Stil in GraphQL (Version mit Fensterfunktion)
Implementierung der Login-Funktion in Django
Unterschied in der Ausgabe der Fensterfunktion mit gerader Länge
Rank Learning über ein neuronales Netzwerk (RankNet-Implementierung von Chainer)
Ein Memo, dass ich eine Grundfunktion in Python mit Wiederholung geschrieben habe