diff --git a/spring-data-mongodb-cross-store/src/main/java/org/springframework/data/persistence/document/mongo/MongoDocumentBacking.aj b/spring-data-mongodb-cross-store/src/main/java/org/springframework/data/persistence/document/mongo/MongoDocumentBacking.aj index e98e8395c..41dd4ca12 100644 --- a/spring-data-mongodb-cross-store/src/main/java/org/springframework/data/persistence/document/mongo/MongoDocumentBacking.aj +++ b/spring-data-mongodb-cross-store/src/main/java/org/springframework/data/persistence/document/mongo/MongoDocumentBacking.aj @@ -1,179 +1,207 @@ -package org.springframework.data.persistence.document.mongo; - -import java.lang.reflect.Field; - -import javax.persistence.Transient; -import javax.persistence.Entity; -import javax.persistence.Id; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.reflect.FieldSignature; - -import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.data.document.mongodb.mapping.Document; - -import org.springframework.data.persistence.document.DocumentBacked; -import org.springframework.data.persistence.document.DocumentBackedTransactionSynchronization; -import org.springframework.data.persistence.ChangeSet; -import org.springframework.data.persistence.ChangeSetPersister; -import org.springframework.data.persistence.ChangeSetPersister.NotFoundException; -import org.springframework.data.persistence.HashMapChangeSet; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -/** - * Aspect to turn an object annotated with @Document into a persistent document - * using Mongo. - * - * @author Thomas Risberg - */ -public aspect MongoDocumentBacking { - - private static final Log LOGGER = LogFactory - .getLog(MongoDocumentBacking.class); - - // Aspect shared config - private ChangeSetPersister changeSetPersister; - - public void setChangeSetPersister( - ChangeSetPersister changeSetPersister) { - this.changeSetPersister = changeSetPersister; - } - - // ITD to introduce N state to Annotated objects - declare parents : (@Entity *) implements DocumentBacked; - -// declare @type: DocumentBacked+: @Configurable; - - declare @field: @Document * (@Entity+ *).*:@Transient; - declare @field: ChangeSet (DocumentBacked+).*:@Transient; - declare @field: ChangeSetPersister (DocumentBacked+).*:@Transient; - - // ------------------------------------------------------------------------- - // Advise user-defined constructors of ChangeSetBacked objects to create a new - // backing ChangeSet - // ------------------------------------------------------------------------- - pointcut arbitraryUserConstructorOfChangeSetBackedObject(DocumentBacked entity) : - execution((DocumentBacked+).new(..)) && - !execution((DocumentBacked+).new(ChangeSet)) && - this(entity); - - pointcut finderConstructorOfChangeSetBackedObject(DocumentBacked entity, - ChangeSet cs) : - execution((DocumentBacked+).new(ChangeSet)) && - this(entity) && - args(cs); - - protected pointcut entityFieldGet(DocumentBacked entity) : - get(@Document * DocumentBacked+.*) && - this(entity) && - !get(* DocumentBacked.*); - - protected pointcut entityFieldSet(DocumentBacked entity, Object newVal) : - set(@Document * DocumentBacked+.*) && - this(entity) && - args(newVal) && - !set(* DocumentBacked.*); - -// protected pointcut entityIdSet(DocumentBacked entity, Object newVal) : -// set(@Id * DocumentBacked+.*) && -// this(entity) && -// args(newVal) && -// !set(* DocumentBacked.*); - - before(DocumentBacked entity) : arbitraryUserConstructorOfChangeSetBackedObject(entity) { - LOGGER - .debug("User-defined constructor called on DocumentBacked object of class " - + entity.getClass()); - entity.itdChangeSetPersister = changeSetPersister; - // Populate all properties - ChangeSet changeSet = new HashMapChangeSet(); - // changeSetManager.populateChangeSet(changeSet, entity); - entity.setChangeSet(changeSet); - if (!TransactionSynchronizationManager.isSynchronizationActive()) { - throw new InvalidDataAccessResourceUsageException( - "No transaction synchronization is active"); - } - TransactionSynchronizationManager - .registerSynchronization(new DocumentBackedTransactionSynchronization( - changeSetPersister, entity)); - } - - // ------------------------------------------------------------------------- - // ChangeSet-related mixins - // ------------------------------------------------------------------------- - // Introduced field - private ChangeSet DocumentBacked.changeSet; - - private ChangeSetPersister DocumentBacked.itdChangeSetPersister; - - public void DocumentBacked.setChangeSet(ChangeSet cs) { - this.changeSet = cs; - } - - public ChangeSet DocumentBacked.getChangeSet() { - return changeSet; - } - - // Flush the entity state to the persistent store - public void DocumentBacked.flush() { - Object id = itdChangeSetPersister.getPersistentId(this, this.changeSet); - itdChangeSetPersister.persistState(this, this.changeSet); - } - - public Object DocumentBacked.get_persistent_id() { - return itdChangeSetPersister.getPersistentId(this, this.changeSet); - } - - /** - * delegates field reads to the state accessors instance - */ - Object around(DocumentBacked entity): entityFieldGet(entity) { - Field f = field(thisJoinPoint); - String propName = f.getName(); - LOGGER.trace("GET " + f + " -> ChangeSet value property [" + propName - + "] using: " + entity.getChangeSet()); - if (entity.getChangeSet().getValues().get(propName) == null) { - try { - this.changeSetPersister.getPersistentState(entity.getClass(), - entity.get_persistent_id(), entity.getChangeSet()); - } catch (NotFoundException e) { - } - } - Object fValue = entity.getChangeSet().getValues().get(propName); - if (fValue != null) { - return fValue; - } - return proceed(entity); - } - - /** - * delegates field writes to the state accessors instance - */ - Object around(DocumentBacked entity, Object newVal) : entityFieldSet(entity, newVal) { - Field f = field(thisJoinPoint); - String propName = f.getName(); - LOGGER.trace("SET " + f + " -> ChangeSet number value property [" + propName - + "] with value=[" + newVal + "]"); - entity.getChangeSet().set(propName, newVal); - return proceed(entity, newVal); - } - -// /** -// * delegates field writes to the state accessors instance -// */ -// Object around(DocumentBacked entity, Object newVal) : entityIdSet(entity, newVal) { -// Field f = field(thisJoinPoint); -// String propName = f.getName(); -// LOGGER.trace("SET @Id -> ChangeSet @Id property [" + propName -// + "] with value=[" + newVal + "]"); -// entity.getChangeSet().set("_id", newVal); -// return proceed(entity, newVal); -// } - - Field field(JoinPoint joinPoint) { - FieldSignature fieldSignature = (FieldSignature) joinPoint.getSignature(); - return fieldSignature.getField(); - } -} +package org.springframework.data.persistence.document.mongo; + +import java.lang.reflect.Field; + +import javax.persistence.Transient; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.FieldSignature; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.document.mongodb.mapping.Document; + +import org.springframework.data.persistence.document.DocumentBacked; +import org.springframework.data.persistence.document.DocumentBackedTransactionSynchronization; +import org.springframework.data.persistence.ChangeSet; +import org.springframework.data.persistence.ChangeSetPersister; +import org.springframework.data.persistence.ChangeSetPersister.NotFoundException; +import org.springframework.data.persistence.HashMapChangeSet; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Aspect to turn an object annotated with @Document into a persistent document + * using Mongo. + * + * @author Thomas Risberg + */ +public aspect MongoDocumentBacking { + + private static final Log LOGGER = LogFactory + .getLog(MongoDocumentBacking.class); + + // Aspect shared config + private ChangeSetPersister changeSetPersister; + + public void setChangeSetPersister( + ChangeSetPersister changeSetPersister) { + this.changeSetPersister = changeSetPersister; + } + + // ITD to introduce N state to Annotated objects + declare parents : (@Entity *) implements DocumentBacked; + + // The annotated fields that will be persisted in MongoDB rather than with JPA + declare @field: @Document * (@Entity+ *).*:@Transient; + + // ------------------------------------------------------------------------- + // Advise user-defined constructors of ChangeSetBacked objects to create a new + // backing ChangeSet + // ------------------------------------------------------------------------- + pointcut arbitraryUserConstructorOfChangeSetBackedObject(DocumentBacked entity) : + execution((DocumentBacked+).new(..)) && + !execution((DocumentBacked+).new(ChangeSet)) && + this(entity); + + pointcut finderConstructorOfChangeSetBackedObject(DocumentBacked entity, + ChangeSet cs) : + execution((DocumentBacked+).new(ChangeSet)) && + this(entity) && + args(cs); + + protected pointcut entityFieldGet(DocumentBacked entity) : + get(@Document * DocumentBacked+.*) && + this(entity) && + !get(* DocumentBacked.*); + + protected pointcut entityFieldSet(DocumentBacked entity, Object newVal) : + set(@Document * DocumentBacked+.*) && + this(entity) && + args(newVal) && + !set(* DocumentBacked.*); + +// protected pointcut entityIdSet(DocumentBacked entity, Object newVal) : +// set(@Id * DocumentBacked+.*) && +// this(entity) && +// args(newVal) && +// !set(* DocumentBacked.*); + + before(DocumentBacked entity) : arbitraryUserConstructorOfChangeSetBackedObject(entity) { + LOGGER + .debug("User-defined constructor called on DocumentBacked object of class " + + entity.getClass()); + // Populate all ITD fields + entity.setChangeSet(new HashMapChangeSet()); + entity.itdChangeSetPersister = changeSetPersister; + entity.itdTransactionSynchronization = + new DocumentBackedTransactionSynchronization(changeSetPersister, entity); + registerTransactionSynchronization(entity); + } + + private static void registerTransactionSynchronization(DocumentBacked entity) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + if (!TransactionSynchronizationManager.getSynchronizations().contains(entity.itdTransactionSynchronization)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Adding transaction synchronization for " + entity.getClass()); + } + TransactionSynchronizationManager.registerSynchronization(entity.itdTransactionSynchronization); + } + else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Transaction synchronization already active for " + entity.getClass()); + } + } + } + else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Transaction syncronization is not active for " + entity.getClass()); + } + } + } + + // ------------------------------------------------------------------------- + // ChangeSet-related mixins + // ------------------------------------------------------------------------- + // Introduced field + @Transient private ChangeSet DocumentBacked.changeSet; + + @Transient private ChangeSetPersister DocumentBacked.itdChangeSetPersister; + + @Transient private DocumentBackedTransactionSynchronization DocumentBacked.itdTransactionSynchronization; + + public void DocumentBacked.setChangeSet(ChangeSet cs) { + this.changeSet = cs; + } + + public ChangeSet DocumentBacked.getChangeSet() { + return changeSet; + } + + // Flush the entity state to the persistent store + public void DocumentBacked.flush() { + Object id = itdChangeSetPersister.getPersistentId(this, this.changeSet); + itdChangeSetPersister.persistState(this, this.changeSet); + } + + public Object DocumentBacked.get_persistent_id() { + return itdChangeSetPersister.getPersistentId(this, this.changeSet); + } + + // lifecycle methods + @javax.persistence.PrePersist public void DocumentBacked.prePersist() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JPA lifecycle called PrePersist: " + this.getClass().getName() + " :: " + this.get_persistent_id()); + } + registerTransactionSynchronization(this); + } + @javax.persistence.PreUpdate public void DocumentBacked.preUpdate() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JPA lifecycle called PreUpdate: " + this.getClass().getName() + " :: " + this.get_persistent_id()); + } + registerTransactionSynchronization(this); + } + @javax.persistence.PreRemove public void DocumentBacked.preRemove() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JPA lifecycle called PreRemove: " + this.getClass().getName() + " :: " + this.get_persistent_id()); + } + registerTransactionSynchronization(this); + } + @javax.persistence.PostLoad public void DocumentBacked.postLoad() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("JPA lifecycle called PostLoad: " + this.getClass().getName() + " :: " + this.get_persistent_id()); + } + registerTransactionSynchronization(this); + } + + /** + * delegates field reads to the state accessors instance + */ + Object around(DocumentBacked entity): entityFieldGet(entity) { + Field f = field(thisJoinPoint); + String propName = f.getName(); + LOGGER.trace("GET " + f + " -> ChangeSet value property [" + propName + + "] using: " + entity.getChangeSet()); + if (entity.getChangeSet().getValues().get(propName) == null) { + try { + this.changeSetPersister.getPersistentState(entity.getClass(), + entity.get_persistent_id(), entity.getChangeSet()); + } catch (NotFoundException e) { + } + } + Object fValue = entity.getChangeSet().getValues().get(propName); + if (fValue != null) { + return fValue; + } + return proceed(entity); + } + + /** + * delegates field writes to the state accessors instance + */ + Object around(DocumentBacked entity, Object newVal) : entityFieldSet(entity, newVal) { + Field f = field(thisJoinPoint); + String propName = f.getName(); + LOGGER.trace("SET " + f + " -> ChangeSet number value property [" + propName + + "] with value=[" + newVal + "]"); + entity.getChangeSet().set(propName, newVal); + return proceed(entity, newVal); + } + + Field field(JoinPoint joinPoint) { + FieldSignature fieldSignature = (FieldSignature) joinPoint.getSignature(); + return fieldSignature.getField(); + } +} diff --git a/spring-data-mongodb-cross-store/src/test/java/org/springframework/data/document/persistence/CrossStoreMongoTests.java b/spring-data-mongodb-cross-store/src/test/java/org/springframework/data/document/persistence/CrossStoreMongoTests.java index b7fc0036e..b52a4f17e 100644 --- a/spring-data-mongodb-cross-store/src/test/java/org/springframework/data/document/persistence/CrossStoreMongoTests.java +++ b/spring-data-mongodb-cross-store/src/test/java/org/springframework/data/document/persistence/CrossStoreMongoTests.java @@ -13,7 +13,11 @@ import org.springframework.persistence.document.test.Resume; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; import com.mongodb.DBCollection; import com.mongodb.Mongo; @@ -30,6 +34,9 @@ public class CrossStoreMongoTests { private EntityManager entityManager; + @Autowired + private PlatformTransactionManager transactionManager; + @PersistenceContext public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; @@ -71,6 +78,7 @@ public class CrossStoreMongoTests { Assert.assertEquals("DiMark, DBA, 1990-2000" + "; " + "VMware, Developer, 2007-", found.getResume().getJobs()); found.getResume().addJob("SpringDeveloper.com, Consultant, 2005-2006"); + found.setAge(44); } @Test @@ -87,4 +95,22 @@ public class CrossStoreMongoTests { + "VMware, Developer, 2007-" + "; " + "SpringDeveloper.com, Consultant, 2005-2006", found.getResume().getJobs()); } + + @Test + public void testMergeJpaEntityWithMongoDocument() { + TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); + final Person found = entityManager.find(Person.class, 1L); + found.setAge(77); + found.getResume().addJob("TargetRx, Developer, 2000-2005"); + txTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + entityManager.merge(found); + return null; + } + }); + final Person updated = entityManager.find(Person.class, 1L); + // assert that the new values are in respective DBs + // TODO: during merge we lose the changeset since JPA creates a new persistent instance - + // we need to move the existing changeset over somehow + } } diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index a1980c35b..f2ad6ff58 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ Spring Data MongoDB Support - 2.3 + 2.4 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingTests.java index 2d9beeeee..d92d40469 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingTests.java @@ -24,11 +24,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.junit.After; +import com.mongodb.Mongo; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.data.document.mongodb.MongoDbUtils; import org.springframework.data.document.mongodb.MongoTemplate; import org.springframework.data.document.mongodb.query.Criteria; import org.springframework.data.document.mongodb.query.Query; @@ -38,28 +41,32 @@ import org.springframework.data.document.mongodb.query.Query; */ public class MappingTests { + private static final Logger LOGGER = LoggerFactory.getLogger(MongoDbUtils.class); + ApplicationContext applicationContext; MongoTemplate template; + MongoMappingContext mappingContext; @Before - public void setUp() throws InterruptedException { + public void setUp() throws Exception { + Mongo mongo = new Mongo(); + mongo.getDB("database").getCollection("person").drop(); applicationContext = new ClassPathXmlApplicationContext("/mapping.xml"); template = applicationContext.getBean(MongoTemplate.class); - } - - @After - public void tearDown() { - template.dropCollection("person"); - template.dropCollection("account"); + mappingContext = applicationContext.getBean(MongoMappingContext.class); } @Test - public void testPersonPojo() { + public void testPersonPojo() throws Exception { + LOGGER.info("about to create new personpojo"); PersonPojo p = new PersonPojo(12345, "Person", "Pojo"); + LOGGER.info("about to insert"); template.insert(p); + LOGGER.info("done inserting"); assertNotNull(p.getId()); - List result = template.find(new Query(Criteria.where("ssn").is(12345)), PersonPojo.class); + List result = template.find( + new Query(Criteria.where("ssn").is(12345)), PersonPojo.class); assertThat(result.size(), is(1)); assertThat(result.get(0).getSsn(), is(12345)); } @@ -69,12 +76,13 @@ public class MappingTests { PersonCustomIdName p = new PersonCustomIdName(123456, "Custom", "Id"); template.insert(p); - List result = template.find(new Query(Criteria.where("ssn").is(123456)), PersonCustomIdName.class); + List result = template.find( + new Query(Criteria.where("ssn").is(123456)), PersonCustomIdName.class); assertThat(result.size(), is(1)); assertNotNull(result.get(0).getCustomId()); } - @Test + @Test public void testPersonMapProperty() { PersonMapProperty p = new PersonMapProperty(1234567, "Map", "Property"); Map accounts = new HashMap(); @@ -89,10 +97,12 @@ public class MappingTests { template.insert(p); assertNotNull(p.getId()); - List result = template.find(new Query(Criteria.where("ssn").is(1234567)), PersonMapProperty.class); + List result = template.find( + new Query(Criteria.where("ssn").is(1234567)), PersonMapProperty.class); assertThat(result.size(), is(1)); assertThat(result.get(0).getAccounts().size(), is(2)); - assertThat(result.get(0).getAccounts().get("checking").getBalance(), is(1000.0f)); + assertThat(result.get(0).getAccounts().get("checking").getBalance(), + is(1000.0f)); } @Test @@ -125,7 +135,8 @@ public class MappingTests { assertNotNull(p.getId()); - List result = template.find(new Query(Criteria.where("ssn").is(123456789)), Person.class); + List result = template.find( + new Query(Criteria.where("ssn").is(123456789)), Person.class); assertThat(result.size(), is(1)); assertThat(result.get(0).getAddress().getCountry(), is("USA")); assertThat(result.get(0).getAccounts(), notNullValue()); @@ -146,13 +157,9 @@ public class MappingTests { template.insert("person", p1); template.insert("person", p2); - List result = template.find(new Query(Criteria.where("ssn").is(1234567890)), Person.class); + List result = template.find( + new Query(Criteria.where("ssn").is(1234567890)), Person.class); assertThat(result.size(), is(1)); } - @Test - public void testEvents() { - - } - } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/query/QueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/query/QueryTests.java index 76426e4b8..ddec63659 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/query/QueryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/query/QueryTests.java @@ -29,13 +29,6 @@ public class QueryTests { Assert.assertEquals(expected, q.getQueryObject().toString()); } - @Test - public void testSimpleQueryWithChainedCriteria() { - Query q = new Query(where("name").is("Thomas").and("age").lt(80)); - String expected = "{ \"name\" : \"Thomas\" , \"age\" : { \"$lt\" : 80}}"; - Assert.assertEquals(expected, q.getQueryObject().toString()); - } - @Test public void testQueryWithNot() { Query q = new Query(where("name").is("Thomas")).and(where("age").not().mod(10, 0)); @@ -81,6 +74,22 @@ public class QueryTests { Assert.assertEquals(expected, q.getQueryObject().toString()); } + @Test + public void testSimpleQueryWithChainedCriteria() { + Query q = new Query(where("name").is("Thomas").and("age").lt(80)); + String expected = "{ \"name\" : \"Thomas\" , \"age\" : { \"$lt\" : 80}}"; + Assert.assertEquals(expected, q.getQueryObject().toString()); + } + + @Test + public void testComplexQueryWithMultipleChainedCriteria() { + Query q1 = new Query(where("name").regex("^T.*").and("age").gt(20).lt(80).and("city").in("Stockholm", "London", "New York")); + Query q2 = new Query(where("name").regex("^T.*").and("age").gt(20).lt(80)).and(where("city").in("Stockholm", "London", "New York")); + Assert.assertEquals(q1.getQueryObject().toString(), q2.getQueryObject().toString()); + Query q3 = new Query(where("name").regex("^T.*")).and(where("age").gt(20).lt(80)).and(where("city").in("Stockholm", "London", "New York")); + Assert.assertEquals(q1.getQueryObject().toString(), q3.getQueryObject().toString()); + } + @Test public void testQueryWithElemMatch() { Query q = new Query(where("openingHours").elemMatch(where("dayOfWeek").is("Monday").and("open").lte("1800")));