Designing mongodb schema with searchable nested arrays. - mongodb

I'm new to Mongodb coming from relational databases and I'd also like to point out I'm using SpringBoot with JPA. If I were to build an automotive classified site where I would have thousands of Users and 100s of thousands of listings, how would I go about setting up the schema? I've read some articles that say normalizing nosql data is bad practices.
Anyhow lets say we have the following structure.
User
id
name
email
Cars
id
make
model
year
I would need to be able to list many cars with the User and what i've seen in my examples is it creates a nested array of cars within User. This would work great for user accounts where I'd like to provide the user with all their cars
Where I get a bit confused is with the cars. The cars need to be able to be searched very quickly and would not need the user info right away. In a sql db I would typically do a search against the cars (year, make, model) and grab the user later on if I needed it.
In mongodb, do you create a User document that contains a nested car array? or do you somehow create 2 documents that are both automatically maintained and search against the car document for performance reasons?
Sample code
#Document(collection = "person")
public class Person {
#Id
private String id;
private String firstName;
private String lastName;
// #DBRef(lazy = true)
private List<Listing> listings;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
#Document(collection = "listing")
public class Listing {
#Id
public String id;
public String year;
public String make;
public String model;
public String trim;
public Listing(String year, String make, String model, String trim) {
this.year = year;
this.make = make;
this.model = model;
this.trim = trim;
}
}
#Override
public void run(String... args) throws Exception {
repository.deleteAll();
List<Listing> listings = new ArrayList<>();
Listing listing = new Listing("2008", "Ford", "Focus", "SE");
//listingRepository.save(listing);
listings.add(listing);
Person person = new Person("Alice", "Smith");
person.setListings(listings);
// save a couple of customers
repository.save(person);
person = new Person("Bob", "Smith");
listings = new ArrayList<>();
listings.add(new Listing("2018", "Chrysler", "300", "S"));
person.setListings(listings);
repository.save(person);
// fetch all customers
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Person _person : repository.findAll()) {
System.out.println(_person);
}
System.out.println();
// fetch an individual customer
System.out.println("Person found with findByFirstName('Alice'):");
System.out.println("--------------------------------");
System.out.println(repository.findByFirstName("Alice"));
System.out.println("Persons found with findByLastName('Smith'):");
System.out.println("--------------------------------");
for (Person _person : repository.findByLastName("Smith")) {
System.out.println(_person);
}
List<Listing> _listings = listingRepository.findAll();
System.out.println("listings " + _listings.size());
_listings.forEach(v -> {
System.out.println(v.toString());
});
}

