Skip to content

Commit

Permalink
Adds documentation for polymorphic tables.
Browse files Browse the repository at this point in the history
This commit also adds some info about association proxies in the same
document.
  • Loading branch information
jfinkels committed Jun 5, 2016
1 parent b3bd32d commit 6823696
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 11 deletions.
3 changes: 2 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ Not yet released.
- :issue:`7`: allows filtering before function evaluation.
- :issue:`49`: deserializers now expect a complete JSON API document.
- :issue:`200`: be smarter about determining the ``collection_name`` for
polymorphic tables.
polymorphic models defined with single-table inheritance.
- :issue:`253`: don't assign to callable attributes of models.
- :issue:`481,488`: added negation (``not``) operator for search.
- :issue:`492`: support JSON API recommended "simple" filtering.
- :issue:`536`: adds support for single-table inheritance.

Version 1.0.0b1
---------------
Expand Down
15 changes: 6 additions & 9 deletions docs/basicusage.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. currentmodule:: flask.ext.restless

.. _basicusage:

Creating API endpoints
======================

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
219 changes: 219 additions & 0 deletions docs/databasesetup.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ the JSON API specification.
basicusage
requestformat
customizing
databasesetup

API reference
-------------
Expand Down

0 comments on commit 6823696

Please sign in to comment.