yahoo / elide

Elide is a Java library that lets you stand up a GraphQL/JSON-API web service with minimal effort.
https://elide.io
Other
1k stars 229 forks source link

[Question] Multiple rootable entities and entities playing both rootable and non-rootable at the same time #1164

Closed thaingo closed 4 years ago

thaingo commented 4 years ago

Hi all,

I am still validating if my approach makes sense but currently I do have use-cases when I need to have multiple rootable entities and entity with rootable and non-rootable roles at the same time in an Elide-based application.

Specifically, I have company and customer models and they in turn have a few children entities to form multiple sub-graphs. Either can be rootable. The customer is playing rootable and non-rootable roles at the same time to meet a variety of business requirements. One confusion is that there are different API endpoints allowing to perform some similar/same business requirements.

Could you please advise if any performance penalty or any other side effect I should be aware if I use multiple rootable entities in an Elide-based application? The same question if I make an entity both rootable and non-rootable?

Thanks, Thai

aklish commented 4 years ago

There is no performance penalty for having multipole rootable entities in Elide - or having an entity both rooted and a member of a collection that is reachable another way.

The only consideration is making sure your security rules cover your access patterns.

This document covers some common questions around Elide performance: https://elide.io/pages/guide/16-performance.html

thaingo commented 4 years ago

big thank @aklish .

thaingo commented 4 years ago

Sorry, I am getting back to this as I do have some more time experimenting Elide. I do have 2 models customer and shop and it is a one-to-may relationship. I made them rootable entities.
I subsequently,

  1. created a customer and a shop via POST /customer and POST /shops respectively.
  2. issued GET /customer but could not get shop information, null instead. Likewise, GET /shops did give null object of customer.

Please refer to 2nd commit of this sample project

So, how would I achieve my goal: given 2 or more rootable entities that have a relationship, how to expose API leveraging Elide so that from one entity, I can get info other entities and vice-verse. Please advise on this.

Thanks.

aklish commented 4 years ago

Can you provide the POST & GET commands you issued in addition to the sample project?

aklish commented 4 years ago

I took a quick look at your example.

I think there might be two issues here and we need to separate them:

  1. Questions about how the Elide API works.
  2. The non-standard usage of Elide data stores.

You are using two different DataStores in the context of the same transaction where a relationship exists between two entities managed by different stores. For reads, this is generally not an issue. For writes though, the ORM is going to complain.

If I switch everything over to a single JpaDataStore - I can create shops and customers is a single request or multiple requests - link them etc.

If you are having questions about the Elide API, I recommend first trying a single store and getting familiar with how to use the API (so your efforts are not blocked by errors in how the stores are being used).

If you want to persist multiple, related models in a single hibernate transaction with different stores - this will get tricky because each Elide data store transaction will save and flush objects independently. The ORM will complain.

For what you are trying to do, I would create a single Elide DataStore - and create your own abstraction inside of it to manage different load behavior for different models.

I hope this helps.

thaingo commented 4 years ago

@aklish as always, really appreciate your time and thanks for being patient with me. I have followed your advice and below is my detailed feedback.

I. Trying with original JpaDataStore - as per 4th commit in the sample project

1. Create a customer request: POST http://localhost:8080/api/v1/customer with body:

{
    "data": {
    "type": "customer",
    "attributes": {
        "email": "xyz@abc.com"
        }
    }
}

response:

{
    "data": {
        "type": "customer",
        "id": "1",
        "attributes": {
            "email": "xyz@abc.com"
        },
        "relationships": {
            "shops": {
                "data": []
            }
        }
    }
}

log:

2020-03-20 09:56:26.306 DEBUG 61778 --- [qtp918307166-14] org.hibernate.SQL                        : insert into Customer (id, email) values (null, ?)
2020-03-20 09:56:26.312 TRACE 61778 --- [qtp918307166-14] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [xyz@abc.com]

2. Create a shop request: POST http://localhost:8080/api/v1/shops with body

{
    "data": {
    "type": "shops",
    "attributes": {
        "name": "Shop ABC"
        }
    }
}

response:

{
    "data": {
        "type": "shops",
        "id": "1",
        "attributes": {
            "name": "Shop ABC"
        },
        "relationships": {
            "customer": {
                "data": null
            }
        }
    }
}

log:

2020-03-20 09:58:01.625 DEBUG 61778 --- [qtp918307166-19] org.hibernate.SQL                        : insert into Shop (id, customer_id, name) values (null, ?, ?)
2020-03-20 09:58:01.633 TRACE 61778 --- [qtp918307166-19] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [null]
2020-03-20 09:58:01.633 TRACE 61778 --- [qtp918307166-19] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Shop ABC]