Going by your entity model,I think what you are looking for is analogous to Many to Many/One to Many relationship in a relational database. So you can go for One way Embedding or Two way Embedding in MongoDb.
For One way embedding, You can create a Car collection like below:
db.carCollection.insertMany([{
_id:1,
make: 'porcha',
model:'qwerty',
year:'2018'
},
{
_id:2,
make: 'ferrara',
model:'uiop',
year:'2018'
}])
You can then go on to create user collection as below:
db.userCollection.insert({
_id:1,
user:'Tom',
email:'tom#tom.com',
car_ids:[1,2]
})
The car_ids is an array which will hold the ids of cars that belong to the user.
You can fetch the cars belonging to an user as(using findOne to fetch the user. Search parameter should be an unique id. I am considering email to be unique here.Ideally it should be user's id) :
var user=db.userCollection.findOne({email:'tom#tom.com'})
db.carCollection.find({_id:{$in:user.car_ids}})
This will fetch you all the cars per user
For fetching cars only you can simply do:
db.carCollection.find({})
For Two way embedding you can have similar array (as in user collection) inside cars collection so that each car can be identified to its user.

Related

How can i design a Model of a document contains more than one collection in Mongodb

I am novice in mongodb.
I have a mongodb and i have a table / document(Ex: sampleTable).
I know how to retrieve data from this single table.
I used below code to retrieve data and working fine.
#Document(collection="sampleTable")
public class sampleMapping {
private String id;
private String strClassName;
public sampleMapping () {}
public sampleMapping (String strClassName) {
super();
this.strClassName = strClassName;
}
public String getStrClassName() {
return strClassName;
}
public void setStrClassName(String strClassName) {
this.strClassName = strClassName;
}
}
But now, i wanted to retrieve data from more than one table. How can i do that?
Say, i need to retrieve table1 and table2.
How can i update the above code to get data from multiple collections?

Morphia (MongoDB) Datastore "get" returns null

So I started working with Morphia and I'm encountering a weird problem.
Here's my entity class
#Entity("movies")
#Indexes(#Index(value = "Name", fields = #Field("Name")))
#Converters(LocalDateConverter.class)
public class MovieDetails implements Serializable
{
#Id
public String Id;
public String Name;
public String Description;
public String ImageName;
public LocalDate ReleaseDate;
public String Director;
public int Duration;
public String Genres;
public String Actors;
public MovieDetails()
{
}
public MovieDetails(String id, String name, String description, String imageName, String director, String actors, LocalDate releaseDate, String genres, int duration)
{
this (name, description, imageName, director, actors, releaseDate, genres, duration);
Id = id;
}
public MovieDetails(String name, String description, String imageName, String director, String actors, LocalDate releaseDate, String genres, int duration)
{
Name = name;
Description = description;
ImageName = imageName;
Director = director;
Actors = actors;
ReleaseDate = releaseDate;
Genres = genres;
Duration = duration;
}
}
Here's my little test:
final Morphia morphia = new Morphia();
// tell Morphia where to find your classes
// can be called multiple times with different packages or classes
morphia.mapPackage("nimrodpasha.cinema.objects");
// create the Datastore connecting to the default port on the local host
final Datastore datastore =
morphia.createDatastore(SingleMongoClient.getInstance().getClient(),
Constants.DB.TICKET_DATABASE);
datastore.ensureIndexes();
//region new movie
MovieDetails movie = new MovieDetails("The Mask", "Stanley Ipkiss (Jim Carrey) is a bank clerk that is an incredibly nice man. Unfortunately," +
" he is too nice for his own good and is a pushover when it comes to confrontations. After one of the worst days of his life, he finds a mask that depicts Loki, " +
"the Norse night god of mischief. Now, when he puts it on, he becomes his inner, self: a cartoon romantic wild man. However, a small time crime boss, Dorian Tyrel (Peter Greene), " +
"comes across this character dubbed The Mask by the media. After Ipkiss's alter ego indirectly kills his friend in crime," +
" Tyrel now wants this green-faced goon destroyed.",
"MASK.jpg", "Chuck Russell", "Jim Carrey as Stanley Ipkiss/The Mask,Cameron Diaz as Tina Carlyle,Amy Yasbeck as Peggy Brandt,Joely Fisher as Maggie", new LocalDate(1994, 2, 1), "Action,Comedy,CrimeAction,Family,Fantasy", 88);
//endregion
// Clearing the db first
datastore.delete(datastore.createQuery(MovieDetails.class));
// Saving a new entity and getting the result saved id
String id = (String) datastore.save(movie).getId();
// This returns as null
MovieDetails movieRetrieved = datastore.get(MovieDetails.class, id);
// This returns with one item
List<MovieDetails> allMovies = datastore.createQuery(MovieDetails.class).asList();
When I use
datastore.get(MovieDetails.class, id)
I get null
When I use:
datastore.createQuery(MovieDetails.class).asList();
I do see my movie in the DB, with the Id used in the get function.
Tried the id in many variations... toString(), ObjectId(id), Key (The value returned from the save result).
The Id in the DB (viewed with Mongo Explorer) does show as something which isn't string (blue colored), suspicious:
Mongo Explorer item picture
Any ideas?
Edit:
* the Id is indeed a string, the cast works and it was verified using watch + instanceof
Edit 2:
* Somehow the cast from ObjectId to String passed and the Id wasnt really a String.
I would change your I'd field from String to a BSON ObjectId field which MongoDB will automagically assign on save. If you then do your get call with the ObjectId as the parameter, it should work. Using ObjectId as your ID field is highly recommended with Mongo.
I am guessing that Morphia is trying to marshall an ObjectId into your String Id field and there is a small bug somewhere. I would try calling datastore.get(Example.class, new ObjectId(id)).
After Nic Cottrell (Thanks!) answer I've had a new perspective of the problem.
As my #Id field was not assigned by me it was automaticaly assigned in the DB as ObjectId.
As I still want to use String I've simply assigned the #Id field on object creation.
Id = ObjectId.get().toString();
found the solution on:
MongoDB / Morphia saves technical id as ObjectId although it's a String in Java

Searching by query for attributes of complex objects in Java EE

I have created the object Person, I can deleted and modify it and I can also search for Person by his name or phonenumber... but I don't know for exemple how to search for a person by his ** home address**. Here is my code:
My entity Person.java:
public class Person{
private Long id;
private String name;
#ManyToOne(cascade = CascadeType.ALL)
private Address address;
....
}
My entity Address.java
public class Address{
...
private String streetName;
...
}
And here is the most interesting function that I am trying to modify to get what I want, I would like to search for Persons who live in xxx (streetName = xxx). Here is my function getByQuery:
public List<Person> getByQuery(PersonSearchQuery searchQuery) {
Map<String, String> criteriaQuery = new HashMap<String, String>();
if (searchQuery.getName() != null)
criteriaQuery.put("name",searchQuery.getName());
TypedQuery<Person> query = this.findByQuery(criteriaQuery);
return query.getResultList();
}
The object PersonSearchQuery contains just to attributes name (String) and streetName (String) and their getters.
Function findByQuery:
public TypedQuery<T> findByQuery(Map<String, String> criteriaQuery) {
CriteriaBuilder builder = this.em.getCriteriaBuilder();
CriteriaQuery<T> criteria = builder.createQuery(this.entityClass);
Root<T> root = criteria.from(this.entityClass);
criteria.select(root);
Predicate predicate = builder.conjunction();
if (criteriaQuery.size() != 0) {
for (String key : criteriaQuery.keySet()) {
try{
predicate = builder.and(predicate, builder.equal(root.<String>get(key), criteriaQuery.get(key)));
}catch(IllegalArgumentException e){
continue;
}
}
}
criteria.where(predicate);
return this.em.createQuery(criteria);
}
So I can search for Persons by their names by I cannot search for them by streetName the problem is my function getByQuery I would like to do something like this:
if (searchQuery.getStreetName() != null)
criteriaQuery.put("Address.streetName",searchQuery.getStreetName());
The problem is I don't know how to define the key in this case. Thanks for your help
I only use CriteriaBuilder if I have several similar Entities which needs to be used/rendered in the same way, so if person is the only Entity with an Address reference I would just use JPQL, like this:
entityManager.createQuery(
"select p from Person p where p.address.streetName like :streetName", Person.class)
.setParameter("streetName", "xyz" + "%").getResultList()
The main reason I tend to avoid CriteriaBuilder, is because it has a rather steep learning curve, and you need to write a lot of code to express very simple concepts. In contrast any developer familiar with SQL can read and maintain JPQL code.
These days I always use frameworks, like DeltaSpike Data (for EE) and Spring Data, they both implements most of the basic DAO/Repository features, so If you don't mind an extra dependency (and some magic) it can save you a lot of boilerplate JPA code.

How to use criteria query on refrence collection in mongo db

How do I find all the person which are having city ="XYZ" in Address collection
public class Person {
#Id
private String id;
private String description
#DBRef
private Address address;
// Getters and Setters
}
public class Address
{
#Id
private String id;
private String area
private String city
// Getters and Setters
}
Mongo understands #DBRef as a reference to another document, in this case, an Address document and ultimately when the object is loaded from MongoDB, those references will be eagerly resolved and this will get populated to the user as a HATEOAS friendly link. You will get back a mapped object that looks the same as if it had been stored embedded within your master document.
You can define your repository, which will map the endpoints to your database, for the given object, like PersonRepository defined below as an example:
import com.mycompany.domain.Person;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface PersonRepository extends MongoRepository<Person, String> {
List<Person> findByCity(#Param("city") String city);
}
Another way you could go around this using the query criteria methods is executing two queries.
First query would be to fetch the address documents which have the city = "XYZ". Resolve the ids from the list returned.
Generate another query on the Person entity using the ids from the previous operation.
The following demonstrates this approach
Query addressQuery = new Query(where("city").is("XYZ"));
addressQuery.fields().include("_id");
List<Address> addressList = mongoTemplate.find(addressQuery, Address.class, "address"); // get the addresses list that satisfy the given city criteria
// Resolve the ids for the addresses
final List<ObjectId> addressIds = new ArrayList<ObjectId>(addressList.length);
for(final Address address : addressList) {
addressIds.add(new ObjectId(address.getId()));
}
// Get the Person list using the ids from the previous operation
Query personQuery = new Query(where("address.$id").in(addressIds));
List<Person> list = mongoTemplate.find(personQuery, Person.class, "person");
If you knew the address id before hand you can then use a custom query:
public interface PersonRepository extends MongoRepository<Person, String> {
#Query("{ 'address': {'$ref': 'address', '$id': { '$oid': ?0 } } }")
List<Person> findByAddres(String addressIdAsString);
}

Many to Many prepared query

I am using ORMLite and I have this models (I have omitted id fields and visibility for readability):
class Book {
String title;
List<Author> authors;
}
class Author {
String name;
List<Book> books;
}
class BookAuthor {
#DatabaseField(foreign = true)
Author author;
#DatabaseField(foreign = true)
Book book;
}
I want a query with all books order_by title and with their authors in their list, so I have this methods, the first to resolve many-to-many and then to get books shorted:
private PreparedQuery<Author> makeAuthorsForBookQuery() throws SQLException {
QueryBuilder<BookAuthor, Integer> bookAuthorQb = bookAuthorDao.queryBuilder();
bookAuthorQb.selectColumns("author");
SelectArg userSelectArg = new SelectArg();
bookAuthorQb.where().eq("", userSelectArg);
QueryBuilder<Author, String> authorQb = authorDao.queryBuilder();
authorQb.where().in("name", bookAuthorQb);
return authorQb.prepare();
}
private PreparedQuery<Book> makeBooksQuery() throws SQLException {
QueryBuilder<Book,Integer> bookQb = bookDao.queryBuilder();
bookDao.orderBy(Manga.TITLE_COLUMN_NAME, true);
return bookDao.prepare();
But if I do to this way then I have to iterate over each book and to fire the AutorsForBookQuery and assign the result to the book list.
My question is: Does exist any other way to get this? I mean, something as to do an inner-join where map the authors in the books automagically and save it as a PreparedQuery?
PD: My goal is to load this prepared query in a CloseableIterator
I want a query with all books order_by title and with their authors in their list...
What I would do #Fran is to do 2 queries. One for Books ordered by title. Then another query for the authors joined with the BookAuthor class with book.id in the list of books you got. So it is 2 queries but then it is not another query by Book.
ORMLite isn't helping you much there for sure.