GraphQL是这两年兴起的一种查询语言,国内一些比较潮的公司正在使用,它解决了Rest接口方式的一些问题,同时也带来了一些新的问题。对于咱们底层程序员来讲,学就对了,万一用上了呢。javascript
graphql在各类语言,各类框架都有对应的实现,能够查看官网根据状况选择适合本身的实现,概念上都是一致的。本文更着重于实际代码,理论部分请结合官网教程观看。css
本文使用nodejs做为开发语言,使用express做为服务器,展现graphql的简单搭建过程,并逐步添加mysql,typescript,type-graphql,typeorm的支持。这个过程是渐进的,若是你不喜欢(学不动)某个部分,跳过就好。注意本文携带大量私货,未必是最佳实践,若是有错误,请评论指出,共同窗习,谢谢html
首先咱们先快速实现一个graphql的服务器前端
mkdir graphqldemo;
cd graphqldemo;
npm init -yes;
npm i express apollo-server-express;
复制代码
而后建立一个index.jsjava
const express = require("express");
const { ApolloServer } = require("apollo-server-express");
const PORT = 4000;
const app = express();
const box = {
width: 100,
height: 200,
weight: "100g",
color: "white"
}
const typeDefs = [` """ 一个盒子模型 """ type Box{ """ 这是盒子的宽度 """ width:Int, height:Int, color:String } type Query { getBox: Box } type Mutation{ setWidth(width:Int):Box } schema { query: Query, mutation: Mutation }`]
const resolvers = {
Query: {
getBox(_) {
return box;
}
},
Mutation: {
setWidth(_, { width }) {
box.width = width;
return box
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers
});
server.applyMiddleware({ app });
app.listen(PORT, () =>
console.log(
`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
)
);
复制代码
而后运行node
node index.js
复制代码
等出现成功提示后就能够在浏览器打开 http://localhost:4000/graphql,就能够看到graphql提供的playgroundmysql
刚刚咱们使用express和Apollo Server实现了一个最简单的graphql服务器(Apollo Server是graphql规范的一个实现)。react
const server = new ApolloServer({
typeDefs,
resolvers
});
复制代码
在new一个ApolloServer的时候,传入了两个参数,一个typeDefs和一个resolvers。typeDefs是一个字符串或者字符串数组,里面的内容是咱们定义的schema,而resolvers是schema的实现,也就是typeDefs里的Query和Mutation,注意全部的schema都要实现以后程序才能启动。ios
也能够只传入一个schema参数来new ApolloServer,使用buildSchema方法能够将typeDefs和resolvers生成schema(schema这个概念在graphql中处处出现,不要搞混了)。程序员
resolver的返回值须要符合定义的类型,不然会报错。在ApolloServer中,也能够返回对应类型的Promise。
server.applyMiddleware({ app });
复制代码
这一行将Apollo做为Express的一个中间件
const box = {
width: 100,
height: 200,
weight: "100g",
color: "white"
}
复制代码
声明一个盒子,做为数据源。graphql并不在乎数据是从哪里来的,能够从普通变量,数据库,redis,甚至http请求中获取,只要这个数据的结构能符合定义便可。如今咱们向服务器请求一下这个box
graphql一共有三种操做类型,query、mutation 或 subscription,这里演示一下query、mutation
query是graphql中的查询操做,在playground左侧输入
query {
getBox {
width
height
color
}
}
复制代码
点击按钮,能够在右边得到返回值
{
"data": {
"getBox": {
"width": 100,
"height": 200,
"color": "white"
}
}
}
复制代码
咱们能够随意减小getBox里的字段(至少有一个),好比只要width
query {
getBox {
width
}
}
复制代码
能够看到返回值里只有width属性了。
{
"data": {
"getBox": {
"width": 100
}
}
}
复制代码
graphql在这里解决了传统接口模式中一个问题,就是后端在向前端传输数据的过程当中,会传递不少无效字段,无效字段过多会影响传输效率,前端能够主动获取本身所需的字段。
另外一方面,后端的DAO层的一些字段从安全角度也是不该该传递给前端的,在上文的这个例子里,box的weight属性能够理解为一个前端不该可见的字段,由于在graphql中没有被定义,因此被自动过滤了,前端没法查询到。传统后端解决这个问题的方案是在DAO层之上引入一个DTO层。
mutation表明对数据源会产生反作用的操做,在playground中输入
mutation {
setWidth(width: 108) {
width
height
color
}
}
复制代码
获得结果
{
"data": {
"setWidth": {
"width": 108,
"height": 200,
"color": "white"
}
}
}
复制代码
能够看到box的width已经被更新到108了。注意,query和mutation均可以发起多个,服务器内部会顺序执行,可是query和mutation不能同时使用,下面是一个多个mutation的例子,query同理
mutation {
m1:setWidth(width: 108) {
width
}
m2:setWidth(width: 99) {
width
}
}
复制代码
返回值
{
"data": {
"m1": {
"width": 108
},
"m2": {
"width": 99
}
}
}
复制代码
由于setWidth重复使用了两次,重名了,因此咱们使用m一、m2做为别名(Aliases),语法如上,很是简单。
刚刚的mutation咱们直接在语句里写了参数,由于语句自己是字符串不利于组合,同时也不适合传递复杂的参数,因此咱们须要定义参数。点击playground左下的Query Variables,在这里能够声明参数,注意须要是标准json格式
{
"length": 128
}
复制代码
同时将语句改成
mutation($length: Int) {
setWidth(width: $length) {
width
}
}
复制代码
在length前加一个$就能在语句中使用了,能够查看一下浏览器控制台的请求有什么变化
咱们再看一点复杂的模型,如今给盒子里装点随机的小球,将数据源修改成以下形式
class Ball {
constructor() {
this.size = ((Math.random() * 10) | 0) + 5;
this.color = ["black", "red", "white", "blue"][(Math.random() * 4) | 0];
}
}
const box = {
width: 100,
height: 200,
weight: "100g",
color: "white",
balls: new Array(10).fill().map(n => new Ball())
}
复制代码
而后在typeDefs中增长一个类型,而且修改box的类型
type Box{
width:Int,
height:Int,
color:String,
balls:[Ball]
}
type Ball{
size:Int,
color:String
}
复制代码
重启服务,进行一次查询
query {
getBox {
width
balls {
size
color
}
}
}
复制代码
结果
{
"data": {
"getBox": {
"width": 100,
"balls": [
{
"size": 5,
"color": "black"
},
//...
]
}
}
}
复制代码
彷佛没有报错,不过这种状况并不符合graphql设计的本意。graphql的数据一层应该只携带本层的信息,想象一下这个需求,我须要box和box里全部color为red的球。正确作法以下,先修改box让他有参数
type Box{
width:Int,
height:Int,
color:String,
balls(color:String):[Ball]
}
复制代码
而后在resolvers里添加一个Box,注意resolver的第一个参数parent指向的是他的父元素也就是box,这一点很重要,若是有复数的盒子,须要这个参数判断返回哪一个盒子里的球
const resolvers = {
Query: {
getBox(_) {
return box
},
},
Mutation: {
setWidth(_, { width }) {
box.width = width;
return box
}
},
Box: {
balls(parent, { color }) {
return color ? box.balls.filter(ball => ball.color === color) : box.balls
}
}
};
复制代码
如今可使用查询查出全部的颜色为red的球
query {
getBox {
width
balls (color:"red"){
size
color
}
}
}
复制代码
若是没有参数,就是所有的球。graphql这么设计的好处是,能够在数据库查询中,少写不少的join,坏处是更多的查询次数
在使用http请求graphql服务器时的载体仍然是json,因此即便不使用任何特殊的库也能够与graphql服务器通讯
先用比较经典的axios来试一下,建立一个html文件
<script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
<script> const query = `query($color:String) { getBox { width balls (color:$color){ size color } } }`; const variables = { color: "red" }; axios .post("http://localhost:4000/graphql", { query, variables }) .then(res => { console.log("res: ", res); }); </script>
复制代码
另外GET也是彻底合法的
axios.get("http://localhost:4000/graphql", {
params: { query, variables }
})
复制代码
或者直接访问
http://localhost:4000/graphql?query=query($color:String){getBox{width,balls(color:$color){size,color}}}&variables={"color":"red"}
复制代码
相比传统方式,graphql的特色就是返回值可预测,并且由于地址、请求方式和参数名固定,封装起来更简单。
如今看一下专业的客户端是这么作的,既然服务端使用了apollo-server,那客户端就看一下apollo-client怎么作的apollo-client官网。由于提供了错误处理,数据缓存,错误处理等等,配置项稍显复杂,官方提供了一个apollo-boost的东西简化了配置。咱们能够本身对照官方实现一个简化版,深刻学习一下。
私货警告
如下内容在react16.8+的hooks API和typescript下实现,模仿官方包的api设计,去掉了缓存等功能。缓存能够说是apollo提供的核心功能了,但为了缓存增长了巨量的代码,并不适合学习。 首先咱们建立一个新的react工程
create-react-app graphql-client --typescript
复制代码
接下来咱们要参考官方包实现如下几个使用频率最高的模块(超级精简版):ApolloClient、ApolloProvider、useQuery、Query
入参包括uri,fetchOptions等,实际就是一个http请求库,这部分省点事直接用axios替代吧。注意官方实例,使用了从graphql-tag导出的gql方法处理graphql字符串,包括server端也有这个方法,它的做用是将字符串转换成ast,方便检查编写schema文件时出现的错误,本文中都省略掉了,都直接使用字符串。
import Axios, { AxiosInstance } from "axios";
type config = {
uri: string;
};
class Client {
constructor({ uri }: config) {
this.uri = uri;
this.axios = Axios.create();
}
private uri: string;
private axios: AxiosInstance;
query({ query, variables }: { query: string; variables: any }) {
return this.axios.post(this.uri, { query, variables });
}
}
复制代码
这个组件看得出是将client做为一个context提供给下文,使用createContext便可完成这个组件
interface ProviderProps {
client: Client;
}
const graphqlContext: React.Context<{
client: Client;
}> = React.createContext(null as any);
const GraphProvider: React.FC<ProviderProps> = ({ client, children }) => {
return (
<graphqlContext.Provider value={{ client }}>
{children}
</graphqlContext.Provider>
);
};
复制代码
由于graphql的入参固定,因此建立一个hook很容易。这里使用了一个泛型T去定义预期返回值的类型,官方包在这里还使用了第二个泛型来肯定variables参数的类型。
import { useState, useContext, useEffect, Dispatch } from "react";
const useQuery = <T = any>(query: string, variables?: any) => {
const { client } = useContext(graphqlContext);
const [data, setData]: [T, Dispatch<T>] = useState(null as any);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null as any);
useEffect(() => {
setLoading(true);
setError(null);
client
.query({ query, variables })
.then(res => {
setData(res.data.data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [query, variables, client]);
return { data, loading, error };
};
复制代码
到这里就可使用封装后的组件了 首先是App.tsx
const client = new Client({
uri: "http://localhost:4000/graphql"
});
const App: React.FC = () => {
return (
<Provider client={client}>
<Home></Home>
</Provider>
);
};
复制代码
而后是Home.tsx
interface ResData {
getBox: {
width: number;
balls: { size: number; color: string }[];
};
}
const query = `
query {
getBox {
width
balls (color:"red"){
size
color
}
}
}`;
const Home: FC = () => {
const { data, loading, error } = useQuery<ResData>(query);
if (loading) return <div>loading</div>;
if (error) return <div>{error}</div>;
return (
<div>
<h2>{data.getBox.width}</h2>
<ul>
{data.getBox.balls.map(n => (
<li>
size:{n.size} color:{n.color}
</li>
))}
</ul>
</div>
);
}
复制代码
由于获取的数据是可预测的,因此在写出查询语句的同时完成类型文件。若是到如今编码正确,你的react项目上已经能够看到效果了
该组件在建立hook以后就很是容易了,一笔带过
interface queryProps {
query: string;
variables?: any;
children: React.FC<{ data: any; loading: boolean; error: any }>;
}
const Query: React.FC<queryProps> = ({ query, variables, children }) => {
const { data, loading, error } = useQuery(query, variables);
return children({ data, loading, error });
};
复制代码
对前端来讲,应用graphql并非难点,只要能写出正确的查询语句,必然能获得正确的查询结果,难点多是融合进现有项目,使用typescript等工具,加速开发效率。改造的难度依然在后端,想到咱们以前的后端太过简陋,如今来优化一下吧,顺便补齐后端很是重要的身份认证等功能
后端一直以来咱们都在一个文件里写,随着模型变得复杂,代码开始臃肿了,同时在字符串里写schema也挺别扭,最好能写到单独的graphql/gql文件里去,这样还能有编辑器提供的格式化功能(我使用的是vscode中的Apollo GraphQL插件)。
我在这里的处理是将typeDefs拆分红对应的graphql文件,resolvers也进行文件拆分,而后使用文件扫描器自动依赖。
如今建立一个typeDefs文件夹,而后建立一个index.graphql文件,将原来的typeDefs字符串复制进去。
在同一个目录建立一个ball.gql文件,将index.graphql文件中Ball相关的定义剪贴进去。
接下来建立一个util.js,写一个代码扫描器,由于须要获取的就是字符串,因此直接用fs模块读取文件就好了
const fs = require("fs");
const path = require("path");
function requireAllGql(dir, parentArray) {
let arr = [];
let files = fs.readdirSync(dir);
for (let f of files) {
let p = path.join(dir, f);
let stat = fs.statSync(p);
if (stat.isDirectory()) {
requireAllGql(p, arr);
} else {
let extname = path.extname(p);
if (extname === ".gql" || extname === ".graphql") {
let text = fs.readFileSync(p).toString();
if (!parentArray) {
arr.push(text);
} else {
parentArray.push(text);
}
}
}
}
return arr;
}
module.exports = {
requireAllGql
};
复制代码
这样index.js里的typeRefs就能够改为这样
const { requireAllGql } = require('./utils.js')
const path = require("path")
const typeDefs = requireAllGql(path.resolve(__dirname, './typeDefs'))
复制代码
用一样的方式解决resolver,不过要先建立一个dataSource.js,将Ball和box移到这个文件里,而后建立一个resolvers文件夹,而后建立一个query.js文件,一个mutation.js文件,一个box文件(通常根据功能模块分文件,这里是个例子)。好比如今query.js就是这样
const { box } = require('../dataSource.js')
exports.default = {
Query: {
getBox(_) {
return box
}
}
}
复制代码
其他略过。再在utils.js建立一个resolver扫描器,每一个文件的默认导出都是一个普通对象,因此处理起来并不复杂
function requireAllResolvers(dir, parentArray) {
let arr = [];
let files = fs.readdirSync(dir);
for (let f of files) {
let p = path.join(dir, f);
let stat = fs.statSync(p);
if (stat.isDirectory()) {
requireAllResolvers(p, arr);
} else {
let extname = path.extname(p);
if (extname === ".js" || extname === ".ts") {
let resolver = require(p).default;
if (!parentArray) {
arr.push(resolver);
} else {
parentArray.push(resolver);
}
}
}
}
return arr;
}
复制代码
同理能够搞定index文件里的resolvers
const resolvers = requireAllResolvers(path.resolve(__dirname, './resolvers'))
复制代码
Apollo会帮咱们把数组内的内容进行merge,因此咱们只要保证每一个文件里的内容符合格式便可。若是一切顺利的话,项目仍然能够正确运行,并无什么改变,可是却能够在这基础上横向扩展了。
对于一个web服务来讲,数据应该储存在专门的数据库中,好比mysql、redis等,此处以经常使用的mysql为例,看看graphql在跟数据库结合时有什么不一样。还以以前的盒子小球为例,建立一个数据库。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_ball`;
CREATE TABLE `t_ball` (
`id` int(10) NOT NULL,
`size` int(255) DEFAULT NULL,
`color` varchar(255) DEFAULT NULL,
`boxId` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
BEGIN;
INSERT INTO `t_ball` VALUES (1, 5, 'red', 1);
INSERT INTO `t_ball` VALUES (2, 6, 'blue', 1);
INSERT INTO `t_ball` VALUES (3, 7, 'white', 2);
INSERT INTO `t_ball` VALUES (4, 8, 'black', 2);
COMMIT;
DROP TABLE IF EXISTS `t_box`;
CREATE TABLE `t_box` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`width` int(255) DEFAULT NULL,
`height` int(255) DEFAULT NULL,
`color` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
BEGIN;
INSERT INTO `t_box` VALUES (1, 100, 100, 'red');
INSERT INTO `t_box` VALUES (2, 200, 200, 'blue');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
复制代码
回到express项目,由于模型有点细微的变化,加入了主键id,因此gql文件中的schema须要加入id,下面列出须要修改的schema
type Box {
id: Int
width: Int
height: Int
color: String
balls(color: String): [Ball]
}
type Ball {
id: Int
size: Int
color: String
}
type Query {
getBox: [Box]
}
type Mutation {
setWidth(width: Int, id: Int): Box
}
复制代码
在项目里增长mysql的包
yarn add mysql
复制代码
创建链接池,将查询简单封装,就不引入DAO层了,毕竟一共没几句sql
const mysql = require('mysql')
const pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'password',
database: 'graphqldemo',
port: 3306
})
const query = (sql, params) => {
return new Promise((res, rej) => {
pool.getConnection(function (err, connection) {
connection.query(sql, params, function (err, result) {
if (err) {
rej(err);
} else {
res(result);
}
connection.release();
});
});
})
}
复制代码
在resolver中引入sql前须要知道resolver的四个参数。第一个参数parent,是当前元素的父元素,顶级的schema的父元素称为root,大部分教程中用_代替。第二个参数是params,也就是查询参数。第三个参数是config,其中有一个参数dataSources咱们过会儿须要用到。第四个参数是context,它的入参是express的Request和Response,能够用来传入身份信息,进行鉴权等操做。
咱们把封装好的query函数放进这个dataSources。在index.js中修改
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
query
})
});
复制代码
接着就能够修改resolvers,先实现第一个getBox,由于如今不止一个盒子,因此返回的是一个数组,schema已经进行了修改
Query: {
getBox(_, __, { dataSources: { query } }) {
return query('select * from t_box')
}
}
复制代码
query返回的是一个Promise,Apollo是支持这种写法的
而后完成Box的balls,咱们须要从parent中拿到父元素的id
Box: {
balls(parent, { color }, { dataSources: { query } }) {
return query('select * from t_ball where box_id=? and color=?', [parent.id, color])
}
}
复制代码
最后还有一个Mutation须要修改,schema中的定义返回的是被修改后的box,因此须要两条sql来完成这个部分
Mutation: {
async setWidth(_, { width, id }, { dataSources: { query } }) {
await query('update t_box set width=? where id=?', [width, id])
return query('select * from t_box where id=?', [id]).then(res => res[0])
}
}
复制代码
到这里,基本已经完成了一个graphql项目的基础,在此基础上横向扩展就可以完成一个简单的项目。另外,正式的项目中,仍是须要DAO层来管理数据,不然重构会教你作人的。
迎合潮流,咱们须要typescript的加持,不然怎么写都会被认为是玩具。可是咱们思考一个问题,typescript的类型和graphql都是对模型的描述,基本一致,只在语法上有一些区别,能不能通用呢。官方提供了相关的API实现这个需求,可是语法并不简洁,type-graphql也许是更好的选择。
先导入typescript和type-graphql,还有以前用到的包的描述文件,另外type-graphql扫描注解用到了reflect-metadata这个还未进入标准的特性,因此须要引入这个包
yarn add typescript type-graphql reflect-metadata @types/mysql @types/express
复制代码
typescript老规矩,先写tsconfig.json,大概有如下内容就差很少了
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["es6", "es7", "esnext", "esnext.asynciterable"],
"noImplicitAny": false,
"moduleResolution": "node",
"baseUrl": ".",
"esModuleInterop": true,
"inlineSourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"watch": true
}
}
复制代码
而后把全部require改为import,后缀名js改为ts就好了若是有报错就写个any
运行项目咱们使用ts-node,全局安装ts-node以后执行ts-node index.ts便可启动。正式项目咱们可使用pm2指定解释器或者将项目编译成js来运行。而后咱们将type-graphql引入项目。
很是遗憾的是引入type-graphql后代码结构发生大改,除了数据库相关的内容基本能够重写了,graphql文件也不须要了。先创建一个models文件夹,新增两个文件Box.ts和Ball.ts
import { ObjectType, Field } from "type-graphql";
@ObjectType()
export default class Ball {
@Field()
id: number;
@Field()
size: number;
@Field()
color: string;
boxId: number;
}
复制代码
import { ObjectType, Field, Int } from "type-graphql";
import Ball from "./Ball";
@ObjectType({ description: "这是盒子模型" })
export default class Box {
@Field(type => Int)
id: number;
@Field(type => Int, { nullable: true, description: "这是宽度" })
width: number;
@Field(type => Int)
height: number;
@Field()
color: string;
@Field(() => [Ball])
balls: Ball[];
}
复制代码
ObjectType注解表明这个类是graphql中的对象类型,而被Field注解的属性就是定义到graphql中的属性。Field的第一个参数是个函数用来表示类型,函数的入参没有意义,写type是为了语义化,返回值是类型(typescript的数字类型是number,但graphql的数字类型分为Int和Float,若是不指定为Int,typegraphql默认number为Float);第二个参数是配置项,nullable默认为false,这里能够改成true,description是注释
而后修改一下index.ts,引入graphql以前的东西基本都不要了,就保留数据库的query方法,另外query方法不放到dataSources中了,放到Context中
import "reflect-metadata";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import path from "path";
import query from "./db";
import { buildSchema } from "type-graphql";
const PORT = 4000;
const app = express();
app.use(express.static("public"));
buildSchema({
resolvers: [path.resolve(__dirname, "./resolvers/*.resolver.ts")]
}).then(schema => {
const server = new ApolloServer({
schema,
context: () => ({
query
})
});
server.applyMiddleware({ app });
app.listen(PORT, () =>
console.log(
`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
)
);
});
复制代码
buildSchema是异步的,因此ApolloServer启动要放在这以后,以前提到过ApolloServer须要提供typeDefs和resolvers两个参数或一个schema参数。看buildSchema这个函数的入参,就知道咱们还有resolvers没有改造,在resolvers文件夹下新建一个Box.resolver.ts,把以前的三个resolver改造一下
import {
Resolver,
Query,
Arg,
Mutation,
Ctx,
FieldResolver,
Root
} from "type-graphql";
import Box from "../models/Box";
import Ball from "../models/Ball";
@Resolver(Box)
export default class BoxResolver {
@Query(returns => [Box])
getBox(@Ctx() { query }) {
return query("select * from t_box");
}
@FieldResolver(returns => [Ball])
balls(@Root() box: Box, @Ctx() { query }, @Arg("color") color: string) {
return query("select * from t_ball where boxId=? and color=?", [
box.id,
color
]);
}
@Mutation(returns => Box)
async setWidth(
@Arg("width") width: number,
@Arg("id") id: number,
@Ctx() { query }
) {
await query("update t_box set width=? where id=?", [width, id]);
return query("select * from t_box where id=?", [id]).then(res => res[0]);
}
}
复制代码
简单说一下几个注解的意思。Query和Mutation表明是这两个基础类型下的resolver,参数是个函数,表示预期的返回类型;FieldResolver与类注解Resolver关联,表明Box这个对象类型下的字段balls的resolver;Arg注解的是参数,第一个参数是入参的参数名,它还有第二个参数,能够配置nullable;Root注解的参数是父元素,在balls方法中拿到了父盒子的id;Ctx注解的是Context,从中取到了apolloserver中context的query方法。
到这里,不须要手写graphql文件,依然完成了一个graphql服务器,并且typeDefs和resolvers结合到了一块儿,不用担忧漏写了。而且咱们的程序已经大变样,不深刻学习一番已经看不懂了,恭喜你离创建技术护城河更进一步。
type-graphql有个内置的权限管理,有兴趣的话能够看看Authorized注解
ORM是否要引入项目,主要仍是看项目需求。这里使用typeorm,它在写法上与typegraphql很是契合,由于他能够直接复用typegraphql中建立的model,是typegraphql在数据层上很是好的一个实现方式。typeorm自己内容不少,能够单独写一篇文章,本文只介绍与graphql有关的部分,先引入typeorm
yarn add typeorm
复制代码
而后在项目根目录建立一个ormconfig.json,输入数据库配置
{
"type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"username": "root",
"password": "password",
"database": "graphqldemo",
"synchronize": false,
"logging": false,
"entities": ["./models/*.ts"]
}
复制代码
其中type是数据库类型,typeorm支持MySQL、MariaDB、Postgres、SQLite、Oracle、MongoDB等多种数据库。synchronize若是为true,typeorm会根据模型自动建表,若是模型有修改还会对表结构进行修改(外键等缘由会致使修改失败项目没法启动,须要手动干预或者使用typeorm中的migrations)。logging为true会在控制台打印自动生成的sql语句。
先修改index.ts
import { createConnection } from "typeorm";
//····在app.listen以前添加
createConnection().then(() => {
app.listen(PORT, () =>
console.log(
`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
)
);
});
//····
复制代码
跟typegraphql的buildSchema同样,createConnection也是个异步的promise,因此为了防止一些意外状况,app.listen的操做要在这两个过程以后。若是已经创建了ormconfig.json文件,createConnection会自动读取其中的配置,不然须要将其做为参数填进去。
而后修改models,以后models能够同时在type-graphql中表示类型,也能够在typeorm中做为数据实体,一模两吃
import { ObjectType, Field, Int } from "type-graphql";
import Box from "./Box";
import {
Column,
ManyToOne,
Entity,
BaseEntity,
PrimaryGeneratedColumn
} from "typeorm";
@Entity({ name: "t_ball" })
@ObjectType()
export default class Ball extends BaseEntity {
@PrimaryGeneratedColumn()
@Field(type => Int)
id: number;
@Column()
@Field(type => Int)
size: number;
@Column({ type: "varchar", length: 255 })
@Field()
color: string;
@Column()
boxId: number;
@ManyToOne(type => Box)
box: Box;
}
复制代码
import { ObjectType, Field, Int } from "type-graphql";
import Ball from "./Ball";
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
BaseEntity
} from "typeorm";
@Entity({ name: "t_box" })
@ObjectType({ description: "这是盒子模型" })
export default class Box extends BaseEntity {
@PrimaryGeneratedColumn()
@Field(type => Int)
id: number;
@Column()
@Field(type => Int, { nullable: true, description: "这是宽度" })
width: number;
@Column()
@Field(type => Int)
height: number;
@Column({ type: "varchar", length: 255 })
@Field()
color: string;
@OneToMany(type => Ball, ball => ball.box)
@Field(() => [Ball])
balls: Ball[];
}
复制代码
首先将类继承自typeorm中的BaseEntity,而且类上增长了一个Entity注解,参数的配置项中加一个name能够指定表名;Column注解这个属性是数据库的一列,参数能够指定具体的类型和长度;PrimaryGeneratedColumn注解表明这是一个自增主键;OneToMany是个特殊的注解,用来描述实体直接的relations,一共包括OneToMany,ManyToOne,ManyToMany三种,具体用法说来话长,请自行摸索。这样就和以前创建的数据库对应了,感兴趣的可使用typeorm的自动建表功能看看有什么不一样。
如今咱们能够抛弃简陋封装的query方法,直接使用typeorm提供的数据获取方式
import {
Resolver,
Query,
Arg,
Mutation,
FieldResolver,
Root,
Int
} from "type-graphql";
import Box from "../models/Box";
import Ball from "../models/Ball";
@Resolver(Box)
export default class BoxResolver {
@Query(returns => [Box])
getBox() {
return Box.find();
}
@FieldResolver(returns => [Ball])
balls(@Root() box: Box, @Arg("color", { nullable: true }) color: string) {
return Ball.find({ boxId: box.id, color });
}
@Mutation(returns => Box)
async setWidth(
@Arg("width", type => Int) width: number,
@Arg("id", type => Int) id: number
) {
let box = await Box.findOne({ id });
box.width = width;
return box.save();
}
}
复制代码
整个程序实现很是的优雅,而且很难懂~。typeorm的内容很是多,若是对其余的orm有经验,上手仍是很快的。另外若是真的有typeorm写不出来的sql,该手写就手写吧~
这篇文章我很早就开始写了,可是摊子铺的太大,因此一直写不完主要是打怪物猎人冰原。文中选取的模型也很是简单,可是基本完成了一个graphql服务器的框架,固然也留下不少内容没有讲,好比很是重要的标量类型和输入类型。由于本人主业是前端,会一点java后端,因此若是文章中出现概念性错误,请评论指出,共同进步。