Koncepcje, Idee, Strategie
Koncepcje, Idee, StrategieMapowanie schematu GraphQL dla Twojej strony, motywu lub wtyczki WordPress

Mapowanie schematu GraphQL dla Twojej strony, motywu lub wtyczki WordPress

Zdecydowałeś się więc zacząć używać GraphQL na swoim istniejącym serwisie WordPress. Świetnie! Niezależnie od tego, czy jest używany do nowych, czy istniejących funkcjonalności, GraphQL będzie musiał współdziałać z podstawową warstwą danych, dla której będziesz musiał odwzorować model danych swojej aplikacji (czy to niestandardowy kod PHP na Twojej stronie WordPress, w motywie lub we wtyczce) na schemat GraphQL.

Jak należy przeprowadzić mapowanie? Czy musi być wykonane od razu? Czy powinno być dokładną repliką istniejącego modelu danych? Co z poprawieniem nieodpowiedniej nazwy w trakcie? A w kwestii długu technicznego — czy należy go utrzymać, czy rozwiązać?

Przyjrzyjmy się kilku strategiom mapowania modelu danych istniejącej aplikacji WordPress na schemat GraphQL.

Mapuj schemat we własnym tempie

Dodanie GraphQL do aplikacji nie jest rozwiązaniem „wszystko albo nic". Ta sama aplikacja może być zasilana przez kilka API jednocześnie, w takim przypadku GraphQL będzie współistniał z innymi API tak długo, jak będzie to konieczne. Na przykład możemy zachować istniejące funkcjonalności zasilane przez REST i włączyć GraphQL wyłącznie dla wszystkich nowych funkcjonalności.

Jeśli chcesz przeprowadzić pełną migrację do GraphQL, nie musi się to odbywać naraz. Istniejące funkcjonalności mogą być stopniowo, lecz systematycznie migrowane do GraphQL, aż pewnego dnia GraphQL stanie się jedynym API w aplikacji.

Dlatego, choć możesz stworzyć kompletny schemat GraphQL już pierwszego dnia, nie musisz tego robić: w dowolnym momencie tylko te encje wymagane przez funkcjonalność muszą być obecne w schemacie (za pośrednictwem ich typów, pól i interfejsów). Możesz je mapować stopniowo, w sposób progresywny.

Nie pozwól, by interfejs dźwigał ciężar implementacji

Serwer GraphQL zaimplementuje logikę dostępu do danych aplikacji. Zrobi to, wywołując funkcjonalności WordPressa, na przykład wywołując get_posts w celu pobrania danych postów. Na tej warstwie znajduje się kod PHP obsługujący resolwery.

Schemat GraphQL jest jednak interfejsem: deklaruje kontrakty dostępu do danych w API. Nie zajmuje się szczegółami implementacji: nic nie wie o WordPressie, o funkcji get_posts, tabeli bazy danych wp_posts ani queries SQL.

W związku z tym powinniśmy w jak największym stopniu unikać wycieku informacji między warstwami.

Jest to ważne, ponieważ model danych często będzie skażony przez jego implementację. WordPress dostarcza jasnego przykładu tego z CPT "attachment", służącym do reprezentowania plików multimedialnych, takich jak obrazy.

Ponieważ jest to Custom Post Type, obraz jest traktowany jak post. Możemy być więc kuszeni, by reprezentować pliki multimedialne za pomocą typu Post, który zawiera następujące pola:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
}

Ale to może nie być odpowiednie dla aplikacji. Znaczenie pola "content" jest jasne dla posta, ale nie dla obrazu. Najprawdopodobniej nie powinno się tam znajdować.

Obraz został zamodelowany jako CPT w WordPressie z wygody, aby mógł ponownie wykorzystać istniejącą logikę i być przechowywany w istniejącej tabeli wp_posts.

Jednak wygodne nie oznacza właściwe i może ostatecznie prowadzić do długu technicznego (tj. wadliwego kodu, który nie może być poprawiony bez wprowadzenia zmiany łamiącej wsteczną kompatybilność, więc jest utrzymywany w aplikacji dłużej niż powinien).

W miarę możliwości nie chcemy utrzymywać długu technicznego w naszej aplikacji. Kiedy tylko nadarzy się okazja, powinniśmy go naprawiać. Mapowanie modelu danych na schemat GraphQL daje taką możliwość, pozwalając nam skorygować problem na warstwie interfejsu danych.

