什麼是GraphQL
GraphQL 是一種面向 API 的查詢語言。在網際網路早期,需求都以 Web 為主,那時候資料和業務需求都不復雜,所以用 RestAPI 的方式完全可以滿足需求。但是隨著網際網路的發展,資料量增大,業務需求多變。還有各種客戶端需要介面適配,基於 RestAPI 的方式,顯得越來呆板,因此 GraphQL 便應運而生。它至少可以提供以下三個方面的優勢
- GraphQL 提供更方便的 API 查詢
不同的客戶端有時候需要返回的資料格式不同,之前使用 RestAPI 的方式,需要後端針對每一個客戶端提供單獨的介面。隨著業務需求的增加,維護的成本隨機呈指數級躍升。而使用 GraphQL 就比較開心了,只需要寫一套介面即可
- 解決前後端過於依賴
在開發的過程中,前端需要和後端反反覆覆確認各個欄位,防止到時候開發到一半,因為沒有對好欄位,要大塊大塊地改程式碼。現在有 GraphQL 就比較方便了,你需要什麼型別的欄位,就自己寫對應的查詢語法
- 節約網路和計算機記憶體資源
之前通過 RestAPI 的方式寫介面,有一個很大的問題在於,對於介面的定義,需要前期做大量的工作,針對介面做各種力度的拆分,但即使這樣,也沒辦法應對需求的風雲突變。有時候需要返回的僅僅是某個使用者的某一型別的資料,但不得不把該使用者的其他資訊也一併返回來,這既浪費了網路的資源,也消耗了計算機的效能。顯然不夠優雅,GraphQL 再一次證明了它的強大,它能夠提供 DIY 獲取所需要的資料,用多少,拿多少,可以說是相當環保了
PS : 更多 GraphQL 的介紹可以看文末的參考資料
介紹
這篇文章,我將用一個具體的 Todo List 例項,和大家一起,一步步手動搭建一個 GraphQL + MongoDB 的專案例項。我們將會在其中用到以下庫,開始之前需要提前安裝好:
- graphene_mongo
- graphene
- mongoengine
- flask_graphql
- Flask
在開始之前,我們來梳理一下我們的核心需求,我們要建立一個 Todo List 產品,我們核心的表只有兩個,一個是使用者表,儲存所有的使用者資訊,另外一個是任務表,儲存著所有使用者的任務資訊。任務表通過使用者 id 與對應的使用者關聯。表結構對應的是一對多的關係,核心的資料欄位如下:
task表
{
"_id" : ObjectId("5c353fd8771502a411872712"),
"_in_time" : "2019-01-09 08:26:53",
"_utime" : "2019-01-09 09:26:39",
"task" : "read",
"start_time" : "2019-01-09 08:26:53",
"end_time" : "2019-01-09 08:26:53",
"repeat" : [
"Wed"
],
"delete_flag" : NumberInt(0),
"user" : "1"
}
複製程式碼
user表
{
"_id" : "1",
"_in_time" : "2019-01-09 08:39:16",
"_utime" : "2019-01-09 09:23:25",
"nickname" : "xiao hong",
"sex" : "female",
"photo": "http://xh.jpg",
"delete_flag" : NumberInt(0)
}
複製程式碼
專案結構
一圖勝千言,為更清晰的瞭解專案的整體結構,我將專案的整體目錄結構列印下來,小夥伴們可以參照著目錄結構,看接下來的搭建步驟
----task_graphql\
|----api.py
|----database\
| |----__init__.py
| |----base.py
| |----model_task.py
| |----model_user.py
|----requirements.txt
|----schema.py
|----schema_task.py
|----schema_user.py
複製程式碼
- user_model 和 task_model 定義資料模組,直接資料庫 mongo 對接
- 上層定義的 schema 操作 shema_user 和 schema_task 對資料 model 進行增刪改查操作
- 最後 flask 搭建對外的 api 服務實現和外界的請求互動
建立資料模型
我們的資料模型結構非常簡單
- user_model 列出所有的使用者資訊
- task_model 列出所有的任務資訊,通過user欄位與使用者表關聯,表示該任務歸屬於哪一個使用者
base.py
from mongoengine import connect
connect("todo_list", host="127.0.0.1:27017")
複製程式碼
只需要通過呼叫 mongoengine 的 connect 指定對應的資料庫連結資訊和資料庫即可,後面直接引入至Flask模組會自動識別連線
model_user.py
import sys
sys.path.append("..")
from mongoengine import Document
from mongoengine import (StringField, IntField)
from datetime import datetime
class ModelUser(Document):
meta = {"collection": "user"}
id = StringField(primary_key=True)
_in_time = StringField(required=True, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
_utime = StringField(required=True, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
nickname = StringField(required=True)
sex = StringField(default="unknown", required=True)
delete_flag = IntField(default=0, required=True)
複製程式碼
所要定義的資料文件都通過 mongoengine 的 Document 繼承,它可以將對應欄位轉換成類屬性,方便後期對資料進行各種操作,meta 欄位指定對應的你需要連結的是哪張 mongo 表
model_task.py
import sys
sys.path.append("..")
from mongoengine import Document
from mongoengine import (StringField, ListField, IntField, ReferenceField)
from .model_user import ModelUser
from datetime import datetime
class ModelTask(Document):
meta = {"collection": "task"}
_in_time = StringField(default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), required=True)
_utime = StringField(default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), required=True)
task = StringField(default="", required=True)
start_time = StringField(default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), required=True)
end_time = StringField(default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), required=True)
repeat = ListField(StringField(required=True))
delete_flag = IntField(default=0, required=True)
user = ReferenceField(ModelUser, required=True)
複製程式碼
其中 required 表示這個欄位是必須欄位,default 可以設定該欄位的預設值。ReferenceField 可以指定和哪個模型相關聯,這裡指定的是 ModelUser 欄位,關聯預設為對應 mongo 表中的 _id 欄位
建立GraphQL查詢
現在我們已經將資料庫和模型部分的連線功能完成了,接下來建立 API 部分,在我們的 task_graphql 目錄下,有兩個檔案,schema_task.py 和 schema_user.py 分別將 model_task 和 model_user 類對映成 Graphene schema物件
schema_task.py
from database.model_task import ModelTask
from graphene_mongo import MongoengineObjectType
import graphene
import schema_user
from datetime import datetime
class TaskAttribute:
id = graphene.ID()
_in_time = graphene.String()
_utime = graphene.String()
task = graphene.String()
start_time = graphene.String()
end_time = graphene.String()
repeat = graphene.List(graphene.String)
delete_flag = graphene.Int()
user = graphene.String()
class Task(MongoengineObjectType):
class Meta:
model = ModelTask
class TaskNode(MongoengineObjectType):
class Meta:
model = ModelTask
interfaces = (graphene.relay.Node, )
複製程式碼
schema_user.py
from database.model_task import ModelTask
from graphene_mongo import MongoengineObjectType
import graphene
from datetime import datetime
class TaskAttribute:
id = graphene.ID()
_in_time = graphene.String()
_utime = graphene.String()
task = graphene.String()
start_time = graphene.String()
end_time = graphene.String()
repeat = graphene.List(graphene.String)
delete_flag = graphene.Int()
user = graphene.String()
class Task(MongoengineObjectType):
class Meta:
model = ModelTask
class TaskNode(MongoengineObjectType):
class Meta:
model = ModelTask
interfaces = (graphene.relay.Node, )
複製程式碼
現在我們建立一個 schema.py 的檔案,把剛才定義好的 schema_task.py 和 schema_user.py 檔案引入進來,定義兩個對外訪問的介面
- tasks: 查詢所有任務資訊,返回一個list
- users: 查詢所有使用者資訊,返回一個list
import schema_user
import schema_task
import graphene
from graphene_mongo.fields import MongoengineConnectionField
class Query(graphene.ObjectType):
node = graphene.relay.Node.Field()
tasks = MongoengineConnectionField(schema_task.TaskNode)
users = MongoengineConnectionField(schema_user.UserNode)
schema = graphene.Schema(query=Query)
複製程式碼
建立 Flask 應用
在主目錄下建立一個 api.py 檔案,將我們之前定義好的資料庫連線和 schema 引入進來,用 Flask 的 add_url_rule 方法將兩者關聯起來,為了方便訪問,我們通過引入 flask_graphql 的 GraphQLView 方法,將介面視覺化出來,方便除錯
from flask import Flask
from schema import schema
from flask_graphql import GraphQLView
from database.base import connect
from logger import AppLogger
log = AppLogger("task_graphql.log").get_logger()
app = Flask(__name__)
app.debug = True
app.add_url_rule("/graphql", view_func=GraphQLView.as_view("graphql", schema=schema, graphiql=True))
if __name__ == '__main__':
app.run()
複製程式碼
到這裡,我們就已經用 graphql 成功建立了一個可查詢的 Todo List 介面,接下來。我們可以用它來測試一下查詢介面吧。然後在開始查詢之前大家需要自己 mock 點資料到 mongo 裡面
我們訪問介面地址(http://127.0.0.1:5000/graphql),來查詢一下看看效果
新增 GraphQL 更新方法(mutation)
GraphQL 官方將更新建立操作,全部整合在 mutation 下,它包含了插入和更新資料功能,接下來我們就繼續上面的操作,將這部分功能完善
schema_task.py
from database.model_task import ModelTask
from graphene_mongo import MongoengineObjectType
import graphene
from datetime import datetime
class TaskAttribute:
id = graphene.ID()
_in_time = graphene.String()
_utime = graphene.String()
task = graphene.String()
start_time = graphene.String()
end_time = graphene.String()
repeat = graphene.List(graphene.String)
delete_flag = graphene.Int()
user = graphene.String()
class Task(MongoengineObjectType):
class Meta:
model = ModelTask
class TaskNode(MongoengineObjectType):
class Meta:
model = ModelTask
interfaces = (graphene.relay.Node, )
class CreateTaskInput(graphene.InputObjectType, TaskAttribute):
pass
class CreateTask(graphene.Mutation):
task = graphene.Field(lambda: TaskNode)
class Arguments:
input = CreateTaskInput(required=True)
def mutate(self, info, input):
task = ModelTask(**input)
task.save()
return CreateTask(task=task)
class UpdateTask(graphene.Mutation):
task = graphene.Field(lambda: TaskNode)
class Arguments:
input = CreateTaskInput(required=True)
def mutate(self, info, input):
id = input.pop("id")
task = ModelTask.objects.get(id=id)
task._utime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
task.update(**input)
task.save()
return UpdateTask(task=task)
複製程式碼
schema_user.py
from database.model_user import ModelUser
from graphene_mongo.types import MongoengineObjectType
import graphene
from datetime import datetime
class UserAttribute:
id = graphene.String()
_in_time = graphene.String()
_utime = graphene.String()
nickname = graphene.String()
photo = graphene.String()
sex = graphene.String()
delete_flag = graphene.Int()
class User(MongoengineObjectType):
class Meta:
model = ModelUser
class UserNode(MongoengineObjectType):
class Meta:
model = ModelUser
interfaces = (graphene.relay.Node, )
class CreateUserInput(graphene.InputObjectType, UserAttribute):
pass
class CreateUser(graphene.Mutation):
user = graphene.Field(lambda: UserNode)
class Arguments:
input = CreateUserInput(required=True)
def mutate(self, info, input):
user = ModelUser(**input)
user.save()
return CreateUser(user=user)
class UpdateUser(graphene.Mutation):
user = graphene.Field(lambda: UserNode)
class Arguments:
input = CreateUserInput(required=True)
def mutate(self, info, input):
id = input.pop("id")
user = ModelUser.objects.get(id=id)
user._utime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
user.update(**input)
user.save()
return UpdateUser(user=user)
複製程式碼
一看程式碼便知,我們將需要新增的資訊,通過input傳入進來,然後將對應的引數進行對映即可。我們再通過例項看下建立資料的效果
我們再來試下修改資料的操作,like this
bingo!!!
至此,我們通過 GraphQL 搭配 MongoDB 的操作就完美收關了。
完整專案請檢視 github: github.com/hacksman/ta…
以上都是自己一路踩過了很多坑之後總結出的方法,如有疏漏,還望指正