GraphQL
Воронцов Максим
students/42 |
students/42/friends |
? |
/students/42/groups/ /students/42/departments/
/students/42/friends/groups/ /students/42/friends/departments/
/students/42/friends_with_groups_and_departments/
Сложности в проектировании при росте зависимостей между сущностями
Большое число запросов
Лишние данные в ответе от сервера
Всегда необходимо помнить об обратной совместимости
Нет удобных инструментов для разработки
Строгая типизация
Получаем только то, что действительно необходимо
Возможность получить все необходимые данные за один запрос
Отсутствие проблем с обратной совместимостью и расширением
Удобные инструменты для разработки
Реализации на всех популярных языках
query {
student(id: 42) {
friends {
group {
name
}
department {
name
}
}
}
}
ID, Int, Float, String, Boolean
type Group { id: ID name: String } type Student { id: ID name: String age: Int group: Group } |
type Query { group(id: ID!): Group groups: [Group] student(id: ID!): Student students: [Student] } |
type Student { id: ID name: String age: Int group: Group } type Lecturer { id: ID name: String degree: String } union Person = Student | Lecturer
|
|
|
|
|
|
query StudentsQuery {
first: student(id: 1) {
id
name
age
}
second: student(id: 2) {
id
name
age
}
}
fragment StudentFields on Student {
id
name
age
group {
id
name
}
}
query {
first: student(id: 1) {
...StudentFields
}
second: student(id: 2) {
...StudentFields
}
}
query {
persons {
__typename # Meta field
... on Student {
name
group {
name
}
}
... on Lecturer {
name
degree
}
}
}
query StudentQuery($id: ID!) { student(id: $id) { id name age } }
// Variables
{
"id": "1"
}
query StudentQuery($id: ID!, $withGroup: Boolean!) { student(id: $id) { id name age group @include(if: $withGroup) { id name } } }
query StudentQuery($id: ID!, $withoutAge: Boolean!) { student(id: $id) { id name age @skip(if: $withoutAge) group { id name } } }
mutation CreateStudent($name: String!, $age: Int!, $groupId: ID!) { createStudent(name: $name, age: $age, groupId: $groupId) { id name age group { id name } } }
$ npm install --save graphql
const { graphql, buildSchema } = require('graphql');
const students = [
{ id: '1', name: 'Maxim' },
{ id: '2', name: 'Ivan' }
];
const schema = buildSchema(`
type Student {
id: ID
name: String
}
type Query {
student(id: ID!): Student
}
`);
const root = {
student: args => {
return students.find(student => student.id === args.id);
}
};
const query = `
query {
student(id: 1) {
name
}
}
`;
graphql(schema, query, root).then(response => {
// { data: { student: { name: "Максим" } } }
});
const schema = buildSchema(`
type Student {
id: ID
name: String
}
type Mutation {
createStudent(name: String!): Student
}
`);
const root = {
createStudent: args => {
const newStudent = {
id: newId,
name: args.name
};
students.push(newStudent);
return newStudent;
}
};
const mutation = `
mutation {
createStudent(name: "Peter") {
id
name
}
}
`;
graphql(schema, mutation, root).then(response => {
console.log(response);
});
// {"data": { "createStudent": { "id": 4, "name": "Peter" }}}
$ npm install --save express express-graphql
const express = require('express');
const graphqlHTTP = require('express-graphql');
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(4000, () => {
console.log('Listening on http://localhost:4000/graphql');
});
const {
GraphQLID,
GraphQLString,
GraphQLObjectType
} = require('graphql');
const GroupType = new GraphQLObjectType({
name: 'Group',
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString }
}
});
const StudentType = new GraphQLObjectType({
name: 'Student',
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
age: { type: GraphQLInt },
group: {
type: GroupType,
resolve: parentValue => {
return groups.find(group => {
return group.id === parentValue.groupId
});
}
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
student: {
type: StudentType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve(parentValue, { id }) {
return students.find(student => {
return student.id === id;
});
}
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
// ...
students: {
type: new GraphQLList(StudentType),
resolve() {
return students;
}
}
}
});
module.exports = new GraphQLSchema({
query: Query
});
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createStudent: {
type: StudentType,
args: {
name: new GraphQLNonNull(GraphQLString),
age: new GraphQLNonNull(GraphQLInt),
groupId: new GraphQLNonNull(GraphQLID)
},
resolve(parentValue, args) {
return StudentModel.create(args);
}
}
}
});
module.export = new GraphQLSchema({
query: Query,
mutation: Mutation
});
query {
student(id: 1) {
name
wrongField
}
}
{
"errors": [
{
"message": "Cannot query field \"wrongField\" on type \"Student\".",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
{
"data": null,
"errors": [
{
"code": "UNQ",
"message": "Fields must be unique",
"details": {
"fields": [
"name"
]
}
}
]
}
Новая технология
Мало паттернов
Сложности при работе с SQL базами данных
Сложности при работе с реактивными данными
Название | Описание |
Lokka | Максимально простой в использовании. Базовая поддержка Query и Mutations. Простейшее кэширование |
Apollo | Более гибкий. Хороший баланс между функциональностью и сложностью использования |
Relay | Наиболее функциональный, из-за чего наиболее сложный в использовании. Много внимания уделено производительности (особенно на мобильных). |
$ npm install --save apollo-client react-apollo graphql-tag
import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-client';
import { ApolloProvider } from 'react-apollo';
const client = new ApolloClient({});
const Root = () => (
<ApolloProvider client={client}>
<div>Root Component</div>
</ApolloProvider>
);
ReactDOM.render(
<Root />,
document.getElementById('root')
);
import React from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const query = gql`
query Student($id: ID!) {
student(id: $id) {
name
group {
name
}
}
}
`;
import React from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const Student = ({ data }) => (
<div>
<div>Имя: {data.student.name}</div>
<div>Группа: {data.student.group.name}</div>
</div>
);
export default graphql(query, {
options: ({ id }) => ({ variables: { id } })
})(Student);
Uncaught TypeError: Cannot read property 'name' of undefined
import React from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const Student = ({ data }) => {
if (data.loading) {
return <div>Loading...</div>;
}
// ...
};
export default graphql(query, {
options: ({ id }) => ({ variables: { id } })
})(Student);
const Student = ({ data }) => {
// ...
return (
<div>
<div>Имя: {data.student.name}</div>
<div>Группа: {data.student.group.name}</div>
<button>Удалить</button>
</div>
);
}
const mutation = gql`
mutation RemoveStudent($id: ID!) {
removeStudent(id: $id)
}
`;
export default graphql(mutation)(graphql(query, {
options: ({ id }) => ({ variables: { id } })
})(Student));
import { compose } from 'react-apollo';
// compose(f, g, h)(x) === f(g(h(x)))
export default compose(
graphql(query, {
options: ({ id }) => ({ variables: { id } })
}),
graphql(mutation)
)(Student);
class Student extends React.Component {
handleRemoveClick() {
this.props.mutate({
variables: { id: this.props.id }
})
.then(() => { ... })
.catch(response => {
console.log(response.graphQLErrors);
});
}
// ...
}
class Student extends React.Component {
render() {
// ...
return (
<div>
<div>Имя: {data.student.name}</div>
<div>Группа: {data.student.group.name}</div>
<button onClick={this.handleRemoveClick}>
Удалить
</button>
</div>
);
}
}
|
|
class Student extends React.Component {
handleRemoveClick() {
this.props.mutate({
variables: { id: this.props.id },
refetchQueries: [
'Students'
]
})
.then(() => { ... })
.catch(() => { ... });
}
// ...
}
Простота использования
Гибкая настройка кэширования
Fragments
Optimistic Updates
Polling
GraphQL
GraphQL Clients
Examples