GAE/J OneToMany JPA Persistence Failure When Deployed - google-app-engine

I am using Google App Engine with JPA to implement a one-to-many bidirectional relationship. Everything works just fine when I debug and test my application on my machine at home, but after I deploy it to the App Engine persistence seems to break down.
This is the model I have (stripped down for simplicity):
User.java:
#Entity
class User implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key id;
#OneToMany(mappedBy = "owner",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
private List<Book> books;
public getBooks() { return this.books; }
}
Book.java:
#Entity
class Book implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key id;
#ManyToOne(fetch = FetchType.LAZY)
private User owner;
private String name;
}
To create a new User:
User user = new User()
// This is done just for testing. It works fine.
user.getBooks().add(new Book("TEST"))
EntityManager em = /* ... */
EntityTransaction transaction = em.getTransaction();
try
{
transaction.begin();
em.persist(user);
transaction.commit();
}
/* Exceptions handling. */
finally
{
if (transaction.isActive())
transaction.rollback();
em.close();
}
And to add a book:
User user = /* ... */
Book book = new Book("A new book");
user.getBooks().add(book);
EntityManager em = /* ... */
EntityTransaction transaction = em.getTransaction();
try
{
transaction.begin();
/* user.getBooks().add(book); - placing this here doesn't change anything */
em.merge(user);
transaction.commit();
}
/* Exceptions handling. */
finally
{
if (transaction.isActive())
transaction.rollback();
em.close();
}
What I saw, before I added the 'test' book which is created along with the User, is that creation of the first book entity works just fine, but whenever I create another one, the previous one is somehow removed from the Datastore and is replaced by the new one which I just created (I can tell because of the books' names). So I cannot create more than one book for the same user.
I tried to see if I somehow messed up persistence of the Book entity, and for that reason I added the 'TEST' book. The problem persists, only that now I have the first book ("TEST") and I keep replacing the second book on the list whenever I try to add a new one.
Again, this doesn't happen when I debug my application, only after I deploy.
I tried calling em.persist(book) before the call to em.merge(user), but that caused an exception saying the book's owner was already set when it was persisted and cannot be changed. I tried setting the relationship myself (like in this thread), but that caused a failed transaction when adding a book.
I'm not sure if its relevant, but the type of the 'books' column I see is datastore_types.Key.from_path, as in:
[datastore_types.Key.from_path(u'User', 9001L, u'Book', 1L, _app=u's~myapp'),
datastore_types.Key.from_path(u'User', 9001L, u'Book', 2001L, _app=u's~myapp')]
Any help would be appreciated,
Thank you!

Can you try below:
User user = /* ... */
Book book = new Book("A new book");
book.setOwner(user);
EntityManager em = /* ... */
EntityTransaction transaction = em.getTransaction();
try{
transaction.begin();
em.persist(user);
transaction.commit();
}finally{
if (transaction.isActive())
transaction.rollback();
em.close();
}

Related

Spring Data MongoDB #Transactional failure

Could someone please tell me why this spring transaction is not rolling back appropriately?
The error I get is this:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available
This is my repository with a save transaction that will intentionally fail:
#Repository
public class TransactionalRepository {
private final PlayerRepository playerRepository;
#Autowired
public TransactionalRepository(PlayerRepository playerRepository) {
this.playerRepository = playerRepository;
}
public Player saveSuccess(Player player) {
return playerRepository.save(player);
}
#Transactional
public Player saveFail(Player player) {
player.setName("FAIL"); // should not be saved in DB if transaction rollback is successful
player = playerRepository.save(player);
throw new IllegalStateException("intentionally fail transaction");
}
}
And here is the test:
#RunWith(SpringRunner.class)
#SpringBootTest
public class MongoTransactionApplicationTests {
#Autowired
public TransactionalRepository playerRepository;
#Test
public void contextLoads() {
Player player = new Player();
player.setId(UUID.randomUUID().toString());
final String PLAYER_NAME = "new-"+player.getId().subSequence(0,8);
player.setName(PLAYER_NAME);
player = playerRepository.saveSuccess(player);
try {
player = playerRepository.saveFail(player);
} catch (IllegalStateException e) {
// this is supposed to fail
}
Assert.assertEquals(PLAYER_NAME, player.getName());
}
}
Download all the code here if you want to see it run
Unlike other implementations the Spring Data MongoDB module does not by default register a PlatformTransactionManager if none is present. This is up to the users configuration, to avoid errors with non MongoDB 4.x servers as well as projects already using #Transactional along with a non MongoDB specific transaction manager implementation. Please refer to the reference documentation for details.
Just add a MongoTransactionManager to your configuration.
#Bean
MongoTransactionManager txManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
You might also want to check out the Spring Data Examples and have a look at the one for MongoDB transactions.

Spring data with LDAP (Active Directory) returns WILL NOT PERFORM in any writing operation

I'm currently trying to implement an simple CRUD integrated with Active Directory via LDAP, using Spring Data, for managing my internal users.
The problem is, while the reading works as expected, any writing on AD (creating or editing a user, for example) results in a generic error message, shown below:
[LDAP: error code 53 - 0000209A: SvcErr: DSID-031A107A, problem 5003 (WILL_NOT_PERFORM), data 0\n\u0000]; remaining name 'DC=company, DC=com'
The ldap connection is being made using LDAPS with an admin user. I can even work with the same credentials without any issues in a simple nodejs test application. So I'm probably making some mistake with Spring Data.
The relevant source code is attached below.
Entity class:
// Person.java (Entity model)
#Data
#Entry(
base = "ou=Employees,dc=company,dc=com",
objectClasses = {"person", "top"}
)
public class Person {
#Id
private Name dn;
#Attribute(name = "cn")
private String commonName;
#Attribute(name = "sAMAccountName")
private String accountName;
#Attribute(name = "userPrincipalName")
private String username;
#Attribute(name = "mail")
private String mail;
#Attribute(name = "userPassword")
private String password;
#Attribute(name = "description")
private String desc;
#Attribute(name = "memberOf")
private List<String> groups;
#Attribute(name = "company")
private String company;
#Attribute(name = "objectClass")
private List<String> objectClasses;
#Attribute(name = "objectCategory")
private String objectCategory;
}
Repository class:
// PersonRepository.java
#Repository
public interface PersonRepository extends LdapRepository<Person> {
Person findByMailIgnoreCase(String mail);
}
Service class:
#Service
public class UserService {
#Autowired
private PersonRepository personRepository;
/**
* Save the user at AD.
*
* #param username the user login name
* #param name the user name and surename
* #param companyExtName the company external name
* #param email the user email
* #param description the user description
* #return the newly created user
*/
public Person createPerson(String username, String name, String companyExtName,
String email, String description) {
final Person user = new Person();
user.setAccountName(username);
user.setCommonName(name);
user.setCompany(companyExtName);
user.setMail(email);
user.setUsername(email);
String tempPass = RandomStringUtils.randomAscii(10);
user.setPassword(digestSHA(tempPass));
user.setDn(LdapNameBuilder.newInstance("DC=company, DC=com")
.build());
List<String> objClasses = new ArrayList<>();
objClasses.add("person");
objClasses.add("top");
user.setObjectClasses(objClasses);
user.setObjectCategory("CN=Person,CN=Schema,CN=Configuration,DC=company,DC=com");
List<String> groups = new ArrayList<>();
groups.add("CN=Administrators,CN=Builtin,DC=company,DC=com");
user.setGroups(groups);
if (description != null && !description.isEmpty()) {
user.setDesc(description);
}
return personRepository.save(user);
}
/**
* Encodes the user password as it is used at Active Directory
*
* #param plain the plain text password
* #return the password hash
*/
private static String digestSHA(String plain) {
try {
MessageDigest digester = MessageDigest.getInstance("SHA-256");
digester.update(plain.getBytes());
return String.format("{SHA}%s", Base64.getEncoder().encodeToString(digester.digest()));
} catch (NoSuchAlgorithmException ex) {
return null;
}
}
The exception is thrown when I call personRepository.save(user);
As a addtional information, I've already tried a few variations of the code attached -- tried to remove almost all user data beforing saving it, different password encodings and hashing -- but the result is always the same.
Any help on this will be greatly appreciated.
Thanks!
EDIT:
Investigation indicates that the cause is probably something related with the way I'm sending my user DN.
Anyway, I'm still wrestling with this issue.
I was able to create/edit my Active Directory users with a workaround.
In my UserService, instead of using the Spring Data Ldap repository, I've used the LdapTemplate methods, like shown below.
// UserService.java
public void createPerson() {
Name userDn = LdapNameBuilder
.newInstance()
.add("ou", ou)
.add("cn", accountName)
.build();
DirContextAdapter context = new DirContextAdapter(userDn);
context.setAttributeValue("cn", accountName);
context.setAttributeValue("sn", accountName);
context.setAttributeValue("userPassword", digestSHA(password));
context.setAttributeValue("company", company);
context.setAttributeValue("description", desc);
context.setAttributeValue("mail", mail);
context.setAttributeValue("sAMAccountName", accountName);
context.setAttributeValue("userPrincipalName", username);
context.setAttributeValue("objectCategory", objectCategory);
context.setAttributeValues("objectClass", objectClasses.toArray());
DirContextAdapter context = user.getLdapContext("Users");
ldapTemplate.bind(context);
}
Since I used the same values for user creation with both Spring Data and LdapTemplate, my original issue is probably related to some treatment Spring does before sending the data to my Active Directory server.
Since the method above is currently working for me, I'll follow with it. When I have some spare time I'll go back to this to find out what I was doing wrong with Spring.
For future use, I believe it is related to memberOf attribute. This attribute must be set after the user is created, but it seems that Spring Data is filling this property with an empty string even if I set the attribute to null when creating the user.
Lots of articles says that it is because of a lack of SSL connection to a LDAP server.
Here are some links:
stackoverflow.com/questions/17290539
stackoverflow.com/questions/6797955
forum.spring.io/forum/spring-projects/data/ldap
community.oracle.com/thread/2177638

GAE + JPA + JsonMappingException + You have just attempted to access field

I'm new with JPA and GAE. I'm having some problems trying to retrive some entities to the front end. I copy mi entity code:
#Entity
public class User implements Serializable{
private static final long serialVersionUID = -7949567692618870100L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
private String userName;
private String email;
#OneToMany(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
private List<User> friends;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Credential Credential;
//getters & setters
}
The Credential entity only contains userName and pass:
#Entity
public class Credential implements Serializable{
private static final long serialVersionUID = -3277842132830057420L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
private String userName;
private String pass;
//getters and setters
}
I generate UserEndpoint with GAE Eclipse Plugin and then I added a method to simulate a Login, this is the method:
#ApiMethod(name = "CredentialLogin")
public User CredentialLogin(#Named("pass") String pass,#Named("userName") String userName){
EntityManager mgr = getEntityManager();
Query query = mgr.createQuery("SELECT FROM User u WHERE u.email = :email");
query.setParameter("email", userName);
User user = null;
try {
user = (User) query.getSingleResult();
if (!user.getCredential().getPass().equals(pass))
//INVALID LOGIN
}
finally {
mgr.close();
}
return user;
}
Debugging the code I get the correct User from the datastore. Doing "user.getCredential()" the Credential fetchs to the User entity. As the "friends" field has LAZY fetching when I return the User entity I will appear NULL (This is what I want).
After the return I'm getting this error:
"com.google.appengine.repackaged.org.codehaus.jackson.map.JsonMappingException: You have just attempted to access field "friends" yet this field was not detached when you detached the object. Either dont access this field, or detach it when detaching the object."
I don't want to put EAGER fetching to my "friends" attribute because the list could be really big.
I don't want to remove my "friends" getter and setter method, because without them I couldn't access to the field.
Somebody knows what should I do.
Thank you very much
You can change the enhancement process to call a listener instead of throwing an exception when an undetached field is accessed. See http://www.datanucleus.org/products/accessplatform_3_1/enhancer.html and "detachListener" option

GAE + JPA + updating entity is losing other entity relation

Hi I´m new using GAE and JPA, and I´m having some problems trying to update an entity. I copy next a code example:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key key;
private String userName;
#ManyToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
private Address address;
}
When I save on datastore a User instance it's store without problems.
After this, I retrive this instance from the datastore, then I set a new userName and try to update it.
public User updateUser(User user) {
EntityManager mgr = getEntityManager();
if (!containsUser(user)) {
throw new EntityNotFoundException("Object does not exist");
}
mgr.persist(user);
}
The update is performed, the new userName is stored in the dataStore, but as the Address field has FetchType.LAZY I'm losing the persisted value on the update.
How can I make an update of some fields without losing other values?
Try getting and saving the entity in the same method, otherwise you will lost the session and the manager will recognize the lazy attributes as null ones.
Hope it helps!

JPA One to Many Relation in Google App Engine

I have a one to many relationship (profile to message). I tried to save a message owned by a certain user/profile. What wrong with the code below?
public Message createMessage(Message msg, String recepient) {
EntityManager em = EMF.get().createEntityManager();
UserAccess access = new UserAccess();
Profile user = access.searchUser(recepient);
msg.setUser(user);
em.getTransaction().begin();
em.persist(msg);
em.getTransaction().commit();
em.close();
return msg;
}
search user method
public Profile searchUser(String displayName){
EntityManager em = EMF.get().createEntityManager();
Profile user;
try{
Query q = em.createNamedQuery("Profile.searchByDisplayName");
q.setParameter("displayName", displayName);
user = (Profile) q.getSingleResult();
} catch(javax.persistence.NoResultException e){
user = null;
}
em.close();
return user;
}
I encountered the error below:
java.lang.IllegalStateException: Primary key for object of type Profile is null.
Can it be because you do not have an "Id", a primary key, defined in your Profile Entity? Or is not set to any value when you created the specific profile?
Also, check if the relationships are defined properly between the Profile and Message entities.
jpa OneToMany & ManyToOne

Resources