From 9255fd69b208d1f1380c7cc4b98f0567ff2cb35e Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 5 Jun 2016 14:38:25 -0400 Subject: [PATCH] Adds documentation for polymorphic tables. This commit also adds some info about association proxies in the same document. --- docs/basicusage.rst | 15 ++- docs/conf.py | 2 +- docs/databasesetup.rst | 219 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 4 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 docs/databasesetup.rst diff --git a/docs/basicusage.rst b/docs/basicusage.rst index 7c53b371..007d62da 100644 --- a/docs/basicusage.rst +++ b/docs/basicusage.rst @@ -1,7 +1,3 @@ -.. currentmodule:: flask.ext.restless - -.. _basicusage: - Creating API endpoints ====================== @@ -10,10 +6,11 @@ SQLAlchemy or Flask-SQLALchemy. The basic setup in either case is nearly the same. If you have defined your models with Flask-SQLAlchemy, first, create your -:class:`~flask.Flask` object, :class:`~flask.ext.sqlalchemy.SQLAlchemy` object, -and model classes as usual but with one additional restriction: each model must -have a primary key column named ``id`` of type :class:`sqlalchemy.Integer` or -type :class:`sqlalchemy.Unicode`. +:class:`~flask.Flask` object, :class:`~flask_sqlalchemy.SQLAlchemy` object, and +model classes as usual but with one additional restriction: each model must +have a primary key column named ``id`` of type +:class:`~sqlalchemy.sql.sqltypes.Integer` or type +:class:`~sqlalchemy.sql.sqltypes.Unicode`. .. sourcecode:: python @@ -67,7 +64,7 @@ If you are using pure SQLAlchemy:: Base.metadata.create_all() Second, instantiate an :class:`APIManager` object with the -:class:`~flask.Flask` and :class:`~flask.ext.sqlalchemy.SQLAlchemy` objects:: +:class:`~flask.Flask` and :class:`~flask_sqlalchemy.SQLAlchemy` objects:: from flask.ext.restless import APIManager diff --git a/docs/conf.py b/docs/conf.py index 1f07c499..72d7525a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -261,7 +261,7 @@ intersphinx_mapping = { 'python': ('http://docs.python.org/', None), 'flask': ('http://flask.pocoo.org/docs', None), - 'sqlalchemy': ('http://sqlalchemy.org/docs', None), + 'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest', None), 'flasksqlalchemy': ('http://flask-sqlalchemy.pocoo.org', None), 'flasklogin': ('https://flask-login.readthedocs.org/en/latest', None) } diff --git a/docs/databasesetup.rst b/docs/databasesetup.rst new file mode 100644 index 00000000..344191ff --- /dev/null +++ b/docs/databasesetup.rst @@ -0,0 +1,219 @@ +Common SQLAlchemy setups +======================== + +Flask-Restless automatically handles SQLAlchemy models defined with +`association proxies`_ and `polymorphism`_. + +.. _association proxies: http://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html +.. _polymorphism: http://docs.sqlalchemy.org/en/latest/orm/inheritance.html + +Association proxies +------------------- + +Flask-Restless handles many-to-many relationships transparently through +association proxies. It exposes the remote table in the ``relationships`` +element of a resource in the JSON document and hides the intermediate table. + +For example, consider a setup where there are articles and tags in a +many-to-many relationship:: + + from sqlalchemy import Column, Integer, Unicode, ForeignKey + from sqlalchemy.ext.associationproxy import association_proxy + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.orm import relationship, backref + + Base = declarative_base() + + class Article(Base): + __tablename__ = 'article' + id = Column(Integer, primary_key=True) + tags = association_proxy('articletags', 'tag') + + class ArticleTag(Base): + __tablename__ = 'articletag' + article_id = Column(Integer, ForeignKey('article.id'), + primary_key=True) + article = relationship(Article, backref=backref('articletags')) + tag_id = Column(Integer, ForeignKey('tag.id'), primary_key=True) + tag = relationship('Tag') + + class Tag(Base): + __tablename__ = 'tag' + id = Column(Integer, primary_key=True) + +Resource objects of type ``'article'`` will have ``tags`` relationship that +proxies directly to the ``Tag`` resource through the ``ArticleTag`` table: + +.. sourcecode:: json + + { + "data": { + "id": "1", + "type": "article", + "relationships": { + "tags": { + "data": [ + { + "id": "1", + "type": "tag" + }, + { + "id": "2", + "type": "tag" + } + ], + } + } + } + } + +By default, the intermediate ``articletags`` relationship does not appear as a +relationship in the resource object. + + +Polymorphic models +------------------ + +Flask-Restless automatically handles polymorphic models. For single-table +inheritance, we have made some design choices we believe are reasonable. +Requests to create, update, or delete a resource must specify a ``type`` that +matches the collection name of the endpoint. This means you cannot request to +create a resource of the subclass type at the endpoint for the superclass type, +for example. On the other hand, requests to fetch a collection of objects that +have a subclass will yield a response that includes all resources of the +superclass and all resources of any subclass. + +For example, consider a setup where there are employees and some employees are +managers:: + + from sqlalchemy import Column, Integer, Enum + from sqlalchemy.ext.declarative import declarative_base + + Base = declarative_base() + + class Employee(Base): + __tablename__ = 'employee' + id = Column(Integer, primary_key=True) + type = Column(Enum('employee', 'manager'), nullable=False) + __mapper_args__ = { + 'polymorphic_on': type, + 'polymorphic_identity': 'employee' + } + + class Manager(Employee): + __mapper_args__ = { + 'polymorphic_identity': 'manager' + } + +Collection name +............... + +When creating an API for these models, Flask-Restless chooses the polymorphic +identity as the collection name:: + + >>> from flask.ext.restless import collection_name + >>> + >>> manager.create_api(Employee) + >>> manager.create_api(Manager) + >>> collection_name(Employee) + 'employee' + >>> collection_name(Manager) + 'manager' + +Creating and updating resources +............................... + +Creating a resource require the ``type`` element of the resource object in the +request to match the collection name of the endpoint:: + + >>> from flask import json + >>> import requests + >>> + >>> headers = { + ... 'Accept': 'application/vnd.api+json', + ... 'Content-Type': 'application/vnd.api+json' + ... } + >>> resource = {'data': {'type': 'employee'}} + >>> data = json.dumps(resource) + >>> response = requests.post('https://example.com/api/employee', data=data, + ... headers=headers) + >>> response.status_code + 201 + >>> resource = {'data': {'type': 'manager'}} + >>> data = json.dumps(resource) + >>> response = requests.post('https://example.com/api/manager', data=data, + ... headers=headers) + >>> response.status_code + 201 + +If the ``type`` does not match the collection name for the endpoint, the server +responds with a :http:statuscode:`409`:: + + >>> resource = {'data': {'type': 'manager'}} + >>> data = json.dumps(resource) + >>> response = requests.post('https://example.com/api/employee', data=data, + ... headers=headers) + >>> response.status_code + 409 + +The same rules apply for updating resources. + +Fetching resources +.................. + +Assume the database contains an employee with ID 1 and a manager with ID 2. +You can only fetch each individual resource at the endpoint for the exact type +of that resource:: + + >>> response = requests.get('https://example.com/api/employee/1') + >>> response.status_code + 200 + >>> response = requests.get('https://example.com/api/manager/2') + >>> response.status_code + 200 + +You cannot access individual resources of the subclass at the endpoint for the +superclass:: + + >>> response = requests.get('https://example.com/api/employee/2') + >>> response.status_code + 404 + >>> response = requests.get('https://example.com/api/manager/1') + >>> response.status_code + 404 + +Fetching from the superclass endpoint yields a response that includes resources +of the superclass and resources of the subclass:: + + >>> response = requests.get('https://example.com/api/employee') + >>> document = json.loads(response.data) + >>> resources = document['data'] + >>> employee, manager = resources + >>> employee['type'] + 'employee' + >>> employee['id'] + '1' + >>> manager['type'] + 'manager' + >>> manager['id'] + '2' + +Deleting resources +.................. + +Assume the database contains an employee with ID 1 and a manager with ID 2. +You can only delete from the endpoint that matches the exact type of the +resource:: + + >>> response = requests.delete('https://example.com/api/employee/2') + >>> response.status_code + 404 + >>> response = requests.delete('https://example.com/api/manager/1') + >>> response.status_code + 404 + >>> response = requests.delete('https://example.com/api/employee/1') + >>> response.status_code + 204 + >>> response = requests.delete('https://example.com/api/manager/2') + >>> response.status_code + 204 diff --git a/docs/index.rst b/docs/index.rst index 1959a8c1..9fa7e56a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ the JSON API specification. basicusage requestformat customizing + databasesetup API reference -------------