(Dług techniczny nadal będzie się utrzymywał na poziomie aplikacji, więc nie rozwiązujemy problemu w całości, ale łagodzimy go w miarę naszych możliwości.)

Wprowadźmy tę ideę w życie. Zamiast mieć typ Post reprezentujący pliki multimedialne, bardziej sensowne jest posiadanie typu Media, zawierającego tylko te właściwości, które rzeczywiście mają sens dla encji obrazu:

type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Pod spodem, na poziomie implementacji, field resolver nadal będzie wykonywał funkcję get_posts, aby rozwiązywać wpisy typu Media, ale to nie jest problemem schematu GraphQL.

Oddziel schemat GraphQL od diagramu bazy danych

WordPress jest zaimplementowany w oparciu o ten diagram encja-relacja bazy danych:

Diagram encja-relacja bazy danych w WordPress

Musimy oprzeć schemat GraphQL na diagramie bazy danych, ale nie powinniśmy próbować tworzyć repliki 1 do 1. Dzieje się tak dlatego, że zarówno schemat GraphQL, jak i diagram bazy danych są zbudowane z określonymi warunkami wstępnymi lub ograniczeniami, które nie będą miały zastosowania do drugiego z nich.

Poprzednia sekcja ilustruje przykład, w którym tabela wp_posts przechowuje dane dla CPT obrazu, ale w GraphQL będą dwa odrębne typy: Post i Media.

Rozważmy inny przykład: kategorie. W WordPressie post może mieć jedną kategorię (lub więcej), a każdy CPT może także tworzyć własne kategorie. Na przykład CPT o nazwie "event" będzie miał "event_category".

Zarówno kategorie postów, jak i kategorie wydarzeń są przechowywane w tabeli wp_terms. Ułatwia to WordPressowi pobieranie wierszy z jednego lub drugiego typu kategorii podczas wykonywania query SQL.

Dlatego możemy być kuszeni, by mapować kategorie za pomocą typu Category, do którego odwołują się zarówno posty, jak i wydarzenia:

type Category {
  id: ID!
  name: String!
}
 
type Post {
  categories: [Category]!
}
 
type Event {
  categories: [Category]!
}

Jednak post zawsze będzie zawierał kategorie postów, a wydarzenie zawsze będzie zawierało kategorie wydarzeń. Dane tych dwóch typów kategorii mogą być przechowywane w tej samej tabeli bazy danych, ale nie będą mieszane na poziomie aplikacji. Kategoria posta i kategoria wydarzenia to dwie odrębne encje.

GraphQL ma statyczny system typów. Aby w pełni wykorzystać możliwości GraphQL, różne encje na poziomie aplikacji muszą być modelowane przy użyciu różnych typów w schemacie GraphQL.

W tym przypadku, mapując kategorie na schemat GraphQL, powinniśmy stworzyć inny typ dla każdej z nich: PostCategory i EventCategory. Wtedy typ Post będzie odwoływać się tylko do PostCategory, a typ Event tylko do EventCategory:

type PostCategory {
  id: ID!
  name: String!
}
 
type Post {
  categories: [PostCategory]!
}
 
type EventCategory {
  id: ID!
  name: String!
}
 
type Event {
  categories: [EventCategory]!
}

Jeśli nadal chcemy mieć w schemacie encję obejmującą wszystkie kategorie, można to osiągnąć za pomocą interfejsu Category:

interface Category {
  name: String!
}
 
type PostCategory implements Category {
  id: ID!
  name: String!
}
 
type EventCategory implements Category {
  id: ID!
  name: String!
}

W ten sposób użytkownicy uzyskujący dostęp do API będą mieli jasne zrozumienie, jakie dane zostaną pobrane, niezależnie od tego, jak są zmapowane w diagramie bazy danych i jak są przechowywane w bazie danych.

Gdy mamy już ostateczny schemat GraphQL, możemy dostrzec, że jego kształt będzie nieco przypominał diagram bazy danych WordPressa, ale będzie wyraźnie od niego różny:

Schemat GraphQL

Dostosuj nazewnictwo pól zgodnie z typowaniem statycznym

Pola powinny, w miarę możliwości, zachowywać to samo nazewnictwo, jakie mają w aplikacji.

