Thursday, 24 October 2024

GraphQL

 


Introduction

API is the abbreviation of Application Programming Interface. It means when two or more applications talk to each other they would use this intermediary program. Web API is an API which can be accessed over internet which is usually using HTTP or HTTPS protocol. GraphQL is one of its kind running over HTTP/HTTPS. (but its not hard and fast rule, it can be on any protocol)

Let me re-confirm GraphQL is not a protocol it is specification for client-server communication. It means specification is a description provided how client-server talks to understand each other, in lay man term its how we speak a language for our communication.

GraphQL is on the Web API kind. GraphQL is one of the most modern ways of building and querying APIs given the input in JSON format. It helps with a runtime for fulfilling those queries with your existing data. 

Internet is all around right from system to watch, each serve different set of users serving different demands. Let's imagine we need to fulfil similar request but with different volume data based on the client. It would be inappropriate if we give less/underfetching or more/overfetching information to all. As a saviour GraphQL comes to serve clients only for the asked information without affecting coding changes in the server. 

GraphQL is originally developed to query server hence its called GraphQL - Graph Query Language. One might wonder then why it is Graph, well the information stored in hierarchically is know as graph and using GraphQL you can drill down to every connection and get the node information (we will see below with an example).

GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools (GraphiQL).

Facebook's mobile apps have been powered by GraphQL since 2012.





Today we will see about GraphQL, its features, pros and cons. 

GraphQL Features

  • GraphQL communication

    • GraphQL provides standard three ways of communication :

      • schema

      • query

      • response

  • GraphQL schema

    • Describe data provided by a server in a statically typed Schema.

  • GraphQL query

    • A GraphQL query is a string that is sent to a server to be interpreted and fulfilled, which then returns JSON back to the client.

  • GraphQL response

    • GraphQL query from the API will get exactly what is asked, nothing more nothing less.

    • GraphQL queries mirror their response based on the query asked. This makes it easy to predict the shape of the data returned from a query, as well as to write a query if you know the data your app needs. 

  • GraphQL performance impact

    • Applications using GraphQL are fast and stable because the data is controlled by them not the server.

    • Its hierarchical nature, naturally follows relationships between objects, where a RESTful service may require multiple round-trips.

  • GraphQL single trip

    • GraphQL APIs get all the data your app needs in a single request.

    • Apps using GraphQL can be quick even on slow mobile network connections, as multiple trips are avoided to fetch data from server.

  • GraphQL type system

    • Each level of a GraphQL query corresponds to a particular type, and each type describes a set of available fields.

    • GraphQL uses types to ensure Apps only ask for what’s possible and provide clear and helpful errors.

    • Apps can use types to avoid writing manual parsing code.

  • GraphQL IDE

    • GraphiQL is the GraphQL integrated development environment (IDE)

    • GraphiQL helps developers and api users learn and explore an API quickly without grepping the codebase or struggling with cURL

    • Easily you can access it when your site’s development server is running at http(s)://<ip>:<port>/graphql (Python version)

  • GraphQL versioning

    • When you’re adding new product features, additional fields can be added to the server, leaving existing clients unaffected.

    • By using a single evolving version, GraphQL APIs give apps continuous access to new features and encourage cleaner, more maintainable server code.

  • GraphQL Integrations

    • GraphQL can be easily integrated with the below packages:

      • Graphene-Django

      • Flask-Graphql

      • Graphene-SQLAlchemy

      • Graphene-GAE

      • Graphene-Mongo

      • Starlette

      • FastAPI

    • We will below examples using the Flask integration.

  • GraphQL operations

    • Query: GraphQL Query is used to read or fetch values.

    • Mutation: GraphQL Mutation queries modify data in the data store and returns a value. It can be used to insert, update, or delete data.

    • Subscription: Subscriptions are a GraphQL feature that allows a server to send data to its clients when a specific event happens.



Requirements and Installation

To start using graphql, we would need python 3+ version.

Python >= 3.4

Graphene (3.0) is the module used to the graphql.

Using pip you can easily install graphene.

	pip install "graphene>=3.0"



GraphQL Structure

Fields

GraphQL Fields are generally the columns specified, which could be called in querying the values. GraphQL provides different types of Fields, such as ID, String, Scalar, List, Enum, Object, Interface and Unions.

Eg: 

Query:

projects {
    ProjectID
    ProjectName
    LOBDetails {
        LOBID
        LOBName
    }
}

Output:

{
    “data”: {
        projects:
        {
            ProjectID: “J1”
            ProjectName: “Jio”
            LOBDetails:
            {
                LOBID: “AT”
                LOBName: “Atul”
            }
            {
                LOBID: “BA”
                LOBName: “Bavesh”
            }
        }
    }
}

From the above example, we have returned 1 project which is headed by 2 LOBs. Here the LOBID is ID data type and LOBName is string datatype. There would be other fields also but GraphQL will return only the asked details.

Resolver

Resolver is ideally a function associated with field which generates the response in JSON format for the GraphQL Query. And it would be only called in the query is requesting to view the value of the field. In the above example we have LOBDetails information which would be resolved while its called. Lets see the syntax with example code. Note the function should always start with resolve_<FieldName>

resolve_fieldName:(root, args, context, info) => { result }


Eg:

class Project(graphene.ObjectType):
    LOBID = graphene.ID(required=True)
    ProjectID = graphene.ID(required=True)
    ProjectName = graphene.String()
    ProjectDesc = graphene.String()
    LOBDetails = graphene.List(LOB)

    def resolve_LOBDetails(root, info):
        lobid = root.get('LOBID', None)
        return db.get_lob(lobid)



Schema

The GraphQL Schema is where the data model is described. GraphQL server provides set of resolve methods that know how and from where to fetch the data. Schema will be defined once in the program, which will collect all the type definitions. Later down we have an example, filename called schema.py which holds the sample schema definition. 


Query

GraphQL Query can be said as Read operation. This where you can specify or we can query the API Fields. Even though query seems to be GET operation, but the actual POST HTTP Method is called for this. Class graphene.ObjectType needs to be inherited for framing the Query. Below we have specified the syntax of using query:

//syntax 1
query query_name{ someField }

//syntax 2
{ someField }


Mutation

GraphQL Mutation can be said as Write, Update and Delete operations. Here you will specify the values which needs to be changed, and response after performing the operation. Below we have specified the syntax of using query:

mutation{
   someEditOperation( someInputSchema: 
	{	
		dataField1:"valueOfField1",
		dataField2:"valueOfField2"
	}
    ) {
      returnValue1
      returnValue2
   }
}


Validation

Its one of the very useful feature provided by the module, any input either while querying or mutating the values the validation will be done. Validation such as not null, datatype validation, string length, range, and so on. Below we have any example where the LOBID is mandatory to be provided while insertion if not operation will fail.


Enum

GraphQL supports Enumerations. An enumeration is a set of symbolic names which are bound to unique and having constant values. In graphene we can add description as well to the Enum values using @property with description method.

Eg:

# Enum Definition

class Rating(graphene.Enum):
    EXCELLENT = 1
    GOOD = 2
    OKAY = 3

    @property
    def description(self):
        if self == Rating.EXCELLENT:
            return 'Very Good Performance'
        if self == Rating.GOOD:
            return 'Good Performance'
        return 'Okay Performance'
# Enum Usage as Field
class UpdateEmployee(graphene.Mutation):
	EmpID = graphene.ID(required=True)

EmpRating = Rating(required=True)



Interface

As like general language Interface in GraphQL also provides similar feature. It defines the abstract type that contains certain fields which can be later inherited to other Schemas. For example we have few common fields which we can define in single interface and use it across the respective schemas. Also we would need to write an resolver for this while querying.

Eg:

import graphene


Creation of Interface called AuditInformation

class AuditInformation(graphene.Interface):

CreatedBy = graphene.String()

ModifiedBy = graphene.String()

CreatedOn = graphene.Date()

# Need to define a resolve_type class method, else will return error. This is to map the interface date object to graphene.type

# Error: "Abstract type XXX must resolve to an Object type at runtime for field..."

# https://docs.graphene-python.org/en/latest/types/interfaces/#resolving-data-objects-to-types

@classmethod

def resolve_type(cls, instance, info):

if instance["Type"] == 'Project':

return Project

return LOB



# Calling the Interface as AuditDetails in LOB Schema

class LOB(graphene.ObjectType):

AuditDetails = graphene.Field(AuditInformation)

LOBID = graphene.ID(required=True)

LOBName = graphene.String()

 

def resolve_AuditDetails(root, LOBID): 

    return get_audit_details(LOBID)



# Calling the Interface as AuditDetails in Project Schema

class Project(graphene.ObjectType):

AuditDetails = graphene.Field(AuditInformation)

ProjectID = graphene.ID(required=True)

def resolve_AuditDetails(root, LOBID, ProjectID):

