#297 - Add sample for fluent MongoDB API.
This commit is contained in:
committed by
Oliver Gierke
parent
7a557bfd72
commit
e7123f2647
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@Data
|
||||
class Human {
|
||||
|
||||
final String firstname;
|
||||
final String lastname;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString
|
||||
class Jedi extends SWCharacter {
|
||||
|
||||
private final String lastname;
|
||||
|
||||
Jedi(String name, String lastname) {
|
||||
|
||||
super(name);
|
||||
this.lastname = lastname;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@Data
|
||||
public class Planet {
|
||||
|
||||
final @Id String name;
|
||||
final @GeoSpatialIndexed Point coordinates;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@Document(collection = "star-wars")
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode(of = "name")
|
||||
class SWCharacter {
|
||||
|
||||
private @Id String id;
|
||||
private @Field("firstname") String name;
|
||||
private Planet homePlanet;
|
||||
|
||||
SWCharacter(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface Sith {
|
||||
|
||||
@Value("#{target.name + ' ' + target.lastname}")
|
||||
String getName();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
|
||||
import org.springframework.data.mongodb.core.index.GeospatialIndex;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class ApplicationConfiguration {
|
||||
|
||||
static final String COLLECTION = "star-wars";
|
||||
|
||||
@Bean
|
||||
CommandLineRunner init(MongoTemplate template) {
|
||||
|
||||
return (args) -> {
|
||||
|
||||
if (template.collectionExists(COLLECTION)) {
|
||||
template.dropCollection(COLLECTION);
|
||||
}
|
||||
|
||||
GeospatialIndex index = new GeospatialIndex("homePlanet.coordinates") //
|
||||
.typed(GeoSpatialIndexType.GEO_2DSPHERE) //
|
||||
.named("planet-coordinate-idx");
|
||||
|
||||
template.createCollection(COLLECTION);
|
||||
template.indexOps(SWCharacter.class).ensureIndex(index);
|
||||
|
||||
Planet alderaan = new Planet("alderaan", new Point(-73.9667, 40.78));
|
||||
Planet stewjon = new Planet("stewjon", new Point(-73.9836, 40.7538));
|
||||
Planet tatooine = new Planet("tatooine", new Point(-73.9928, 40.7193));
|
||||
|
||||
Jedi anakin = new Jedi("anakin", "skywalker");
|
||||
anakin.setHomePlanet(tatooine);
|
||||
|
||||
Jedi luke = new Jedi("luke", "skywalker");
|
||||
luke.setHomePlanet(tatooine);
|
||||
|
||||
Jedi leia = new Jedi("leia", "organa");
|
||||
leia.setHomePlanet(alderaan);
|
||||
|
||||
Jedi obiWan = new Jedi("obi-wan", "kenobi");
|
||||
obiWan.setHomePlanet(stewjon);
|
||||
|
||||
Human han = new Human("han", "solo");
|
||||
|
||||
template.save(anakin, COLLECTION);
|
||||
template.save(luke, COLLECTION);
|
||||
template.save(leia, COLLECTION);
|
||||
template.save(obiWan, COLLECTION);
|
||||
template.save(han, COLLECTION);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package example.springdata.mongodb.fluent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.geo.Metrics.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
import static org.springframework.data.mongodb.core.query.Update.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
|
||||
import org.springframework.data.mongodb.core.FluentMongoOperations;
|
||||
import org.springframework.data.mongodb.core.MongoOperations;
|
||||
import org.springframework.data.mongodb.core.query.NearQuery;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import com.mongodb.client.result.UpdateResult;
|
||||
|
||||
/**
|
||||
* Some tests showing usage and capabilities of {@link FluentMongoOperations}. <br />
|
||||
* Please note index and testdata setup in {@link ApplicationConfiguration} with
|
||||
* <dl>
|
||||
* <dt>3 Planets</dt>
|
||||
* <dd>alderaan, stewjon, tatooine</dd>
|
||||
* <dt>4 Jedis</dt>
|
||||
* <dd>anakin, leia, luke, obi-wan</dd>
|
||||
* <dt>1 Human</dt>
|
||||
* <dd>han</dd>
|
||||
* </dl>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
public class FluentMongoApiTests {
|
||||
|
||||
@Autowired FluentMongoOperations mongoOps;
|
||||
|
||||
/**
|
||||
* A predefined, reusable lookup method.
|
||||
*/
|
||||
TerminatingFind<Jedi> findLuke;
|
||||
|
||||
final NearQuery alderaanWithin3Parsecs = NearQuery.near(-73.9667, 40.78).maxDistance(new Distance(3, MILES))
|
||||
.spherical(true);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
findLuke = mongoOps.query(SWCharacter.class) // SWCharacter does only define the collection, id and name
|
||||
.as(Jedi.class) // so we use Jedi as the desired return type to also map "lastname"
|
||||
.matching(query(where("name").is("luke"))); // for all with a matching "name" that maps to "firstname".
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the predefined lookup method {@link #findLuke} to query the {@literal star-wars} collection derived from
|
||||
* {@link SWCharacter}. The originating domain type {@link SWCharacter} is used for mapping the query to
|
||||
* {@link org.bson.Document}s which results in <code>{ firstname : "luke" }</code> as {@link SWCharacter#name} is
|
||||
* mapped to {@literal firstname} via {@link org.springframework.data.mongodb.core.mapping.Field}. <br />
|
||||
* For return type mapping {@link Jedi} is used which allows also reading back the {@link Jedi#lastname} next to the
|
||||
* {@literal firstname}. <br />
|
||||
* The samples below would read something like the following using classic {@link MongoOperations}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* This is not possible with classic MongoOperations.
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
public void usePredefinedFinder() {
|
||||
assertThat(findLuke.one()).contains(new Jedi("luke", "skywalker"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Using {@code as(java.lang.Class)} allows to not only map resulting {@link org.bson.Document}s into a given class
|
||||
* but also creating interfaces, limiting access to properties. <br />
|
||||
* {@link Sith#getName()} defines a SpEL expression for {@code firstname + " " + lastname} of the target object via
|
||||
* {@link org.springframework.beans.factory.annotation.Value}.<br />
|
||||
* The samples below would read something like the following using classic {@link MongoOperations}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* This is not possible with classic MongoOperations.
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
public void fetchInterfaceProjection() {
|
||||
|
||||
Sith anakin = mongoOps.query(SWCharacter.class) // SWCharacter does only define the collection, id and name
|
||||
.as(Sith.class) // use an interface as return type to create a projection
|
||||
.matching(query(where("firstname").is("anakin"))) // so properties are taken as is
|
||||
.oneValue();
|
||||
|
||||
assertThat(anakin.getName()).isEqualTo("anakin skywalker");
|
||||
}
|
||||
|
||||
/**
|
||||
* The difference between the terminating methods {@link TerminatingFind#first()} and {@link TerminatingFind#one()} is
|
||||
* that {@code fist()} returns the very first entry found or none at all. Therefore the query execution is limited to
|
||||
* exactly {@code 1} element via {@link com.mongodb.client.FindIterable#limit(int)}; <br />
|
||||
* {@code one()} on the other hand makes a hard assumption on cardinality of the query result and expects exactly one
|
||||
* element to match the query or none at all. If more than one match is found {@code one()} raises an
|
||||
* {@link IncorrectResultSizeDataAccessException}. Under the hood, the query execution is limited to {@code 2}
|
||||
* elements. <br />
|
||||
* The samples below would read something like the following using classic {@link MongoOperations}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* // find first();
|
||||
* return template.find(query(where("lastname").is("skywalker")).limit(1), SWCharacter.class, "star-wars")
|
||||
* .iterator()
|
||||
* .next();
|
||||
*
|
||||
* // find one();
|
||||
* List result = template.find(query(where("lastname").is("skywalker")).limit(2), SWCharacter.class, "star-wars");
|
||||
* if(result.size() > 1) {
|
||||
* throw new IncorrectResultSize...
|
||||
* }
|
||||
* return result.iterator().next();
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
public void queryFirstVsOne() {
|
||||
|
||||
mongoOps.query(SWCharacter.class) // SWCharacter does only define the collection, id and name
|
||||
.matching(query(where("lastname").is("skywalker"))) // so properties are taken as is
|
||||
.first(); // and we'll just get the first whatever entry if there is.
|
||||
|
||||
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) //
|
||||
.isThrownBy(() -> {
|
||||
|
||||
mongoOps.query(SWCharacter.class) // SWCharacter does only define the collection, id and name
|
||||
.matching(query(where("lastname").is("skywalker"))) // so properties are taken as is
|
||||
.one(); // and we expect there is only one entry matching. Not more! But obviously there is.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoNear operations can be executed via {@code near} which needs to be given a {@link NearQuery}. By doing so the
|
||||
* API will from then on only provide methods suitable for executing a {@literal near} query. In this case options
|
||||
* like {@code first()} or {@code one()} are no longer available. Even the return type for {@code all()} switches from
|
||||
* {@link List} to {@link GeoResults}. <br />
|
||||
* Still it is possible to map the {@code content} of a single {@link org.springframework.data.geo.GeoResult} to a
|
||||
* different type using {@code as(Class)}. <br />
|
||||
* The samples below would read something like the following using classic {@link MongoOperations}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* template.geoNear(alderaanWithin3Parsecs, SWCharacter.class, "star-wars", Jedi.class);
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
public void geoNearQuery() {
|
||||
|
||||
GeoResults<Jedi> results = mongoOps.query(SWCharacter.class) // SWCharacter defines collection, id and name
|
||||
.as(Jedi.class) // but we want to map the results to Jedi
|
||||
.near(alderaanWithin3Parsecs) // and find those with home planet near alderaan
|
||||
.all();
|
||||
|
||||
assertThat(results.getContent()).hasSize(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* In this case {@link Human} does not have an explicit {@link org.springframework.data.mongodb.core.mapping.Document}
|
||||
* annotation which results in {@literal human} as the default collection name. Via {@code inCollection(String)} it is
|
||||
* possible to override the default and set it to whatever collection should be queried. <br />
|
||||
* As there is no different return type declared via {@code as(String)}, the originating domain type {@link Human} is
|
||||
* used for both query and result mapping. <br />
|
||||
* The sample below would read something like the following using classic {@link MongoOperations}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* template.find(query(where("lastname").is("skywalker")), Human.class, "star-wars");
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
public void querySpecificCollection() {
|
||||
|
||||
List<Human> skywalkers = mongoOps.query(Human.class) // Human does not define a collection via @Document
|
||||
.inCollection("star-wars") // so we set an explicit collection name
|
||||
.matching(query(where("lastname").is("skywalker"))) // to find all documents with a matching "lastname"
|
||||
.all();
|
||||
|
||||
assertThat(skywalkers).containsExactlyInAnyOrder(new Human("anakin", "skywalker"), new Human("luke", "skywalker"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple insert operation adding a new {@link Jedi} to the {@literal star-wars} collection.
|
||||
*/
|
||||
@Test
|
||||
public void justInsertOne() {
|
||||
|
||||
SWCharacter chewbacca = new SWCharacter("Chewbacca");
|
||||
|
||||
mongoOps.insert(SWCharacter.class).one(chewbacca);
|
||||
|
||||
assertThat(chewbacca.getId()).isNotBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link FluentMongoOperations#update(Class)} defines the entry point for performing modifications on potentially
|
||||
* already existing document without replacing the entire document. The domain type is used for both mapping the query
|
||||
* identifying the potential update candidates as well as the property mapping for the
|
||||
* {@link org.springframework.data.mongodb.core.query.Update} itself. <br />
|
||||
* The sample below would read something like the following using classic {@link MongoOperations}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* template.upsert(query(where("lastname").is("windu")), update("name", "mence"), Jedi.class, "star-wars");
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
public void updateAndUpsert() {
|
||||
|
||||
UpdateResult result = mongoOps.update(Jedi.class) // Jedi defines the collection and field mapping
|
||||
.matching(query(where("lastname").is("windu"))) // so "last" maps to "lastname".
|
||||
.apply(update("name", "mence")) // We'll update the "firstname" to "mence"
|
||||
.upsert(); // and add a new document if it does not exist already.
|
||||
|
||||
assertThat(result.getMatchedCount()).isEqualTo(0);
|
||||
assertThat(result.getUpsertedId()).isNotNull();
|
||||
|
||||
assertThat(
|
||||
mongoOps.query(Human.class).inCollection("star-wars").matching(query(where("firstname").is("mence"))).one())
|
||||
.contains(new Human("mence", "windu"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user