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

Custom exceptions #1112

Closed
thaingo opened this issue Dec 17, 2019 · 14 comments
Closed

Custom exceptions #1112

thaingo opened this issue Dec 17, 2019 · 14 comments

Comments

@thaingo
Copy link
Contributor

thaingo commented Dec 17, 2019

Hi all,

I have read the documentation but could not find any information guiding of how to throw a custom exception. Let's say, when a client sends request to an Json API to create an entity with id which already exists, I would throw AccountAlreadyExistsException instead of TransactionException. How can I do that?

To be more specific, I had a look at the source code and was aware of CustomErrorException and HttpStatusException as well but still did not see a clear way of doing that. Looks like I can only throw a custom exception for my business logic code (life cycle hooks). Please advise.

Thanks,
Thai

@aklish
Copy link
Member

aklish commented Dec 17, 2019

The only places for throwing custom exceptions are in your own life cycle hooks or data stores. You could create your own store that extends whatever store you are using. On load, you can throw your own CustomErrorException.

@aklish
Copy link
Member

aklish commented Dec 18, 2019

Just a bit more background.

Each DataStore in Elide is responsible for telling Elide which entities it manages. It does this in the populateEntityDictionary method (which Elide calls during startup).

It is pretty common to have one store manage a single entity or multiple entities. When using multiple stores, you should register the MultiplexManager as the primary store which will delegate calls to the appropriate underlying store (based on which stores are responsible for which entities).

If I were you and I wanted this customized exception behavior, I would create a store for the Account model that extends the JPAStore and only register the Account bean. For all other models, I would register them in a different JPAStore.

Both JPAStores can share the same entity manager factory.

To make this a little simpler, I created a PR to allow direct control over the JPAStore and what entities it manages (rather than determining the entities from the Entity Manager Factory). You don't technically need this, but it makes things a little easier.

If you go this route, let me know if you have questions.

#1114

@thaingo
Copy link
Contributor Author

thaingo commented Dec 18, 2019

Great guidance @aklish, thank YOU and I would go this route as per your advise and will definitely ask you for more guidance.

One of other things I also got from your input here is that there might be some particular side-effect when using multiple stores. It would be better if you have an example to demonstrate that point. Sorry to ask you for that as I still do not have enough understanding of the code base and obviously do not have a good view of possible integration points of Elide where I can hook my custom implementation.

@aklish
Copy link
Member

aklish commented Dec 18, 2019

I've updated the docs (in PR) to show how to enable multiple stores:

https://github.com/yahoo/elide-doc/blob/3deb0c6217840c40fe514b16819bc1a5e84ff59d/pages/guide/06-datatstores.md

Please take a look and let me know if that makes sense.

To override the store in your case, I would do something like:

//AccountStore is your custom store to manage account models.
public class AccountStore extends JpaDataStore {
    public AuditStore(EntityManagerSupplier entityManagerSupplier,
                        JpaTransactionSupplier transactionSupplier) {
        
        //Using the code from the PR I submitted, we signal that this store only manages Account
        super(entityManagerSupplier, transactionSupplier, Account.class);
    }   
        
    @Override
    public DataStoreTransaction beginTransaction() {
        //Call the super class to get the actual transaction.
        JpaTransaction jpaTx = super.beginTransaction();
        
        jpaTx.begin();
        
        return new AccountStoreTransaction(jpaTx);
    }
}

//TransactionWrapper just delegates all transaction calls to another wrapped transaction.
public class AccountStoreTransaction extends TransactionWrapper {  
    
    public AccountStoreTransaction(DataStoreTransaction wrappedTx) {
        this.tx = wrappedTx;
    }

    //We'll delegate everything but loadObject.
    @Override
    public Object loadObject(EntityProjection projection, Serializable id,
                             RequestScope scope) {
        Object returnObj = super.loadObject(projection, id, scope);

        //Nothing was found by the provided ID.
        if (returnObj == null) {
           
           //Throw your custom exception here.
           throw new CustomErrorException(...);
        }
    }
}

You may need to also overload loadObjects depending on your use case which loads a collection given a particular filter.

Finally, you'll also need to create your other store which doesn't do anything special. Just create a JpaDataStore and pass in the new constructor everything minus Account.class.

Let me know if that makes sense.

@thaingo
Copy link
Contributor Author

thaingo commented Dec 19, 2019

Awesome @aklish.

I would like to clarify a few more things:

I guess you made a typo in the below code segment, it should be AccountStore instead of AuditStore constructor, correct?

//AccountStore is your custom store to manage account models.
public class AccountStore extends JpaDataStore {
    public AuditStore(EntityManagerSupplier entityManagerSupplier,
                        JpaTransactionSupplier transactionSupplier) {
        
        //Using the code from the PR I submitted, we signal that this store only manages Account
        super(entityManagerSupplier, transactionSupplier, Account.class);
    }   
        
    @Override
    public DataStoreTransaction beginTransaction() {
        //Call the super class to get the actual transaction.
        JpaTransaction jpaTx = super.beginTransaction();
        
        jpaTx.begin();
        
        return new AccountStoreTransaction(jpaTx);
    }
} 

Also, are we supporting Maven snapshot so that I can use the latest code (including your last PR)?

Thanks for your time.
Thai

@aklish
Copy link
Member

aklish commented Dec 19, 2019

Yes - that's a typo.

We don't publish snapshots - but the release is going out today. It may take 24 hours to sync to maven central though. If you need something right away without that change, you can override the AccountStore populateEntityDictionary function:

    @Override
    public void populateEntityDictionary(EntityDictionary dictionary) {
        dictionary.bindEntity(Account.class);
    }

You'll need to do the same thing for the other store with your other models.

@thaingo
Copy link
Contributor Author

thaingo commented Dec 21, 2019

Hi @aklish ,

Leveraged on your spring-boot-elide example, I first made a custom store as per your guidance:

public class AccountStore extends JpaDataStore {
    public AccountStore(
        EntityManagerSupplier entityManagerSupplier,
        JpaTransactionSupplier transactionSupplier) {
        super(entityManagerSupplier, transactionSupplier, **Account.class**);
    }

    @Override
    public DataStoreTransaction beginTransaction() {
        JpaTransaction jpaTxn = (JpaTransaction) super.beginTransaction();
        jpaTxn.begin();
        return new AccountStoreTransaction(jpaTxn);
    }

    private class AccountStoreTransaction extends TransactionWrapper {
        AccountStoreTransaction(DataStoreTransaction wrappedTxn) {
            super(wrappedTxn);
        }

        @Override
        public Object loadObject(
            Class<?> entityClass, Serializable id, Optional<FilterExpression> filterExpression, RequestScope scope) {
            Object returnObj = super.loadObject(entityClass, id, filterExpression, scope);
            if (null == returnObj) {
                throw new MyCustomException("not found");
            }

            return returnObj;
        }

        @Override
        public Iterable<Object> loadObjects(
            Class<?> entityClass,
            Optional<FilterExpression> filterExpression,
            Optional<Sorting> sorting,
            Optional<Pagination> pagination,
            RequestScope requestScope) {
            return super.loadObjects(entityClass, filterExpression, sorting, pagination, requestScope);
        }
    }
}

Then declared a DataStore bean:

@Configuration
public class AppConfig {

    private final EntityManagerSupplier entityManagerSupplier;
    private final JpaTransactionSupplier transactionSupplier;
    private final EntityManagerFactory entityManagerFactory;

    public AppConfig(
        EntityManagerSupplier entityManagerSupplier,
        JpaTransactionSupplier transactionSupplier,
        EntityManagerFactory entityManagerFactory) {
        this.entityManagerSupplier = entityManagerSupplier;
        this.transactionSupplier = transactionSupplier;
        this.entityManagerFactory = entityManagerFactory;
    }

    @Bean
    public DataStore dataStore() {
        DataStore store1 = new JpaDataStore(
            entityManagerFactory::createEntityManager, (NonJtaTransaction::new), Shop.class);

        AccountStore accountStore = new AccountStore(entityManagerSupplier, transactionSupplier);

        return new MultiplexManager(store1, accountStore);
    }
}

However, when I ran the example, the 2 beans EntityManagerSupplier and JpaTransactionSupplier could not be found.

So my follow up question is: how to create these 2 beans without creating any circular reference?

Thanks.

@aklish
Copy link
Member

aklish commented Dec 21, 2019

The EntityManagerSupplier and JpaTransactionSupplier are Elide constructs (just really functions) that are passed to the JPaDataStore during construction.

You are already creating those in your DataStore bean (namely entityManagerFactory::createEntityManager and NonJtaTransaction::new).

You should just remove the following variables:

private final EntityManagerSupplier entityManagerSupplier;
private final JpaTransactionSupplier transactionSupplier;

@thaingo
Copy link
Contributor Author

thaingo commented Dec 22, 2019

Sorry, I should provide you with following information in the last question:

It is always required these EntityManagerSupplier and JpaTransactionSupplier to be passed in for the custom data store constructor.

Meaning that if I removed these EntityManagerSupplier and JpaTransactionSupplier, then I could not create a bean for the custom data store like:
AccountStore accountStore = new AccountStore(entityManagerSupplier, transactionSupplier);

because it is required these 2 functions to be passed in somehow for the custom data store constructor. Specifically, when I ran the example, this error should occur:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in example.stores.AccountStore required a bean of type 'com.yahoo.elide.datastores.jpa.JpaDataStore$EntityManagerSupplier' that could not be found.


Action:

Consider defining a bean of type 'com.yahoo.elide.datastores.jpa.JpaDataStore$EntityManagerSupplier' in your configuration.

Thanks.

@aklish
Copy link
Member

aklish commented Dec 22, 2019

I think I would need to see your code. If you could post a small project to github, I could fork it and send you a PR.

If you really want to, you could create beans for the EntityManagerSupplier and JpaTransactionSupplier:

    @Bean
    public JpaDataStore.EntityManagerSupplier createEntityManagerSupplier(EntityManagerFactory factory) {
        return factory::createEntityManager;
    }

    @Bean
    public JpaDataStore.JpaTransactionSupplier createJpaTransactionSupplier() {
        return NonJtaTransaction::new;
    }

However, that shouldn't be necessary.

@thaingo
Copy link
Contributor Author

thaingo commented Dec 22, 2019

I did create these 2 beans and got the error of circular reference as I mentioned in the previous question

As per your suggestion, please take a look at the branch customer-shop-model of this source code repository which is based on your example custom data store example

Thanks for your time.

@aklish
Copy link
Member

aklish commented Dec 22, 2019

I made these changes to get past that issue:

  1. Remove @Component from CustomerStore
  2. Change AppConfig as follows:
@Configuration
public class AppConfig {

    @Bean
    public DataStore dataStore(EntityManagerFactory entityManagerFactory) {
        DataStore store1 = new JpaDataStore(
            entityManagerFactory::createEntityManager, (NonJtaTransaction::new), Shop.class);

        DataStore store2 = new CustomerStore(
            entityManagerFactory::createEntityManager, (NonJtaTransaction::new ));

        return new MultiplexManager(store1, store2);
    }
}

@thaingo
Copy link
Contributor Author

thaingo commented Dec 22, 2019

It works. Big thank @aklish.

It was strange that I had done the similar way as you did - just created these 2 beans in the AppConfig then used them to create store 2, but always got circular reference error when I ran the example as I mentioned earlier. Anyway, I can move on and do more experiments from now on. Thank you once again.

@thaingo thaingo closed this as completed Dec 22, 2019
@thaingo thaingo reopened this Dec 22, 2019
@thaingo
Copy link
Contributor Author

thaingo commented Dec 22, 2019

Re-open this as it causes 1118. We do need a proper way to define beans needed for a custom data store. Please advise @aklish .

Also, I still do not know how to throw an exception like AccountAlreadyExistsException for the request creating an entity that already exists in database. Is it a proper way to check if an entity id already exists within loadObject or loadObjects method? In any case, can you give me an sample code for that?

Thanks.

@thaingo thaingo closed this as completed Dec 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants