Morphia/MongoDB: Accessing "embedding" object from an #Embedded object - mongodb

I have a Morphia schema similar to this one:
#Entity
class BlogEntry {
#Embedded
List<BlogComment> comments
}
#Embedded
class BlogComment {
String content
Long authorId
}
(code above just for illustration)
I'm trying to get a specific BlogComment in order to update it with new content. I have the corresponding BlogEntry object available, and I have the authorId, which let's say for the purposes of this question that these two together are sufficient to uniquely identify the correct BlogComment.
My question is, BlogComment does not explicitly contain a reference to its "parent" BlogEntry object, so how can I write a morphia query to retrieve this BlogComment? Something like:
//fetch the unique comment corresponding to this blog entry and this author ID.
BlogComment comment = ds.find(BlogComment.class, "blogEntryId =", blogEntry.id)
.filter("authorId", authorId)
.get();

Since you already have the blog entry object why not use a simple Java loop to filter it out?
#Entity
class BlogEntry {
#Embedded
List<BlogComment> comments
public BlogComment findCommentByAuthorId(String authorId) {
if (null == authorId) return null;
for (BlogComment comment: blogEntry.comments) {
if (authorId.equals(comment.authorId) return comment;
}
return null;
}
}

Related

Spring Data - MongoRepository - #Query to find item within nested list of objects

I'm trying to query MongoDB to return a single Answer object contained within a QuestionDocument object.
I am using Spring, MongoRepository, and JDK 11.
My QuestionDocument POJO:
#Data
#Document(collection = "Questions")
#AllArgsConstructor(onConstructor = #__(#Autowired))
#NoArgsConstructor
public class QuestionDocument {
#Id
private String questionId;
(...)
private List<Answer> answers;
(...)
}
My Answer POJO:
#Data
public class Answer implements Serializable {
private String answerId;
(...)
My QuestionRepository:
#Repository
public interface QuestionRepository extends MongoRepository<QuestionDocument, String> {
#Query(value = "{ { 'questionId' : ?0 }, { 'answers.$answerId' : ?1 } }")
Answer findByQuestionIdAndAnswerId(String questionId, String answerId);
My QuestionServiceImpl:
public getAnswer(String questionId, String answerId){
Answer answer = findByQuestionIdAndAnswerId(questionId, answerId);
return answer;
}
protected Answer findByQuestionIdAndAnswerId(String questionId, String answerId){
Answer answer;
try {
answer = questionRepository.findByQuestionIdAndAnswerId(questionId, answerId);
} catch (Exception e) {
throw new IllegalArgumentException("There is no answer with this ID.");
}
return answer;
}
When I hit my endpoint in Postman, the correct response body appears, but all of its values are null. I have verified that the correct questionId and answerId are passed in my parameters.
I have also consulted several additional SO posts and Spring and MongoDB documentation, but so far, implementing what I've read regarding traversing nested objects by property hasn't helped.
How does my #Query value need to change to properly return a specific Answer object from this nested list of answers?
I have attempted to create findBy methods like:
findByQuestion_Answers_AnswerId(String answerId);
I have attempted to add #DBRef above my List<Answer> answers, and adding #Document(collection = "Answers") and #Id above private String answerId; in my Answer POJO. I then cleared my database, created a new question and answer, and queried for the specific answerId, and still returned null data.
What I expect, is that given the questionId and answerId, the query will return one Answer object and its associated information (answerBody, answerAuthor, etc.).
My postman response states SUCCESS, but the data is null.
You can change the Query to this.
#Query(value = "{{'questionId' : ?0, 'answers.answerId' : ?1}}")
or, just define this method.
findByQuestionIdAndAnswerId(String questionId, String answerId);
The return type will be of QuestionDocument, not Answer.
More details here.

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.

Having an ICollection with references only instead of creatiing copies/clones

I have a Survey that contains questions and also which users can/will participate in the survey.
like so
public virtual ICollection<User> ParticipatingUsers { get; set; }
public virtual ICollection<Question> SpecificQuestions { get; set; }
However, due to the ajaxy solution I create the questions first and then simply send in the ID of my created question with the survey data. So all I need to do is change the sortingIndex of the question and then add a reference to it in my Survey.
When it comes to users they belong to a Company entity and I only want to reference them from the survey not own them.
But currently I get all the id's for questions and users in my action method (.net mvc) and so currently I load all questions and users and attach them to my survey entity before sending the survey to the repository.
But when my Repository calls Add on dbset it clones the user and question data instead of simply referencing existing data.
I am lost, I have solved this exact problem for a normal navigation property by adding [Foreignkey] but i don't know how that would work with ICollection
For completeness
Here is my action method recieving the data
[HttpPost]
[FlexAuthorize(Roles = "RootAdmin")]
public ActionResult SaveSurvey(EditSurveyViewModel editModel)
{
if (!ModelState.IsValid)
{
//We dont bother to send this in so we need to fetch the list again
editModel.CompanyList = _companyRepository.GetAll();
List<string> deletionList = new List<string>();
//We clear out all questions from the state as we have custom logic to rerender them with the correct values
foreach (var modelstateItem in ModelState)
{
if (modelstateItem.Key.StartsWith("Questions"))
{
deletionList.Add(modelstateItem.Key);
}
}
foreach (string key in deletionList)
{
ModelState.Remove(key);
}
return View("EditSurvey", editModel);
}
List<Question> questionlist = new List<Question>();
int sort = 1;
Question q;
//We have questions sent in from the ui/client
if (editModel.Questions != null)
{
//Go trough each questions sent in
foreach (var question in editModel.Questions)
{
//if it's a page break, just assign our new question the sent in one and set sort index
if (question.IsPageBreak)
{
q = question;
q.SortIndex = sort;
}
else
{
//It's a question and all questions are already created with ajax from the client
//So we simply find the question and then set sort index and tie it to our survey
q = _questionRepository.GetById(question.Id);
q.SortIndex = sort;
}
questionlist.Add(q);
sort++;
}
}
//assign the new sorted questions to our Survey
editModel.Item.SpecificQuestions = questionlist;
List<User> userlist = new List<User>();
foreach (int id in editModel.SelectedUsers)
{
userlist.Add(_userRepository.GetById(id));
}
editModel.Item.ParticipatingUsers = userlist.ToList();
_surveyRepository.SaveSurveyBindAndSortQuestionsLinkUsers(editModel.Item);
return RedirectToAction("Index");
}
Here is the viewmodel the method gets sent in
public class EditSurveyViewModel
{
public Survey Item { get; set; }
public IEnumerable<Question> Questions { get; set; }
public bool FullyEditable { get; set; }
public IEnumerable<Company> CompanyList { get; set; }
public IEnumerable<int> SelectedUsers { get; set; }
}
Finally here is the repo method (so far i only implemented insert, not update)
public void SaveSurveyBindAndSortQuestionsLinkUsers(Survey item)
{
if (item.Id == 0)
{
Add(item);
}
ActiveContext.SaveChanges();
}
Update/Edit
Moho: You are of course correct, I think to my shame I was testing some things and forgot to reset the method before pasting it in here.
I have updated the action method above.
Slauma: Sorry for lack of details, here comes more.
All my repositories look like this
public class EFSurveyRepository : Repository<Survey>, ISurveyRepository
So they inherit a generic repository and implement an interface
The generic repository (the part we use in code above, looks like this)
public abstract class Repository<T> : IRepository<T> where T : class
{
public EFDbContext ActiveContext { get; private set; }
private readonly IDbSet<T> dbset;
public Repository()
{
this.ActiveContext = new EFDbContext("SurveyConnection");
dbset = ActiveContext.Set<T>();
}
public virtual void Add(T entity)
{
dbset.Add(entity);
}
public virtual T GetById(int id)
{
return dbset.Find(id);
}
I have noticed in the database that my User table (for User entity) now contains a Survey_Id field which i do not want it to have. I want a many-to-many where many surveys can link to many users (the same users) but the users should entity-wise still only belong to a Department in a Company.
Also, right now when I run the code (after I corrected my action method) I get the following error:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
No InnerException, only that when i try to add the new survey.
The problem is that you are using separate contexts per repository:
public Repository()
{
this.ActiveContext = new EFDbContext("SurveyConnection");
//...
}
In your POST action you have four repositories in place: _companyRepository, _questionRepository, _userRepository and _surveyRepository. It means you are working with four different contexts, i.e. you load data from different contexts, create relationships between entities that are attached to different contexts and save the data in yet another context.
That's the reason for the entity duplication in the database, for the "multiple instances of IEntityChangeTracker" exception and will be the source for many other problems you might encounter in future.
You must refactor the architecture so that you are using only one and the same context instance ("unit of work") in every repository, for example by injecting it into the constructor instead of creating a new one:
private readonly EFDbContext _activeContext;
private readonly IDbSet<T> _dbset;
public Repository(EFDbContext activeContext)
{
_activeContext = activeContext;
_dbset = activeContext.Set<T>();
}
You build up questionList, set it to editModel.Item.SpecificQuestions, then overwrite the reference by settting that same property to editModel.Questions.ToList(), which is from your view model (i.e.: not loaded via your database context like questionList's question objects) and therefore appears to be new questions to your database context,
editModel.Item.SpecificQuestions = questionlist;
// what is this? why?
editModel.Item.SpecificQuestions = editModel.Questions.ToList();
Edit after question update:
Instead of using questionList and assigning to the questions property of the Survey, simply use the property directly.
Also, do you realize that if you're reusing Question records from the DB for multiple Surveys, you're updating the sort order at the question itself and not simply for that Survey? Each time you save a new survey that reuses questions, other surveys' question ordering will me altered. Looks like you need a relationship entity that will map Questions to Surveys where you can also store the sort order so that each survey can reuse question entities without messing up existing surveys question ordering.

Using the $in operator through Morphia - doing it wrong?

I have the following Play Framework entity (using Morphia for persistence) as part of a generic blogging app:
#Entity
public class Comment extends Model {
...
#Reference
#Indexed
public SiteUser commenter;
public static List<Comment> getLastCommentsByUsers(final List<SiteUser> users) {
final Query<Comment> query ds().createQuery(Comment.class);
query.field(commenter).hasAnyOf(users);
return query.asList();
}
}
SiteUser:
#Entity(noClassnameStored=true)
public class SiteUser extends AbstractUser {
public String realName;
}
AbstractUser:
public class AbstractUser extends Model {
#Indexed(value= IndexDirection.DESC, unique = true)
public String emailAddress;
#Required
public String password;
}
The method getLastCommentsByUsers() is supposed to return all comments by the users in the users parameter, but I always get an empty List back. The reason that Commment is a separate collection is to be able to retrieve last X Comments by certain users across their associated Posts, which isn't possible if the Comment is embedded in the Post collection.
Is there something wrong with my query (should I be using something other than hasAnyOf), or is it a problem with the relationship mapping - should I be using ObjectId instead?
I use the in() method with a list or set and its working perfectly. Here's a snippet:
List<String> keywordList;
List<Product> products = Product.find().field("keywords").in(keywordList).asList();
This should work for collection of embedded or references too.
You should use List<Key<SiteUser>> to query:
public static List<Comment> getLastCommentsByUsers(final List<SiteUser> users) {
final Query<Comment> query ds().createQuery(Comment.class);
query.field(commenter).hasAnyOf(toKeys(users)); // convert to keys
return query.asList();
}
public static List<Key<SiteUser>> toKeys(List<SiteUser> users) {
List<Key<SiteUser>> keys = new ArrayList<Key<SiteUser>>();
for(SiteUser user: users) {
keys.add(ds().getMapper().getKey(user));
}
return keys;
}
Or you can just get the keys by:
List<Key<SiteUser>> keys = ds().createQuery(SiteUser.class).query().filter(...).asKeyList();