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

Polymorphism in flask-restless #200

Closed
gpopovic opened this issue May 8, 2013 · 9 comments
Closed

Polymorphism in flask-restless #200

gpopovic opened this issue May 8, 2013 · 9 comments
Labels

Comments

@gpopovic
Copy link

gpopovic commented May 8, 2013

I have a SQLAlchemy model named Version, and it has a type column.
Based on that column I have two polymorphic objects called Revision (if type == "revision") and Original (if type == "original"). It is polymorphism in the same table.

When you call create_api() for objects Version, Revision, and Original they will all use the prefix /version/, which is wrong. This happens because they are all using same table.
Flask-restless should a) somehow figure this out, and use model name for API prefix, or b) you should write a note about this in the documentation and tell people to use custom names.
It took me a while to figure out why i always got an HTTP 404 error when making requests to /original and /revision.

I didn't check for polymorphism with different tables but it should probably work.

I also found another serious error.

AttributeError: 'Original' object has no attribute '_sa_polymorphic_on'

I've fixed this by changing line 246, in file views.py, in function _to_dict from

result = dict((col, getattr(instance, col)) for col in columns) 

to

result = dict((col, getattr(instance, col)) for col in columns) if col != "_sa_polymorphic_on")
@jfinkels
Copy link
Owner

jfinkels commented May 8, 2013

Thanks for the bug report.

I will apply the minor fix you suggest in to_dict(). As for the other problem, can you please provide either a test case, or at least a class definition for a model that exhibits the incorrect behavior?

@jfinkels
Copy link
Owner

jfinkels commented May 8, 2013

Two further suggestions: first, please provide only one problem per issue report, and second, please provide the complete error message and code which produced it, instead of just the last line of the error message (AttributeError: ...). Thank you.

@jfinkels
Copy link
Owner

@goranek Please provide a minimal example of the model class or classes that exhibit the incorrect behavior.

@ali5h
Copy link

ali5h commented Jun 5, 2013

I also solved this by setting collection_name. In addition, if I want to use the same scenario, if you POST to /original but set type to revision the record will be added, but an exception will be raised that Original has been deleted.
Related Sample Code:

class Version(db.Meta):
    id = Column(Integer, primary_key=True)
    type = Column(Enum(u'revision', u'original'), nullable=False)
    __mapper_args__ = {
        'polymorphic_on': type,
    }

class Original(Version):
  __mapper_args__ = {
        'polymorphic_identity': 'original'
    }

class Revision(Version):
  __mapper_args__ = {
        'polymorphic_identity': 'revision'
    }

@jfinkels
Copy link
Owner

Thanks for the sample code.

@jfinkels
Copy link
Owner

I'm learning about polymorphism in order to fix this bug, and I wonder what you expect the behavior of Flask-Restless to be in the following situation.

Suppose I have

version1 = Original(id=1)
version2 = Revision(id=2)

(I believe the IDs must be distinct because each object represents a row of the single table version.) Now what happens when the client makes the following requests?

GET /version/1
GET /original/1
GET /revision/1
GET /version/2
GET /original/2
GET /revision/2

There are two possibilities that I see:

  1. We expect that the only requests that should respond with 200 OK are GET /original/1 and GET /revision/2 and that all others respond with 404 Not Found.
  2. We expect that all requests are OK and none are errors.

In either case, we'll have to do some black magic on the model class provided to APIManager.create_api to determine whether it is a polymorphic subclass. In the first case, we'll have to do some additional filtering when the query is created on a fetch request.

I suspect there may be further problems with other types of polymorphism...

@jfinkels
Copy link
Owner

Or perhaps GET /revision/1 and GET /revision/2 are OK because every Version "is a" Revision and every Original "is a" revision? That probably makes more sense with the subclass semantics.

@jfinkels
Copy link
Owner

Okay, I've put some more thought into this and this is how I'm going to implement this for now. If it doesn't make sense, we can change it in the future. As for the original issue, I have fixed it in my local repository, but I'd like to get the following semantics down before I push my solution.

Executive summary

Creating, updating, deleting, and fetching an individual resource will require the URL endpoint to match the actual type of the resource (so no updating an instance of a subclass at the URL for the superclass). However, fetching from a collection endpoint will fetch all instances of the superclass and any subclasses.

Other ways of designing this API are possible (for example, updating a subclass instance from the URL for the superclass), and I'm open to suggestions if this doesn't make sense. But for now, this is how I'm going to implement it.

Example setup code

Suppose I have the following models:

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

class Manager(Employee):
    __mapper_args__ = {
        'polymorphic_identity': 'manager'
    }

