GraphQL

Лекция 19

Недостатки REST

  • многословен - чтобы получить необходимые данные часто требуется несколько запросов
  • слишком много информации - отдаёт все поля объекта
  • нет контрактов / типов

GraphQL

  • спецификация для построения API от Facebook
  • минимальная полезная нагрузка
  • человеко-читаемый формат
  • язык запросов
  • система типов
  • оффициальный сайт

GraphQL

  • C#
  • Go
  • Java
  • Ruby
  • Python
  • JavaScript
  • ...

GraphQL

  • задаём типы
  • определяем функции для работы с источниками данных
  • запускаем сервис обработки запросов

GraphQL - Типы


type Query {
  me: User
}

type User {
  id: ID
  name: String
}
    

GraphQL - Функции


function Query_me(request) {
  return request.auth.user;
}

function User_name(user) {
  return user.getName();
}
    

GraphQL - Запрос


{
  me {
    name
  }
}
    

GraphQL - Ответ


{
  "me": {
    "name": "Luke Skywalker"
  }
}
    

Запросы

Запросы - Частичное чтение


{
  hero {
    name
  }
}
    

"hero": {
  "name": "R2-D2"
}
    

Запросы - Частичное чтение


{
  hero {
    # Queries can have comments!
    friends {
      name
    }
  }
}
    

"hero": {
  "friends": [
    { "name": "Luke Skywalker" },
    ...
  ]
}
    

Запросы - Аргументы


{
  human(id: "1000") {
    name
    height
  }
}
    

"human": {
  "name": "Luke Skywalker",
  "height": 1.72
}
    

Запросы - Аргументы


{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
    

"human": {
  "name": "Luke Skywalker",
  "height": 5.6430448
}
    

Запросы - Алиасы


{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
    

{
  empireHero: ...,
  jediHero: ...
}
    

Запросы - Фрагменты


{
  empireHero: hero(episode: EMPIRE) {
    ...heroFields
  }
  jediHero: hero(episode: JEDI) {
    ...heroFields
  }
}

fragment heroFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
    

Запросы - Имя операции


# type - query, mutation, subscription
# name - для логирования и дебага

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
    

Запросы - Переменные


query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}
    

Запросы - Директивы


# @include(if: Boolean)
# @skip(if: Boolean)

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
    

Запросы - Изменения


# несколько операций в одной мутации
# будут выполнены последовательно

mutation CreateReviewForEpisode(
    $ep: Episode!,
    $review: ReviewInput!
) {
  createReview(episode: $ep, review: $review) {
    # возвращаемые значения
    stars
    commentary
  }
}
    

Запросы - Привидение типов


query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
    

Запросы - Мета-поле - имя типа


{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}
    

Запросы - Мета-поле - имя типа


{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}
    

Типы

Типы


type Character {
  # nullable string
  name: String

  # non-nullable array of Episode
  appearsIn: [Episode]!
}
    

Типы - Встроенные


Int
Float
String
Boolean
ID
    

Типы - Перечисления


enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}
    

Типы - Интерфейсы


interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}
    

Типы - Собственные


type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}
    

Типы - Объединения


union SearchResult = Human | Droid | Starship
    

Типы - Аргументы


type Starship {
  id: ID!
  name: String!

  # не просто поле - функция
  length(unit: LengthUnit = METER): Float
}
    

Резолверы

Резолверы


Query: {
  human(obj, args, context) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

# obj - предыдущий разрезолвленный объект
# args - аргументы запроса к полю
# context - в зависимости от реализации
# может хранить текущего юзера, подключение к БД, ...
    

Резолверы - Асинхронные


Query: {
  human(obj, args, context) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}
    

Резолверы - Тривиальные


Human: {
  name(obj, args, context) {
    return obj.name
  }
}
    

GraphQL - Недостатки

  • плохо интегрируется с ненормализованными данными
  • сложно отлавливать ошибки
  • могут быть проблемы с кэшированием (зависит от реализации)
  • повышается использование CPU на проверку типов

GraphQL - Пример


npm i express
npm i sequelize
npm i graphql
npm i express-graphql
    

GraphQL - Пример


// models/turtle.js
module.exports = (Sequelize, sequelize) => {
  return sequelize.define('turtles', {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },

    name: Sequelize.STRING,
    color: Sequelize.STRING,
  });
};
    

GraphQL - Пример


// models/pizza.js
module.exports = (Sequelize, sequelize) => {
  return sequelize.define('pizzas', {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },

    name: Sequelize.STRING,
    colories: Sequelize.DOUBLE,
  });
};
    

GraphQL - Пример


// models/weapon.js
module.exports = (Sequelize, sequelize) => {
  return sequelize.define('weapons', {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },

    name: Sequelize.STRING,
    dps: Sequelize.INTEGER,
  });
};
    

GraphQL - Пример


// models/index.js
module.exports = (Sequelize, config) => {
  const options = {
    host: config.db.host,
    dialect: 'mysql',
    logging: false,
    define: {
      timestamps: true,
    }
  };

  const sequelize = new Sequelize(...);

  ...
};
    

GraphQL - Пример


// models/index.js
module.exports = (Sequelize, config) => {
  ...

  const Turtle = require('../models/turtle')(Sequelize, sequelize);
  const Pizza = require('../models/pizza')(Sequelize, sequelize);
  const Weapon = require('../models/weapon')(Sequelize, sequelize);

  Weapon.hasOne(Turtle);

  Pizza.hasOne(Turtle, {as: 'favoritePizza'});
  Pizza.hasOne(Turtle, {as: 'secondFavoritePizza'});

  ...
};
    

GraphQL - Пример