Na przykład możemy stworzyć post za pomocą funkcji wp_insert_post, a post ma właściwości "title" i "content". Te nazwy są również dobre dla schematu GraphQL (choć mogą wymagać niewielkich modyfikacji), więc powinniśmy je zachować:

type MutationRoot {
  insertPost(title: String, content: String): Post
}
 
type Post {
  id: ID!
  title: String
  content: String
}

Ale tak nie jest zawsze. Jak widzieliśmy wcześniej, custom posty muszą być oddzielone w swoje własne encje. Dlatego, podczas gdy funkcja get_posts pobiera listę dowolnego CPT, równoważne pole posts w głównym typie schematu będzie pobierać tylko encje typu Post, ale nie Page (które jest również CPT):

type QueryRoot {
  posts: [Post]!
}

Jak więc uzyskać listę wszystkich postów i stron? Za pomocą kolejnego pola, customPosts, które pobiera encje dowolnego CPT zmapowanego pod typem union CustomPostUnion:

union CustomPostUnion = Post | Page
 
type QueryRoot {
  customPosts: [CustomPostUnion]!
}

Ważna lekcja jest następująca: nazewnictwo, które wybieramy dla schematu GraphQL, musi być dostosowane do typu pobieranej encji. A ze względu na silne typowanie GraphQL, taki typ może być różny na poziomach aplikacji i API.

W tym przypadku, podczas gdy w WordPressie "post" może oznaczać dowolny "custom post type", w GraphQL "post" jest koniecznie typem Post. Jeśli pole pobiera custom posty, to pole w schemacie GraphQL musi nazywać się customPosts, a nie posts. Podobnie, jeśli dane wejściowe przyjmują ID dla custom posta, musi być ono nazwane customPostID, a nie postID.

Mapowanie dla pola customPosts

Ta lekcja ma zastosowanie do komentarzy na przykład. Komentarz może być dodany do dowolnego CPT, nie tylko do postów. Dlatego typ Comment musi to jasno określać, zawierając pole customPost (a nie post):

type Comment {
  id: ID!
  customPost: CustomPostUnion!
}

Konwertuj predefiniowane wartości ciągów znaków na enums, używając wielkich liter, gdy to możliwe

Typy wyliczeniowe są, zgodnie z konwencją, definiowane wielkimi literami. Na przykład dokumentacja graphql.org podaje ten przykład:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Ilekroć musimy stworzyć nowy typ enum, powinniśmy używać wielkich liter dla jego zdefiniowanych stałych. Jednak podczas migracji modelu danych z aplikacji możemy napotkać pewne zestawy predefiniowanych wartości, które możemy zmapować za pomocą enum, ale których wartości są ciągami znaków pisanymi małymi literami.

Dla przykładu, posty w WordPressie mają właściwość "status", zawierającą jedną z następujących wartości:

  • "publish"
  • "pending"
  • "draft"
  • "trash"

Mapując tę właściwość w schemacie, pole Post.status mogłoby zwracać String, w ten sposób:

type Post {
  status: String!
}

Jednak ponieważ status będzie koniecznie jedną z tych predefiniowanych wartości i żadną inną, wolelibyśmy zmapować go jako enum:

enum Status {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}
 
type Post {
  status: Status!
}

Teraz możemy mieć problem: enum PUBLISH zostanie przekonwertowany na wartość ciągu znaków "PUBLISH" w aplikacji, a nie "publish".

Używając wartości pisanej wielkimi literami zamiast oczekiwanej małymi, logika w aplikacji może zostać zaburzona. Rzeczywiście, wykonanie następującego kodu w WordPressie nie działa:

// To pobierze wszystkie posty, nie tylko opublikowane
$published_posts = get_posts([
  "post_status" => "PUBLISH",
]);

W tym przypadku możemy rozważyć wymianę konwencji na wygodę, nadal używając enum do mapowania stałych, ale pisanych małymi literami:

enum Status {
  publish
  draft
  pending
  trash
}

Innymi słowy, możemy znaleźć złoty środek między byciem rygorystycznym a byciem praktycznym. Powinniśmy stosować najlepsze praktyki podczas budowania schematu GraphQL, ale pozwolić sobie na odstępstwa od nich, gdy ma to sens.