and I create the APIs like this:

apimanager.create_api(Employee, method=['POST', 'DELETE', 'PATCH', 'GET'])
apimanager.create_api(Manager, method=['POST', 'DELETE', 'PATCH', 'GET'])

We have several variables to deal with:

  1. HTTP method: fetching, creating, updating, or deleting a resource,
  2. the URL: subclass endpoint or superclass endpoint,
  3. resource type: whether the type in the request is for the subclass or the superclass.

Creating resources

At the superclass endpoint

The request

POST /employee HTTP/1.1

{"data": {"type": "employee"}}

yields the response

HTTP/1.1 201 Created
Location: http://example.com/employee/1
Content-Type: application/vnd.api+json

{"data": {"type": "employee", "id": "1"}}

However, the request

POST /employee HTTP/1.1

{"data": {"type": "manager"}}

yields a 409 Conflict error response.

At the subclass endpoint

The request

POST /manager HTTP/1.1

{"data": {"type": "manager"}}

yields the response

HTTP/1.1 201 Created
Location: http://example.com/api/manager/1
Content-Type: application/vnd.api+json

{"data": {"type": "manager", "id": "1"}}

However, the request

POST /manager HTTP/1.1

{"data": {"type": "employee"}}

yields a 409 Conflict error response.

Deleting resources

Assume we have created an employee resource at /employee/1 and a manager
resource at /manager/2.

At the superclass endpoint

The request

DELETE /employee/1 HTTP/1.1

yields a 204 No Content success response.

However, the request

DELETE /employee/2 HTTP/1.1

yields a 404 Not Found error response.

At the subclass endpoint

The request

DELETE /manager/1 HTTP/1.1

yields a 404 Not Found error response.

The request

DELETE /manager/2 HTTP/1.1

yields a 204 No Content success response.

Updating resources

Assume we have created an employee resource at /employee/1 and a manager resource at /manager/2.

At the superclass endpoint

With superclass type

The request

PATCH /employee/1 HTTP/1.1

{"data": {"type": "employee", "id": "1"}}

yields a 204 No Content success response.

However, the request

PATCH /employee/2 HTTP/1.1

{"data": {"type": "employee", "id": "2"}}

yields a 409 Conflict error response.

With subclass type

The request

PATCH /employee/1 HTTP/1.1

{"data": {"type": "manager", "id": "1"}}

yields a 409 Conflict error response.

The request

PATCH /employee/2 HTTP/1.1

{"data": {"type": "manager", "id": "2"}}

yields a 409 Conflict error response.

At the subclass endpoint

With superclass type

The request

PATCH /manager/1 HTTP/1.1

{"data": {"type": "employee", "id": "1"}}

yields a 409 Conflict error response.

The request

PATCH /manager/2 HTTP/1.1

{"data": {"type": "employee", "id": "2"}}

yields a 409 Conflict error response.

With subclass type

The request

PATCH /manager/1 HTTP/1.1

{"data": {"type": "manager", "id": "1"}}

yields a 409 Conflict error response.

The request

PATCH /manager/2 HTTP/1.1

{"data": {"type": "manager", "id": "2"}}

yields a 204 No Content success response.

Fetching collections of resources

Assume we have created an employee resource at /employee/1 and a manager resource at /manager/2.

At the superclass endpoint

Collection of resources

The request

GET /employee HTTP/1.1

yields the response

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "employee",
      "id": "1",
      "links": {
        "self": "http://example.com/employee/1"
      }
    },
    {
      "type": "manager",
      "id": "2",
      "links": {
        "self": "http://example.com/manager/2"
      }
    }
  ]
}

Individual resources

The request

GET /employee/1 HTTP/1.1

yields the response

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "employee",
    "id": "1",
    "links": {
      "self": "http://example.com/employee/1"
    }
  }
}

The request

GET /employee/2 HTTP/1.1

yields a 404 Not Found error response.

At the subclass endpoint

Collection of resources

The request

GET /manager HTTP/1.1

yields the response

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "manager",
      "id": "2",
      "links": {
        "self": "http://example.com/manager/2"
      }
    }
  ]
}

Individual resources

The request

GET /manager/1 HTTP/1.1

yields a 404 Not Found error response.

The request

GET /manager/2 HTTP/1.1

yields the response

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "manager",
    "id": "2",
    "links": {
      "self": "http://example.com/manager/2"
    }
  }
}

@jfinkels
Copy link
Owner

jfinkels commented Jul 1, 2016

Single table inheritance support was added in pull request #536 and joined table in pull request #554.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants