pyeve / eve-sqlalchemy

SQLAlchemy data layer for Eve-powered RESTful APIs
http://eve-sqlalchemy.readthedocs.io
Other
232 stars 70 forks source link

Creating model instance does not respect "viewonly" relationship attribute #188

Open ktal90 opened 5 years ago

ktal90 commented 5 years ago

I have a semi-complex relationship using a secondary. This relationship is useful to me because when querying, but I do not want to write to it. SQLAlchemy has the viewonly option that I can set on the relationship to bypass setting that relationship when I insert a new record. Eve-SQLAlchemy, however, has issues when building the model instance for a resource that has this relationship associated with it. Eve-SQLAlchemy finds this relationship as a list, which is fine, and during insert time, building the model instance fails. This is unfortunate because evaluating this field when inserting is not necessary, since I've already set this as a "viewonly" field. Following is an example of my model. The relationship in question is "organization":

class CommonColumns(Base):
    __abstract__ = True
    _created = Column(DateTime, default=func.now())
    _updated = Column(DateTime, default=func.now(), onupdate=func.now())
    _etag = Column(String(40))

    @declared_attr
    def created_by_id(cls):
        return Column(Integer,
                      ForeignKey('users.id'),
                      default=get_current_user_id)

    @declared_attr
    def updated_by_id(cls):
        return Column(Integer,
                      ForeignKey('users.id'),
                      default=get_current_user_id,
                      onupdate=get_current_user_id)

    @declared_attr
    def created_by(cls):
        return relationship(
            'Users',
            foreign_keys='{}.created_by_id'.format(cls.__name__))

    @declared_attr
    def updated_by(cls):
        return relationship(
            'Users',
            foreign_keys='{}.updated_by_id'.format(cls.__name__))

    @declared_attr
    def organization(cls):
        return relationship(
            'Organizations',
            primaryjoin='{}.created_by_id==Users.id'.format(cls.__name__),
            secondary='join(Users, Organizations, Users.organization_id == Organizations.id)',  # noqa
            viewonly=True)

class Users(Base):
    __tablename__ = 'users'
    _created = Column(DateTime, default=func.now())
    _updated = Column(DateTime, default=func.now(), onupdate=func.now())
    id = Column(Integer, primary_key=True, autoincrement=True)
    organization_id = Column(Integer,
                             ForeignKey('organizations.id'),
                             nullable=False)
    organization = relationship('Organizations',
                                back_populates='users')

class Organizations(Base):
    __tablename__ = 'organizations'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(128))
    users = relationship('Users', back_populates='organization')

Is there a better way to set up this relationship and/or how can we properly handle building up the model instead of it failing to do so?

dkellner commented 5 years ago

I've never used viewonly myself, thanks for bringing this up. I've just tried to reproduce your problems with this (your example is incomplete, so I had to do some guesswork, e.g. you never use CommonColumns and get_current_user_id is not defined).

I've come up with https://github.com/pyeve/eve-sqlalchemy/tree/viewonly/eve_sqlalchemy/examples/viewonly and tried to POST to /nodes, which works fine and GET-ing gives created_by and organization (see below). Can you maybe amend this example to show your problems with viewonly?

$ curl -i -X POST -H "Content-Type: application/json" -d '{}' localhost:5000/nodes
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 227
Location: http://localhost:5000/nodes/1
Server: Eve/0.7.10 Werkzeug/0.11.15 Python/3.6.6
Date: Mon, 12 Nov 2018 07:54:55 GMT

{"_updated": "Mon, 12 Nov 2018 07:54:55 GMT", "_created": "Mon, 12 Nov 2018 07:54:55 GMT", "_etag": "d4c403081de4d25f9005fa33ef10fcaa4f2b0063", "id": 1, "_links": {"self": {"title": "Node", "href": "nodes/1"}}, "_status": "OK"}

$ curl -i -X POST -H "Content-Type: application/json" -d '{}' localhost:5000/nodes
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 227
Location: http://localhost:5000/nodes/2
Server: Eve/0.7.10 Werkzeug/0.11.15 Python/3.6.6
Date: Mon, 12 Nov 2018 07:54:57 GMT

{"_updated": "Mon, 12 Nov 2018 07:54:57 GMT", "_created": "Mon, 12 Nov 2018 07:54:57 GMT", "_etag": "9901a9d0f565369c2e49774aa3b3e3b4764f5aaf", "id": 2, "_links": {"self": {"title": "Node", "href": "nodes/2"}}, "_status": "OK"}

$ curl localhost:5000/nodes | python -m json.tool
{
    "_items": [
        {
            "created_by": 1,
            "updated_by": 1,
            "organization": [
                1
            ],
            "id": 1,
            "_updated": "Mon, 12 Nov 2018 07:54:55 GMT",
            "_created": "Mon, 12 Nov 2018 07:54:55 GMT",
            "_etag": "d4c403081de4d25f9005fa33ef10fcaa4f2b0063",
            "_links": {
                "self": {
                    "title": "Node",
                    "href": "nodes/1"
                }
            }
        },
        {
            "created_by": 1,
            "updated_by": 1,
            "organization": [
                1
            ],
            "id": 2,
            "_updated": "Mon, 12 Nov 2018 07:54:57 GMT",
            "_created": "Mon, 12 Nov 2018 07:54:57 GMT",
            "_etag": "9901a9d0f565369c2e49774aa3b3e3b4764f5aaf",
            "_links": {
                "self": {
                    "title": "Node",
                    "href": "nodes/2"
                }
            }
        }
    ],
    "_links": {
        "parent": {
            "title": "home",
            "href": "/"
        },
        "self": {
            "title": "nodes",
            "href": "nodes"
        }
    },
    "_meta": {
        "page": 1,
        "max_results": 25,
        "total": 2
    }
}