3. Get all created customers request: GET http://localhost:8080/api/v1/customer response:

{
    "data": [
        {
            "type": "customer",
            "id": "1",
            "attributes": {
                "email": "xyz@abc.com"
            },
            "relationships": {
                "shops": {
                    "data": []
                }
            }
        }
    ]
}

log:

2020-03-20 09:59:01.812 DEBUG 61778 --- [qtp918307166-14] c.y.e.d.j.porting.EntityManagerWrapper   : HQL Query: SELECT example_models_Customer FROM example.models.Customer AS example_models_Customer  
2020-03-20 09:59:01.929 DEBUG 61778 --- [qtp918307166-14] org.hibernate.SQL                        : select customer0_.id as id1_0_, customer0_.email as email2_0_ from Customer customer0_ limit ?
2020-03-20 09:59:01.948 DEBUG 61778 --- [qtp918307166-14] org.hibernate.SQL                        : select shops0_.customer_id as customer3_1_0_, shops0_.id as id1_1_0_, shops0_.id as id1_1_1_, shops0_.customer_id as customer3_1_1_, shops0_.name as name2_1_1_ from Shop shops0_ where shops0_.customer_id=?
2020-03-20 09:59:01.948 TRACE 61778 --- [qtp918307166-14] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
  1. Get all created shops request: GET http://localhost:8080/api/v1/shops response:

    {
    "data": [
        {
            "type": "shops",
            "id": "1",
            "attributes": {
                "name": "Shop ABC"
            },
            "relationships": {
                "customer": {
                    "data": null
                }
            }
        }
    ]
    }

    log:

    2020-03-20 10:00:05.783 DEBUG 61778 --- [qtp918307166-19] c.y.e.d.j.porting.EntityManagerWrapper   : HQL Query: SELECT example_models_Shop FROM example.models.Shop AS example_models_Shop  
    2020-03-20 10:00:05.787 DEBUG 61778 --- [qtp918307166-19] org.hibernate.SQL                        : select shop0_.id as id1_1_, shop0_.customer_id as customer3_1_, shop0_.name as name2_1_ from Shop shop0_ limit ?
  2. Create a shop with explicit created customer object (issued only after the customer object created) request: POST http://localhost:8080/api/v1/shops with body

    {
    "data": {
    "type": "shops",
    "attributes": {
        "name": "Shop ABC with explicit customer object"
        },
    "relationships": {
            "customer": {
                "data": {
                    "type": "customer",
                    "id": "1"
                }
            }
        }
    }
    }

    response:

    {
    "errors": [
        "ForbiddenAccessException"
    ]
    }

    log:

    2020-03-20 10:00:47.175 DEBUG 61778 --- [qtp918307166-19] c.y.e.d.j.porting.EntityManagerWrapper   : HQL Query: SELECT example_models_Customer FROM example.models.Customer AS example_models_Customer  WHERE example_models_Customer.id IN (:id_a3c881b_0) 
    2020-03-20 10:00:47.188 DEBUG 61778 --- [qtp918307166-19] org.hibernate.SQL                        : select customer0_.id as id1_0_, customer0_.email as email2_0_ from Customer customer0_ where customer0_.id in (?)
    2020-03-20 10:00:47.190 TRACE 61778 --- [qtp918307166-19] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2020-03-20 10:00:47.200 DEBUG 61778 --- [qtp918307166-19] com.yahoo.elide.Elide                    : ForbiddenAccessException: Message=SharePermission: FAILURE Mode=Optional[USER_CHECKS_ONLY] Expression=[Optional[FAILURE]]
    2020-03-20 10:00:47.200 DEBUG 61778 --- [qtp918307166-19] com.yahoo.elide.Elide                    : ForbiddenAccessException: Message=SharePermission: FAILURE Mode=Optional[USER_CHECKS_ONLY] Expression=[Optional[FAILURE]]

    I would stop here and get a further guidance rather than by-pass the SharePermission.

II. Trying with a single custom data store - as per 3th commit in the sample project

I repeated all 5 above actions and got the same results.

Please advise further. Thanks

aklish commented 4 years ago

If you want to not include SharePermission on an object, you can either:

  1. Create the shop and the customer in the same request using the JSON-API patch extension.
  2. Issue two separate create requests, but create the second object in the collection of the first (POST /shop and then POST /shop/1/customer).
thaingo commented 4 years ago

thanks @aklish.