// models/index.js
module.exports = (Sequelize, config) => {
  ...

  return {
    turtles: Turtle,
    pizzas: Pizza,
    weapons: Weapon,

    sequelize: sequelize,
    Sequelize: Sequelize,
    Op: Sequelize.Op,
  };
};
    

GraphQL - Пример


// models/test-data.js
module.exports = async function (db) {
  await db.sequelize.sync({force: true});

  await db.pizzas.bulkCreate([
    {name: "Neapolitan", colories: 1000},
    {name: "Sicilian", colories: 1400},
    {name: "Pepperoni", colories: 2200},
    {name: "Mozzarella", colories: 1700},
  ]);

  ...
};
    

GraphQL - Пример


// models/test-data.js
module.exports = async function (db) {
  ...

  await db.weapons.bulkCreate([
    {name: "Katana", dps: 79},
    {name: "Nunchucks", dps: 55},
    {name: "Staff", dps: 60},
    {name: "Sai", dps: 70},
  ]);

  ...
};
    

GraphQL - Пример


// models/test-data.js
module.exports = async function (db) {
  ...

  await db.turtles.bulkCreate([
    { name: "Leo", color: "Blue",
      favoritePizzaId: 1,
      weaponId: 1 },
    { name: "Mikey", color: "Orange",
      favoritePizzaId: 1,
      secondFavoritePizzaId: 2,
      weaponId: 2 },
    ...
  ]);
};
    

GraphQL - Пример


// models/test-data.js
module.exports = async function (db) {
  ...

  await db.turtles.bulkCreate([
    ...
    { name: "Donnie", color: "Purple",
      favoritePizzaId: 2,
      weaponId: 3 },
    { name: "Raph", color: "Red",
      favoritePizzaId: 3,
      secondFavoritePizzaId: 1,
      weaponId: 4 },
  ]);
};
    

GraphQL - Пример


// schema.js
const {
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLInt,
  GraphQLString,
  GraphQLFloat,
  GraphQLList,
} = require('graphql');

module.exports = db => {
  ...

  return new GraphQLSchema({ query: Query });
};
    

GraphQL - Пример


// schema.js
const Pizza = new GraphQLObjectType({
  name: 'Pizza',
  fields: {
    id: { type: GraphQLInt },
    name: { type: GraphQLString },
    colories: { type: GraphQLInt },
  },
});
    

GraphQL - Пример


// schema.js
const Weapon = new GraphQLObjectType({
  name: 'Weapon',
  fields: {
    id: {type: GraphQLInt},
    name: {type: GraphQLString},
    dps: {type: GraphQLFloat},
  },
});
    

GraphQL - Пример


// schema.js
const Turtle = new GraphQLObjectType({
  name: 'Turtle',
  fields: {
    id: { type: GraphQLInt },
    name: { type: GraphQLString },
    color: { type: GraphQLString },
    weapon: {
      type: Weapon,
      resolve: turtle =>
        db.weapons.findById(
          turtle.weaponId,
          { raw: true }
        )
    },
    ...
  },
});
    

GraphQL - Пример


// schema.js
const Turtle = new GraphQLObjectType({
  name: 'Turtle',
  fields: {
    ...
    favoritePizza: {
      type: Pizza,
      resolve: turtle =>
        db.pizzas.findById(
          turtle.favoritePizzaId,
          { raw: true }
        )
    },
    ...
  },
});
    

GraphQL - Пример


// schema.js
const Turtle = new GraphQLObjectType({
  name: 'Turtle',
  fields: {
    ...
    secondFavoritePizza: {
      type: Pizza,
      resolve: turtle =>
        db.pizzas.findById(
          turtle.secondFavoritePizzaId,
          { raw: true }
        )
    },
  },
});
    

GraphQL - Пример


// schema.js
const Query = new GraphQLObjectType({
  name: 'TMNT',
  fields: {
    turtles: {
      type: new GraphQLList(Turtle),
      resolve: () =>
        db.turtles.findAll({ raw: true })
    },
    ...
  }
});
    

GraphQL - Пример


// schema.js
const Query = new GraphQLObjectType({
  name: 'TMNT',
  fields: {
    ...
    weapons: {
      type: new GraphQLList(Weapon),
      resolve: () =>
        db.weapons.findAll({ raw: true })
    },
    ...
  }
});
    

GraphQL - Пример


// schema.js
const Query = new GraphQLObjectType({
  name: 'TMNT',
  fields: {
    ...
    pizzas: {
      type: new GraphQLList(Pizza),
      resolve: () =>
        db.pizzas.findAll({ raw: true })
    },
    ...
  }
});
    

GraphQL - Пример


// schema.js
const Query = new GraphQLObjectType({
  name: 'TMNT',
  fields: {
    ...
    turtle: {
      args: { id: { type: GraphQLInt } },
      type: Turtle,
      resolve: (obj, args) =>
        db.turtles.findById(
          args.id,
          { raw: true }
        )
    },
  }
});
    

GraphQL - Пример


// index.js
const Sequelize = require('sequelize');
const Promise = require('bluebird');
const express = require('express');
const graphqlHttp = require('express-graphql');

const config = require('./config');

const db = require('./models')(Sequelize, config);
const schema = require('./schema')(db);

const fillWithTestData = require('./models/test-data');

...
    

GraphQL - Пример


// index.js
...

const app = express();

app.listen = Promise
  .promisify(app.listen)
  .bind(app);

app.use(graphqlHttp({
  schema,
  pretty: true,
  graphiql: true
}));

...
    

GraphQL - Пример


// index.js
...

(async function () {
  await fillWithTestData(db);

  await app.listen(config.port);

  console.log(`Server running at http://localhost:${config.port}`);
})();