eliminate the need for ofy() / don't clone ObjectifyImpl#334
eliminate the need for ofy() / don't clone ObjectifyImpl#334PeterDimov wants to merge 13 commits intoobjectify:masterfrom
Conversation
- identical code in both LoadTransactionalTests and LoadUnlessTests - remove from LoadTransactionalTests
…nt context - they all pass now but subsequent changes will break them due to semantic changes - once we break these tests, we'll fix separate issues in separate subsequent commits
- no more "forking" of transactional contexts/stacks, maintain a single stack per Objectify instance - ObjectifyImpl is no longer cloned - the TransactorNo instance that the bottom of the stack is reused (instead of a new copy created) each time we "escape" from the transactional context
…new semantics - alternatively, we can try to maintain the old semantics... it's doable if we initialize the LiveRef with an Objectify referece, current Transactor instance and instantiate a Loader to use later if we need to dereference
- bind Loader, Saver and Deleter to the active transactor at their instantiation time
- make sure that neither ofy nor the transactor are exposed and accessible where they shouldn't be - expose only what is really needed - factory() and getCache()
- access transaction state via the transactor passed in to the ctor - unfortunately I couldn't come up with a way to write an automated test for this, tested manually
- travis now runs a trusty environment where oraclejdk7 is no longer available, per travis support: "oraclejdk7 is unfortunately no longer available in Trusty, since we can no longer support it on our recent build images due to Oracle's withdrawal. See: http://www.webupd8.org/2017/06/why-oracle-java-7-and-6-installers-no.html" - replace oraclejdk7 with openjdk7 and add openjdk8 to the build matrix
|
Let me first say that I really appreciate the depth of thought that you have put into this, and I'm still mulling over much of it. You are right that there are some awkward evolved patterns here; transactionless() is a kludge and the behavior of I'm afraid I'm probably still going to disappoint you. The behavior of This API you wish to create looks a lot like the API from Objectify v3. That API created some very difficult patterns in several applications that I worked on (both mine and other people's). The problem is when you have bits of code like this: public void buyGame(User user, Game game) {
ofy().transact(...);
}
public void signupForTournament(User user, Game game) throws AlreadyFullException {
ofy().transact(...);
}
public void buyAndSignup(User user, Game game) throws AlreadyFullException {
ofy().transact(() -> {
buyGame(user, game);
enrollInTournament(user, game);
});
}You want to be able to aggregate bits of independent useful behavior, each of which interact with a persistence context but have no knowledge of each other. In your model, you would have to pass a specific Objectify instance as a parameter to every method that needs to work in the same persistence context. I have been trying to draw as much of the "good parts" of EJB into Objectify, and persistence-as-aspect (accessed via the That's not to say that a lot of improvements couldn't be made; I'm not particularly pleased by the way the stack is being maintained right now (it probably belongs in If you would like to continue discussion, we can either continue here or move it to the objectify google group where other people may like to participate. Another approach that might work is that if you wish to make significant changes to the Objectify API that I dislike (eg, options builders instead of functional command objects), create a wrapper as a separate project. I would be totally happy to link to it! Not everyone shares my aesthetic sensibilities, that's fine. |
|
Jeff, thanks for the detailed response! I’ll try to address some points here and will look to kick off separate discussions on the google group. Reading through https://github.com/objectify/objectify/wiki/UpgradeVersion3ToVersion4 I see how some of the stuff on this pull request looks like going back to v3. A few thoughts though: The ofy() persistence-as-aspect pattern can be the recommended to use pattern with objectify. However, it need not preclude one’s ability to use a proxy object pattern, if that’s what the user wants/needs. Ignoring what the title of the pull request says, my changes enable both patterns and I think it would be reasonable for v6 to support both patterns as well. My issue with per-command configuration (as opposed to per instance with ObjectifyOpt) has perhaps more to do with the current implementation rather than the concept. With the way ObjectifyImpl is cloned and state is shared, it’s easy to have bugs (e.g. see testChangeOfyParameterInsideTransactionHasNoEffect()). I’m particularly bothered by the fact that transactionless(), cache() and the likes return a fork/clone of ObjectifyImpl. I’d think that the intention of the ofy() pattern is to never save off the result of e.g. ofy().cache(false) for later use, however, the current implementation allows such use. In the spirit of transact(work), I think it makes perfect sense to scope the configuration to a unit of work. In other words: This transact() style api also eliminates the need to create ObjectifyImpl clones, which makes it much easier to write Objectify wrappers (composition over inheritance). I think it also does make sense for options to be part of the transaction context, so each time options are changed, a new context (Transactor) is pushed on to the stack. To take a step back, within my project I don’t use ObjectifyService and ofy(). I had created an ObjectifyStack wrapper that maintains a stack of ObjectifyImpl instances (so the proxy pattern). I didn’t implement the full Objectify api, only what I needed (transact and transactionless), with the other methods throwing. I mostly didn’t want to deal with methods that return ObjectifyImpl clones. (I imagine these are the same kind of issue that prompted you to drop the Wrapper classes in v4.) It was all working great until I tried using A problem I still haven’t solved is permissions aware datastore access for Ref loading. I currently have a permissions-aware datastore abstraction (similar api to objectify actually, but no forks) that wraps objectify. Most datastore operations go through the permissions layer but few bypass it. When I read a Ref via In terms of practical next steps, I can be using this project fork for my current needs and I’d like to be involved in the v6 discussions, maybe even contribute some code. I’d want to ultimately be running on stock Objectify v6. Potential topics I could kick off in the google group in the coming days/weeks:
Lastly, do you have any timeline for v6 development and release? Is your laundry list of things posted publicly? |
|
I've been keeping this stuff in the back of my mind as I work on v6. I've committed quite a bit of refactoring to the "v6" branch, some of which you may like and some of which you probably will not. The problem with However, I've added From a bigger picture, I believe there are two decent ways of trying to "weave" behavior like authorization into the persistence layer: 1) implementing a DAO layer on top, hiding most direct objectify calls or 2) building an interceptor layer deeper inside Objectify. Trying to implement #2 at the API layer is pretty painful because the API is broad. Simpler would be to be able to register some sort of arbitrary processor that examines entities as they are loaded (or saved). There are fewer places this happens (eg, WriteEngine). BTW permanently associating a Ref with an Objectify instance is not really possible because Refs are serializable. LiveRef already tries to do this and it sucks. I've been thinking of just adding a |
|
Jeff, thanks for following up and sorry I haven't yet followed-up with posts on the google group. We are gradually addressing the issues on this thread though. I checked out what you're doing on the v6 branch and love what I'm seeing there! No more cloning, I actually don't use public class MyObjectifyFactory extends ObjectifyFactory {
private class ObjectifyStack implements Objectify {
...
@Override
public Objectify transactionless() {
throw new IllegalStateException("Not supported!");
}
@Override
public <R> R transactionless(Work<R> work) {
return ofy().transactionless(work);
}
...
}
private ObjectifyStack ofyStack = new ObjectifyStack();
public Objectify ofyStack() {
return ofyStack;
}And would then save off the return value of myFactory.ofyStack() in my DataStore abstraction... and I could almost use the v6 branch without patches on top. As for authorization layer, I ended up doing both 1) and 2). For 1) I have my own DataStore and DataStoreLoader interfaces, the implementations of which wrap access to Objectify. I don't wrap Saver, Deleter and instead have several overloaded methods Regarding Refs, here's how I patched them to get them working reasonably well for me (aside from permission checking). I removed the association with a Objectify instance, so they cannot ever be implicitly loaded anymore. I didn't like the performance implications of that implicit loading. If in one context a Ref field is loaded via The other change I'd need to make on top of the v6 branch is to make ObjectifyFactory.open() public. Your intention is to keep these methods part of the implementation, however, I'm not using ObjectifyService and have my own "service" and Filter that initializes it. The reason is that I want to ensure that all datastore access goes throgh my Objectify wrapper where I implements the permission checks. I don't want stray ofy() code to automagically work, so I cannot ever initialize the standard ObjectifyFactory... not for the time being anyway. Lastly, I agree that using closures for ScopedObjectifyOptions would implement Closeable but not Objectify, the old syntax won't compile, which would make porting easier. Someone can shoot themselves in the foot if they don't use try-with-resources but then the assertion in |
|
In case you'd like to have a look, here is how I patched the v6 branch to get it to work with my project: The Ref changes are quite hacky but if you agree with the proposed explicit load semantics, I'm sure you'll find a cleaner way to work it in. |
Let me start by saying that I love objectify!
ObjectifyService.ofy() has been bothering me for several reasons:
For code review I suggest that you review each diff in this pull request one by one. The first diff in this pull request adds tests to demonstrate some of the issues. The subsequent diffs introduce changes and more tests to address the issues. At a high level, these are the changes:
Backwards compatibility:
I’m happy to get feedback and make additional changes in order to get this merged upstream.