return get_audit_details(LOBID, ProjectID)

Lets see how to query the interfaces:

Query:

query{

    project{

        ProjectID

        LOBID

        AuditDetails{

            CreatedBy

            ModifiedBy

            __typename

        }

    }

}


Result:

{

    "data": {

        "project": [

        {

            "ProjectID": "Proj1",

            "LOBID": "123",

            "AuditDetails": {

                "CreatedBy": "Admin",

                "__typename": "Project"

            }

        },

        {

            "ProjectID": "Proj2",

            "LOBID": "123",

            "AuditDetails": {

                "CreatedBy": "Admin",

                "__typename": "Project"

            }

        }

    }

}


Union

Unions are like we have in dbs, but not that we will have combine different values in single row but still the values are get build in multiple rows. If you want to return more than one type of object they you can create union type, which creates an association between two different object types. In graphene package it must inherit from graphene.Union. Unions don’t have any fields on it, just links to the possible graphene.ObjectType.

Eg:

class Project(graphene.ObjectType):

class Meta:

interfaces = (AuditInformation, )

LOBID = graphene.ID(required=True)

ProjectID = graphene.ID(required=True)

ProjectName = graphene.String()

ProjectDesc = graphene.String()

LOBDetail = graphene.List(LOB)

def resolve_LOBDetail(root, info):

lobid = root.get('LOBID', None)

return db.get_lob(lobid)

AuditDetails = graphene.Field(AuditInformation)

def resolve_AuditDetails(root, info):

return get_audit(root, info)


class POC(graphene.ObjectType):

class Meta:

interfaces = (AuditInformation, )

LOBID = graphene.ID(required=True)

POCID = graphene.ID(required=True)

POCName = graphene.String()

POCDesc = graphene.String()

LOBDetail = graphene.List(LOB)

def resolve_LOBDetail(root, info):

lobid = root.get('LOBID', None)

return db.get_lob(lobid)

AuditDetails = graphene.Field(AuditInformation)

def resolve_AuditDetails(root, info):

return get_audit(root, info)


class LOBSolutions(graphene.Union):

class Meta:

types = (LOB, Project)


@classmethod

def resolve_type(cls, instance, info):

print(instance)

if instance.get("POCID"):

return POC

return Project


Lets see how to query the Unions:

Query:

query{

    lobsolutions{

        ... on Project{

            ProjectID

            ProjectName

         },

        ... on POC {

            POCID

            POCName

            LOBDetail{

                LOBID

                LOBName

            }

        }

    }

}



Results

{

    "data": {

        "lobsolutions": [

            {

                "ProjectID": "Proj1",

                "ProjectName": "Project"

            },

            {

                "ProjectID": "Proj2",

                "ProjectName": "Project"

            },

            {

                "POCID": "POCProj1",

                "POCName": "POCProject",

                "LOBDetail": [

                    {

                        "LOBID": "123",

                        "LOBName": "Test"

                    }

                ]

            },

            {

                "POCID": "POCProj2",

                "POCName": "POCProject",

                "LOBDetail": [

                    {

                        "LOBID": "123",

                        "LOBName": "Test"

                    }

                ]

            }

        ]

    }

}


GraphQL Example

Easiest way to learn is through understand different examples. Lets try that out. Here we will try to create lob (line of business) information.


Source Code:

File: lob_schema.py

Description: This file holds the information about LOB schema.

File Contents:

import lob_db


# Schema Objects

# Inherited by graphene.ObjectType will be used by Query

class LOB(graphene.ObjectType):

# ID : Is a datatype, which refers to the primary key

# required=True -> Makes the LOBID mandatory to be given from the client

LOBID = graphene.ID(required=True)

LOBName = graphene.String()

LOBDesc = graphene.String()


# Inherited by graphene.InputObjectType will be used by Mutation

class LOBInput(graphene.InputObjectType):

LOBID = graphene.ID(required=True)

LOBName = graphene.String()

LOBDesc = graphene.String()



class CreateLOB(graphene.Mutation):

class Arguments:

lob_input = LOBInput(required=True)


ok = graphene.Boolean()

msg = graphene.String()

def mutate(root, info, lob_input=None):

ok, msg = db.insert_lob(lob_input)

return CreateLOB(ok=ok, msg=msg)



File: lob_db.py

Description:

  • This file holds database operations for lob

  • In the below example LOBS are saved in the array of dict

  • But in real use case it could be any database

  • But the return value should match the keys of our defined Schema

File Contents:

LOBS = [

    {

        'LOBID': "123",

        'LOBName': "Test",

        'LOBDesc': "Test Desc"

    } 

]


def get_lob(lobid=None):

    result = LOBS

    if(lobid):

        result = filter(lambda lob: lob['LOBID'] == lobid, LOBS)

    return result


def insert_lob(lob):

    print(lob)

    LOBS.append(lob)

    return True, "Success"



File: schema.py

Description:

  • This file holds the maps between all the schema in one file.

  • We can have more than one sub schemas defined but will link here.

  • createlob is a field returned from CreateLOB 

File Contents:

import lob_schema

import lob_db


class Mutation(graphene.ObjectType):

    createlob = lob_schema.CreateLOB.Field()


class Query(graphene.ObjectType):

    lob = graphene.List(lob_schema.LOB)

    def resolve_lob(root, info):

        return lob_db.get_lob()


schema = graphene.Schema(query=Query, mutation=Mutation)



File: app.py

Description:

  • This file defines the application's starting point

  • GrahpQL here is integrated with Flask

  • Flask server will run

  • /grahpql path will be used both for Query and Mutation

  • For both it will be POST Method


File Contents:

from flask import Flask

from flask_graphql import GraphQLView

from schema import schema


app = Flask(__name__)

app.debug = True


@app.route('/')

def index():

return '<p> Hello World</p>'


app.add_url_rule(

'/graphql',

view_func=GraphQLView.as_view(

'graphql',

schema=schema,

graphiql=True # for having the GraphiQL interface

)

)


if __name__ == '__main__':

    app.run()



Execution GraphQL Server:

Execute: app.py

% python app.py 

* Serving Flask app 'app' (lazy loading)

* Environment: production

WARNING: This is a development server. Do not use it in a production deployment.

Use a production WSGI server instead.

* Debug mode: on

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

* Restarting with watchdog (fsevents)

* Debugger is active!





Output:

Step 1: 

  • Open the URL with the given hostname and port

  • Check the “/” path 

  • From the source code it will print Hello World

Fig1: Root path, as per code it prints Hello World


Step 2:

  • Open the /graphql path. 

  • This is the same path configured in the app.py, this can be any name though

  • Below will shown only if the graphiql is enabled in app.py

Fig 2: graphql path, shows Query and Mutation


Step 3:

  • Lets check the available Mutation, in our scenario we will have createlob

  • LOBInput is the values required to be inserted/updated/deleted - mutated


Fig3: createlob is listed specifying its input and return value


Step 4:

  • Lets see what values in the LOBInput requires

  • The “!” Value next to ID means required


Fig4: Input values for LOB 


Step 5:

  • Query the current values available in the LOB

  • As you could see we have only one record

Fig5: Query output of LOB 


Step 6:

  • Mutation called to perform insert

  • Here LOBID and LOBName values are specified to be inserted

  • LOBDesc is not given, which will be considered as NULL.

Fig6: Mutation query to insert


Step 7:

  • After insert lets query again and see the output.

  • Inserted row should be visible.

  • List of executed queries will also be recorded in the History

  • Necessary fields can be used to extract.

Fig7: After Mutation, Query output




GraphQL Vs REST

  • Below is very good reference written stating the difference of GraphQL and REST

Fig8 : GrapqhQL vs REST

Credits: https://www.mobilelive.ca/blog/graphql-vs-rest-what-you-didnt-know/



GraphQL Pros

  • A GraphQL schema offers an organisation a way to federate its entire API.

  • GraphQL calls are handled in a single round trip. Clients get what they request with no over fetching.

  • Strongly defined data types reduce miscommunication between the client and the server. 

  • GraphQL allows an application API to evolve without breaking existing queries.



GraphQL Cons

  • GraphQL presents a learning curve for developers familiar with REST APIs.

  • GraphQL shifts much of the work of a data query to the server side, which adds complexity for server developers.

  • Caching is more complex than with REST.

  • No standard HTTP error codes supported.

  • URLs cannot be bookmarked, as the query is passed in the POST.

  • API maintainers have the additional task of writing maintainable GraphQL schema.



Conclusion

We have seen a very basic example of GraphQL. But it holds lot more concepts such as Enum, Subscription, Interfaces, Unions, etc. Below references provide very good insights in such concepts.


References


No comments:

Post a Comment

Scarcity Brings Efficiency: Python RAM Optimization

  In today’s world, with the abundance of RAM available, we rarely think about optimizing our code. But sooner or later, we hit the limits a...