Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for joined table inheritance. #554

Merged
merged 1 commit into from
Jul 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Not yet released.
- :issue:`492`: support JSON API recommended "simple" filtering.
- :issue:`508`: flush the session before postprocessors, and commit after.
- :issue:`536`: adds support for single-table inheritance.
- :issue:`546`: adds support for joined table inheritance.

Version 1.0.0b1
---------------
Expand Down
16 changes: 8 additions & 8 deletions docs/databasesetup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ 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.
Flask-Restless automatically handles polymorphic models defined using either
single table or joined 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::
Expand Down
7 changes: 5 additions & 2 deletions flask_restless/serialization/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,12 @@ def _dump(self, instance, only=None):
# Exclude column names that are blacklisted.
columns = (c for c in columns
if not c.startswith('__') and c not in COLUMN_BLACKLIST)
# Exclude column names that are foreign keys.
# Exclude column names that are foreign keys (unless the foreign
# key is the primary key for the model; this can happen in the
# joined table inheritance database configuration).
foreign_key_columns = foreign_keys(model)
columns = (c for c in columns if c not in foreign_key_columns)
columns = (c for c in columns if c not in foreign_key_columns or
c == primary_key_for(model))

# Create a dictionary mapping attribute name to attribute value for
# this particular instance.
Expand Down
130 changes: 113 additions & 17 deletions tests/test_polymorphism.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@
"""Unit tests for interacting with polymorphic models.

The tests in this module use models defined using `single table
inheritance`_.
inheritance`_ and `joined table inheritance`_.

.. _single table inheritance: http://docs.sqlalchemy.org/en/latest/orm/inheritance.html#single-table-inheritance
.. _joined table inheritance: http://docs.sqlalchemy.org/en/latest/orm/inheritance.html#joined-table-inheritance

"""
from operator import itemgetter

from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from sqlalchemy import Unicode

from flask_restless import DefaultSerializer
Expand All @@ -32,15 +34,12 @@
from .helpers import ManagerTestBase


class PolymorphismTestBase(ManagerTestBase):
"""Base class for tests of APIs created for polymorphic models
defined using single table inheritance.

"""
class SingleTableInheritanceSetupMixin(object):
"""Mixin for setting up single table inheritance in test cases."""

def setUp(self):
"""Creates polymorphic models using single table inheritance."""
super(PolymorphismTestBase, self).setUp()
super(SingleTableInheritanceSetupMixin, self).setUp()

class Employee(self.Base):
__tablename__ = 'employee'
Expand All @@ -64,11 +63,42 @@ class Manager(Employee):
self.Base.metadata.create_all()


class FetchingTestBase(PolymorphismTestBase):
class JoinedTableInheritanceSetupMixin(object):
"""Mixin for setting up joined table inheritance in test cases."""

def setUp(self):
"""Creates polymorphic models using joined table inheritance."""
super(JoinedTableInheritanceSetupMixin, self).setUp()

class Employee(self.Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
type = Column(Enum('employee', 'manager'), nullable=False)
name = Column(Unicode)
__mapper_args__ = {
'polymorphic_on': type,
'polymorphic_identity': 'employee'
}

# This model inherits directly from the `Employee` class, so
# there is only one table being used.
class Manager(Employee):
__tablename__ = 'manager'
id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
__mapper_args__ = {
'polymorphic_identity': 'manager'
}

self.Employee = Employee
self.Manager = Manager
self.Base.metadata.create_all()


class FetchingTestMixinBase(object):
"""Base class for test cases for fetching resources."""

def setUp(self):
super(FetchingTestBase, self).setUp()
super(FetchingTestMixinBase, self).setUp()

# Create the APIs for the Employee and Manager.
self.apimanager = self.manager
Expand All @@ -83,7 +113,7 @@ def setUp(self):
self.session.commit()


class TestFetchCollection(FetchingTestBase):
class FetchCollectionTestMixin(FetchingTestMixinBase):
"""Tests for fetching a collection of resources defined using single
table inheritance.

Expand Down Expand Up @@ -155,7 +185,7 @@ def serialize(self, instance, *args, **kw):
assert employees[1]['attributes']['baz'] == 'xyzzy'


class TestFetchResource(FetchingTestBase):
class FetchResourceTestMixin(FetchingTestMixinBase):
"""Tests for fetching a single resource defined using single table
inheritance.

Expand Down Expand Up @@ -202,14 +232,14 @@ def test_subclass_at_superclass(self):
assert response.status_code == 404


class TestCreating(PolymorphismTestBase):
class CreatingTestMixin(object):
"""Tests for APIs created for polymorphic models defined using
single table inheritance.

"""

def setUp(self):
super(TestCreating, self).setUp()
super(CreatingTestMixin, self).setUp()
self.manager.create_api(self.Employee, methods=['POST'])
self.manager.create_api(self.Manager, methods=['POST'])

Expand Down Expand Up @@ -278,11 +308,11 @@ def test_superclass_at_subclass(self):
'type', 'manager', 'employee'])


class TestDeleting(PolymorphismTestBase):
class DeletingTestMixin(object):
"""Tests for deleting resources."""

def setUp(self):
super(TestDeleting, self).setUp()
super(DeletingTestMixin, self).setUp()

# Create the APIs for the Employee and Manager.
self.manager.create_api(self.Employee, methods=['DELETE'])
Expand Down Expand Up @@ -339,11 +369,11 @@ def test_superclass_at_subclass(self):
assert self.session.query(self.Employee).all() == self.all_employees


class TestUpdating(PolymorphismTestBase):
class UpdatingTestMixin(object):
"""Tests for updating resources."""

def setUp(self):
super(TestUpdating, self).setUp()
super(UpdatingTestMixin, self).setUp()

# Create the APIs for the Employee and Manager.
self.manager.create_api(self.Employee, methods=['PATCH'])
Expand Down Expand Up @@ -433,3 +463,69 @@ def test_superclass_at_subclass(self):
response = self.app.patch('/api/manager/1', data=dumps(data))
check_sole_error(response, 404, ['No resource found', 'type',
'manager', 'ID', '1'])


class TestFetchCollectionSingle(FetchCollectionTestMixin,
SingleTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for fetching a collection of resources defined using single
table inheritance.

"""


class TestFetchCollectionJoined(FetchCollectionTestMixin,
JoinedTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for fetching a collection of resources defined using joined
table inheritance.

"""


class TestFetchResourceSingle(FetchResourceTestMixin,
SingleTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for fetching a single resource defined using single table
inheritance.

"""


class TestFetchResourceJoined(FetchResourceTestMixin,
JoinedTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for fetching a single resource defined using joined table
inheritance.

"""


class TestCreatingSingle(CreatingTestMixin, SingleTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for creating a resource defined using single table inheritance."""


class TestCreatingJoined(CreatingTestMixin, JoinedTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for creating a resource defined using joined table inheritance."""


class TestDeletingSingle(DeletingTestMixin, SingleTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for deleting a resource defined using single table inheritance."""


class TestDeletingJoined(DeletingTestMixin, JoinedTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for deleting a resource defined using joined table inheritance."""


class TestUpdatingSingle(UpdatingTestMixin, SingleTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for updating a resource defined using single table inheritance."""


class TestUpdatingJoined(UpdatingTestMixin, JoinedTableInheritanceSetupMixin,
ManagerTestBase):
"""Tests for updating a resource defined using joined table inheritance."""