diff --git a/docs/config.toml b/docs/config.toml index c1a13ecd..6a858b3c 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -23,3 +23,10 @@ ga = "UA-12613282-7" "/docs/django/tutorial/", "/docs/django/filtering/", ] + +[docs.sqlalchemy] + name = "SQLAlchemy" + pages = [ + "/docs/sqlalchemy/tutorial/", + "/docs/sqlalchemy/tips/", + ] diff --git a/docs/css/graphiql.css b/docs/css/graphiql.css index 5cc95b66..926e7c23 100644 --- a/docs/css/graphiql.css +++ b/docs/css/graphiql.css @@ -97,6 +97,7 @@ } #graphiql-container .resultWrap { + position: relative; display: -webkit-flex; display: flex; -webkit-flex-direction: column; @@ -1010,6 +1011,52 @@ span.CodeMirror-selectedtext { background: none; } background-position: right bottom; width: 100%; height: 100%; } +#graphiql-container .spinner-container { + position: absolute; + top: 50%; + height: 36px; + width: 36px; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; +} + +#graphiql-container .spinner { + vertical-align: middle; + display: inline-block; + height: 24px; + width: 24px; + position: absolute; + -webkit-animation: rotation .6s infinite linear; + -moz-animation: rotation .6s infinite linear; + -o-animation: rotation .6s infinite linear; + animation: rotation .6s infinite linear; + border-left: 6px solid rgba(150, 150, 150, .15); + border-right: 6px solid rgba(150, 150, 150, .15); + border-bottom: 6px solid rgba(150, 150, 150, .15); + border-top: 6px solid rgba(150, 150, 150, .8); + border-radius: 100%; +} + +@-webkit-keyframes rotation { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(359deg); } +} + +@-moz-keyframes rotation { + from { -moz-transform: rotate(0deg); } + to { -moz-transform: rotate(359deg); } +} + +@-o-keyframes rotation { + from { -o-transform: rotate(0deg); } + to { -o-transform: rotate(359deg); } +} + +@keyframes rotation { + from { transform: rotate(0deg); } + to { transform: rotate(359deg); } +} .CodeMirror-hints { background: white; -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); @@ -1076,4 +1123,4 @@ li.CodeMirror-hint-active { border-bottom: solid 1px #c0c0c0; border-top: none; margin-bottom: -1px; -} +} \ No newline at end of file diff --git a/docs/package.json b/docs/package.json index 694d0ff8..7e75e4b5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,7 +18,7 @@ "es6-promise": "^3.0.2", "extract-text-webpack-plugin": "^0.9.1", "gatsby": "^0.7.7", - "graphiql": "^0.4.2", + "graphiql": "^0.4.5", "graphql": "^0.4.13", "jeet": "^6.1.2", "lodash": "^3.10.1", diff --git a/docs/pages/docs/sqlalchemy/tips.md b/docs/pages/docs/sqlalchemy/tips.md new file mode 100644 index 00000000..d27e3bcc --- /dev/null +++ b/docs/pages/docs/sqlalchemy/tips.md @@ -0,0 +1,30 @@ +--- +title: Tips +description: Tips when SQLAlchemy in Graphene +--- + +# Tips + +## Querying + +For make querying to the database work, there are two alternatives: + +* Expose the db session when you create the `graphene.Schema`: + +```python +schema = graphene.Schema(session=session) +``` + +* Create a query for the models. + +```python +Base = declarative_base() +Base.query = db_session.query_property() + +class MyModel(Base): + # ... +``` + +If you don't specify any, the following error will be displayed: + +`A query in the model Base or a session in the schema is required for querying.` diff --git a/docs/pages/docs/sqlalchemy/tutorial.md b/docs/pages/docs/sqlalchemy/tutorial.md new file mode 100644 index 00000000..6d1acb03 --- /dev/null +++ b/docs/pages/docs/sqlalchemy/tutorial.md @@ -0,0 +1,199 @@ +--- +title: Tutorial +description: Using SQLAlchemy with Graphene +--- + +# SQLAlchemy + Flask Tutorial + +Graphene comes with builtin support to SQLAlchemy, which makes quite easy to operate with your current models. + +**Note: The code in this tutorial is pulled from the +[Flask SQLAlchemy example app](https://github.com/graphql-python/graphene/tree/master/examples/flask_sqlalchemy)**. + + +## Setup the Project + +We will setup the project, execute the following: + +```bash +# Create the project directory +mkdir flask_sqlalchemy +cd flask_sqlalchemy + +# Create a virtualenv to isolate our package dependencies locally +virtualenv env +source env/bin/activate # On Windows use `env\Scripts\activate` + +# SQLAlchemy and Graphene with SQLAlchemy support +pip install SQLAlchemy +pip install graphene[sqlalchemy] + +# Install Flask and GraphQL Flask for exposing the schema through HTTP +pip install Flask +pip install graphql-flask +``` + +## Defining our models + +Let's get started with these models: + +```python +# flask_sqlalchemy/models.py +from sqlalchemy import * +from sqlalchemy.orm import (scoped_session, sessionmaker, relationship, + backref) +from sqlalchemy.ext.declarative import declarative_base + +engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True) +db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + +Base = declarative_base() +# We will need this for querying +Base.query = db_session.query_property() + + +class Department(Base): + __tablename__ = 'department' + id = Column(Integer, primary_key=True) + name = Column(String) + + +class Employee(Base): + __tablename__ = 'employee' + id = Column(Integer, primary_key=True) + name = Column(String) + hired_on = Column(DateTime, default=func.now()) + department_id = Column(Integer, ForeignKey('department.id')) + department = relationship( + Department, + backref=backref('employees', + uselist=True, + cascade='delete,all')) +``` + +## Schema + +GraphQL presents your objects to the world as a graph structure rather than a more +hierarchical structure to which you may be accustomed. In order to create this +representation, Graphene needs to know about each *type* of object which will appear in +the graph. + +This graph also has a *root type* through which all access begins. This is the `Query` class below. +In this example, we provide the ability to list all employees via `all_employees`, and the +ability to obtain a specific node via `node`. + +Create `flask_sqlalchemy/schema.py` and type the following: + +```python +# flask_sqlalchemy/schema.py +import graphene +from graphene import relay +from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField +from models import db_session, Department as DepartmentModel, Employee as EmployeeModel + +schema = graphene.Schema() + + +@schema.register +class Department(SQLAlchemyNode): + class Meta: + model = DepartmentModel + + +@schema.register +class Employee(SQLAlchemyNode): + class Meta: + model = EmployeeModel + + +class Query(graphene.ObjectType): + node = relay.NodeField() + all_employees = SQLAlchemyConnectionField(Employee) + +schema.query = Query +``` + +## Creating GraphQL and GraphiQL views in Flask + +Unlike a RESTful API, there is only a single URL from which GraphQL is accessed. + +We are going to use Flask to create a server that expose the GraphQL schema under `/graphql` and a interface for querying it easily: GraphiQL under `/graphiql`. + +Afortunately for us, the library `graphql-flask` that we installed previously is making the task quite easy. + +```python +# flask_sqlalchemy/app.py +from flask import Flask +from graphql_flask import GraphQL + +from models import db_session +from schema import schema, Department + +app = Flask(__name__) +app.debug = True + +# This is creating the `/graphql` and `/graphiql` endpoints +GraphQL(app, schema=schema) + +@app.teardown_appcontext +def shutdown_session(exception=None): + db_session.remove() + +if __name__ == '__main__': + app.run() +``` + + +## Creating some data + +```bash +$ python + +>>> from models import engine, db_session, Base, Department, Employee +>>> Base.metadata.create_all(bind=engine) + +>>> # Fill the tables with some data +>>> engineering = Department(name='Engineering') +>>> db_session.add(engineering) +>>> hr = Department(name='Human Resources') +>>> db_session.add(hr) + +>>> peter = Employee(name='Peter', department=engineering) +>>> db_session.add(peter) +>>> roy = Employee(name='Roy', department=engineering) +>>> db_session.add(roy) +>>> tracy = Employee(name='Tracy', department=hr) +>>> db_session.add(tracy) +>>> db_session.commit() +``` + + +## Testing our GraphQL schema + +We're now ready to test the API we've built. Let's fire up the server from the command line. + +```bash +$ python ./app.py + + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +``` + +Go to [localhost:5000/graphiql](http://localhost:5000/graphiql) and type your first query! + +```graphql +{ + allEmployees { + edges { + node { + id + name + department { + name + } + } + } + } +} +``` diff --git a/examples/flask_sqlalchemy/app.py b/examples/flask_sqlalchemy/app.py index d011cc91..98d9f7a8 100644 --- a/examples/flask_sqlalchemy/app.py +++ b/examples/flask_sqlalchemy/app.py @@ -1,7 +1,7 @@ from flask import Flask from database import db_session, init_db -from schema import schema, Department +from schema import schema from graphql_flask import GraphQL app = Flask(__name__) diff --git a/examples/flask_sqlalchemy/schema.py b/examples/flask_sqlalchemy/schema.py index 8dd420f4..4010f5ca 100644 --- a/examples/flask_sqlalchemy/schema.py +++ b/examples/flask_sqlalchemy/schema.py @@ -3,9 +3,7 @@ from graphene import relay from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField from models import Department as DepartmentModel, Employee as EmployeeModel -from database import db_session - -schema = graphene.Schema(session=db_session) +schema = graphene.Schema() @schema.register @@ -21,7 +19,7 @@ class Employee(SQLAlchemyNode): class Query(graphene.ObjectType): - node = relay.NodeField(Department, Employee) + node = relay.NodeField() all_employees = SQLAlchemyConnectionField(Employee) schema.query = Query diff --git a/graphene/contrib/sqlalchemy/types.py b/graphene/contrib/sqlalchemy/types.py index 1d55d80a..07308f39 100644 --- a/graphene/contrib/sqlalchemy/types.py +++ b/graphene/contrib/sqlalchemy/types.py @@ -10,7 +10,7 @@ from ...relay.types import Connection, Node, NodeMeta from .converter import (convert_sqlalchemy_column, convert_sqlalchemy_relationship) from .options import SQLAlchemyOptions -from .utils import is_mapped, get_session +from .utils import is_mapped, get_query class SQLAlchemyObjectTypeMeta(ObjectTypeMeta): @@ -118,8 +118,8 @@ class SQLAlchemyNode(six.with_metaclass( def get_node(cls, id, info=None): try: model = cls._meta.model - session = get_session(info) - instance = session.query(model).filter(model.id == id).one() + query = get_query(model, info) + instance = query.filter(model.id == id).one() return cls(instance) except NoResultFound: return None diff --git a/graphene/contrib/sqlalchemy/utils.py b/graphene/contrib/sqlalchemy/utils.py index b3e47bdf..246a9d86 100644 --- a/graphene/contrib/sqlalchemy/utils.py +++ b/graphene/contrib/sqlalchemy/utils.py @@ -20,9 +20,13 @@ def get_session(info): def get_query(model, info): - query = getattr(model, 'query') + query = getattr(model, 'query', None) if not query: - query = get_session(info).query(model) + session = get_session(info) + if not session: + raise Exception('A query in the model Base or a session in the schema is required for querying.\n' + 'Read more http://graphene-python.org/docs/sqlalchemy/tips/#querying') + query = session.query(model) return query diff --git a/setup.py b/setup.py index 9e67851f..4a2a8ca1 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,8 @@ setup( 'graphql-django-view>=1.1.0', ], 'sqlalchemy': [ - 'sqlalchemy' + 'sqlalchemy', + 'singledispatch>=3.4.0.3', ] },