I'm working on a Grails project, and I want to use GORM in order to read/write documents in a MongoDB. I have a specific scenario I want to implement, and I've been trying to make this work for 2-3 days now, but I can't find out what I'm doing wrong. I've read Grails documentation, many related posts on the web, but still nothing.
For the sake of simplicity I'm working on a simple enough scenario, which is the following:
I have 4 Domain classes:
Someone.groovy
class Someone {
static mapWith = 'mongo'
static embedded = ['animal']
static constraints = {}
String name
Animal animal
}
Animal.groovy
class Animal {
static mapWith = 'mongo'
static mapping = {
discriminator column: "type"
}
static belongsTo = Someone
static constraints = {}
}
Dog.groovy
class Dog extends Animal {
static mapWith = 'mongo'
String dogName
static mapping = {
discriminator value: "dog"
}
static belongsTo = Someone
static constraints = {}
}
Cat.groovy
class Cat extends Animal {
static mapWith = 'mongo'
String catName
static mapping = {
discriminator value: "cat"
}
static belongsTo = Someone
static constraints = {}
}
Now, in a Service class, called CrudService, I have a method, e.g. foo(), and in that method I create a new Someone object, and save it to my Mongo database.
Someone someone = new Someone(name: "A name", animal: new Dog(dogName: "Azor"))
someone.save(flush: true)
When I check my mongoDB collection to see what was saved, I see:
{
"_id" : NumberLong(21),
"animal" : {
"_class" : "Dog",
"dogName" : "Azor"
},
"name" : "A name",
"version" : 0
}
So, as you can see, the document was saved, but the discriminator column name (and values) were completely ignored. The document was saved with the value "_class" as the column name (which is the default discriminator column name) and the Class name (in this case Dog) as its value.
Can anyone please help me and tell me what I'm doing wrong here?
Is there a chance that the discriminator feature of GORM, works correctly for RDBMS but not for NoSQL databases?
(Currently I'm also looking at Morphia in order to see whether it can provide a solution to my problem, but I would prefer to use GORM since it is the default ORM for Grails).
Thank you in advance for any suggestion.
Related
I'm using GORM for MongoDB in my Grails 3 web-app to manage read/writes from DB.
I have the following 2 domain classes:
class Company {
String id
}
class Team {
String id
Company company
}
For teams, their company is saved on DB as String, and with GORM I can simply use team.company to get an instance of Company domain class.
However, I need to override the getter for company, and I need the raw value for company id (as stored on DB), without GORM getting in the way and performing its magic.
Is there a way to get the raw String value?
Any help is welcome! Thanks in advance
Update (May 27)
Investigating #TaiwaneseDavidCheng suggestion, I updated my code to
class Company {
String id
}
class Team {
String id
Company company
String companyId
static mapping = {
company attr: "company" // optional
companyId attr: "company", insertable: false, updateable: false
}
}
Please note that I'm using GORM for MongoDB, which (citing the manual) tries to be as compatible as possible with GORM for Hibernate, but requires a slightly different implementation.
However I found out (by trial&error) that GORM for MongoDB doesn't support a similar solution, as it seems only one property at a time can be mapped to a MongoDB document property.
In particular the last property in alphabetical order wins, e.g. companyId in my example.
I figured out a way to make the whole thing work, I'm posting my own answer below.
given a non-insertable non-updateable column "companyId" in domain class
class Company {
String id
}
class Team {
String id
Company company
Long companyId
static mapping = {
company column:"companyId"
companyId column:"companyId",insertable: false,updateable: false
}
}
(Follows the edit to my question above)
I defined a custom mapping, and made use of Grails transients by also defining custom getter and setter for team's company.
class Company {
String id
}
class Team {
String id
Company company
String companyId
static mapping = {
companyId attr: "company" // match against MongoDB property
}
static transients = [ 'company' ] // non-persistent property
Company getCompany() {
return Company.get(companyId)
}
void setCompany(Company company) {
companyId = company.id
}
}
Is there a recommended way to go about dealing with documents that don't have the _class field with spring-data-couchbase( if there is one)? Trying it simply just throws an exception as expected.
Edit: Apologies if this was a bit too vague, let me add a bit more context.
I want to fetch data from couchbase for some student by name, let's say . The repository looks something like -
#Repository
public interface StudentRepository extends CouchbaseRepository {
Optional<StudentDocument> findByName(String name);
}
Now the documents in couchbase don't have the _class field OR say if we are entering a different "key" and "value" for _class field as we don't want to rely on it, so this method fails. I sort of hacked a workaround for this using -
`
#Override
public Student getStudent(String name) {
N1qlQuery query = N1qlQuery.simple(String.format("select *, META().id AS _ID, META().cas AS _CAS" +
" from student where name = \'%s\';", name));
return Optional.ofNullable(studentRepository.getCouchbaseOperations()
.findByN1QL(query, StudentWrapper.class)
.get(0))
.map(StudentWrapper::getStudent)
.orElseGet(() -> {
throw new HttpClientErrorException(HttpStatus.NOT_FOUND);
});
}
`
I was wondering if there is an alternate way of achieving this
While using Spring spEL, Couchbase will automatically include the _class (or whatever attribute you have defined as your type) for you:
public interface AreaRepository extends CouchbaseRepository<Area, String> {
//The _class/type is automatically included by Couchbase
List<Area> findByBusinessUnityIdAndRemoved(String businessId, boolean removed);
}
However, if you want to use N1QL, you have to add the #{#n1ql.filter} :
public interface BusinessUnityRepository extends CouchbaseRepository<BusinessUnity, String>{
#Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $2 and $1 within #{#n1ql.bucket}")
BusinessUnity findByAreaRefId(String areaRefId, String companyId);
}
the #{#n1ql.filter} will automatically add the filter by type for you.
Below is the domain class Author which has a one-to-many association with Book. When trying to fetch Author, sometimes will get the associated collection of Book domain object and sometimes returns null. Any thoughts on why it is inconsistent?
class Author {
static mapWith = "mongo"
String name
static hasMany = [books: Book]
}
_author.gson
model {
Author author
}
json g.render(author]) {
books g.render(author.books)
}
Environment details:
grailsVersion=3.3.5
gormVersion=6.1.8.RELEASE
We are using GORM multi-tenancy and using MongoDB database.
I have switched to sub-document model and it's working now. Here is the domain model code.
class Author {
static mapWith = "mongo"
String name
List<Books> book
static embedded = ['book']
}
I'm trying to implement a rest api using RepositoryRestResource and RestTemplate
It all works rather well, except for loading #DBRef's
Consider this data model:
public class Order
{
#Id
String id;
#DBRef
Customer customer;
... other stuff
}
public class Customer
{
#Id
String id;
String name;
...
}
And the following repository (similar one for customer)
#RepositoryRestResource(excerptProjection = OrderSummary.class)
public interface OrderRestRepository extends MongoRepositor<Order,String>{}
The rest api returns the following JSON:
{
"id" : 4,
**other stuff**,
"_links" : {
"self" : {
"href" : "http://localhost:12345/api/orders/4"
},
"customer" : {
"href" : "http://localhost:12345/api/orders/4/customer"
}
}
}
Which if loaded correctly by the resttemplate will create a new Order instance with customer = null
Is it possible to eagerly resolve the customer on the repository end and embed the JSON?
Eagerly resolving dependent entities in this case will raise most probably N+1 database access problem.
I don't think there is a way to do that using default Spring Data REST/Mongo repositories implementation.
Here are some alternatives:
Construct an own custom #RestController method that would access the database and construct desired output
Use Projections to populate fields from related collection, e.g.
#Projection(name = "main", types = Order.class)
public interface OrderProjection {
...
// either
#Value("#{customerRepository.findById(target.customerId)}")
Customer getCustomer();
// or
#Value("#{customerService.getById(target.customerId)}")
Customer getCustomer();
// or
CustomerProjection getCustomer();
}
#Projection(name = "main", types = Customer.class)
public interface CustomerProjection {
...
}
The customerService.getById can employ caching (e.g. using Spring #Cachable annotation) to mitigate the performance penalty of accessing the database additionally for each result set record.
Add redundancy to your data model and store copies of the Customer object fields in the Order collection on creation/update.
This kind of problem arises, in my opinion, because MongoDB doesn't support joining different document collections very well (its "$lookup" operator has significant limitations in comparison to the common SQL JOINs).
MongoDB docs also do not recommend using #DBRef fields unless joining collections hosted in distinct servers:
Unless you have a compelling reason to use DBRefs, use manual references instead.
Here's also a similar question.
I have two domain objects,
#Document
public class PracticeQuestion {
private int userId;
private List<Question> questions;
// Getters and setters
}
#Document
public class Question {
private int questionID;
private String type;
// Getters and setters
}
My JSON doc is like this,
{
"_id" : ObjectId("506d9c0ce4b005cb478c2e97"),
"userId" : 1,
"questions" : [
{
"questionID" : 1,
"type" : "optional"
},
{
"questionID" : 3,
"type" : "mandatory"
}
]
}
I have to update the "type" based on userId and questionId, so I have written a findBy query method inside the custom Repository interface,
public interface CustomRepository extends MongoRepository<PracticeQuestion, String> {
List<PracticeQuestion> findByUserIdAndQuestionsQuestionID(int userId,int questionID);
}
My problem is when I execute this method with userId as 1 and questionID as 3, it returns the entire questions list irrespective of the questionID. Is the query method name valid or how should I write the query for nested objects.
Thanks for any suggestion.
Just use the #Query annotation on that method.
public interface CustomRepository extends MongoRepository<PracticeQuestion, String> {
#Query(value = "{ 'userId' : ?0, 'questions.questionID' : ?1 }", fields = "{ 'questions.questionID' : 1 }")
List<PracticeQuestion> findByUserIdAndQuestionsQuestionID(int userId, int questionID);
}
By adding the fields part of the #Query annotation, you are telling Mongo to only return that part of the document. Beware though, it still returns the entire document in the same format - just missing everything you did not specify. So your code will still have to return List<PracticeQuestion> and you will have to do:
foreach (PracticeQuestion pq : practiceQuestions) {
Question q = pq.getQuestions().get(0); // This should be your question.
}
Property expressions
Property expressions can refer only to a direct property of the managed entity, as shown in the preceding example. At query creation time you already make sure that the parsed property is a property of the managed domain class. However, you can also define constraints by traversing nested properties. Assume Persons have Addresses with ZipCodes. In that case a method name of List<Person> findByAddressZipCode(ZipCode zipCode);
creates the property traversal x.address.zipCode. The resolution algorithm starts with interpreting the entire part (AddressZipCode) as the property and checks the domain class for a property with that name (uncapitalized). If the algorithm succeeds it uses that property. If not, the algorithm splits up the source at the camel case parts from the right side into a head and a tail and tries to find the corresponding property, in our example, AddressZip and Code. If the algorithm finds a property with that head it takes the tail and continue building the tree down from there, splitting the tail up in the way just described. If the first split does not match, the algorithm move the split point to the left (Address, ZipCode) and continues.
Although this should work for most cases, it is possible for the algorithm to select the wrong property. Suppose the Person class has an addressZip property as well. The algorithm would match in the first split round already and essentially choose the wrong property and finally fail (as the type of addressZip probably has no code property). To resolve this ambiguity you can use _ inside your method name to manually define traversal points. So our method name would end up like so:
UserDataRepository:
List<UserData> findByAddress_ZipCode(ZipCode zipCode);
UserData findByUserId(String userId);
ProfileRepository:
Profile findByProfileId(String profileId);
UserDataRepositoryImpl:
UserData userData = userDateRepository.findByUserId(userId);
Profile profile = profileRepository.findByProfileId(userData.getProfileId());
userData.setProfile(profile);
Sample Pojo :
public class UserData {
private String userId;
private String status;
private Address address;
private String profileId;
//New Property
private Profile profile;
//TODO:setter & getter
}
public class Profile {
private String email;
private String profileId;
}
For the above Document/POJO in your Repository Class:
UserData findByProfile_Email(String email);
For ref : http://docs.spring.io/spring-data/data-commons/docs/1.6.1.RELEASE/reference/html/repositories.html
You need to use Mongo Aggregation framework :
1) Create custom method for mongo repository : Add custom method to Repository
UnwindOperation unwind = Aggregation.unwind("questions");
MatchOperation match = Aggregation.match(Criteria.where("userId").is(userId).and("questions.questionId").is(questionID));
Aggregation aggregation = Aggregation.newAggregation(unwind,match);
AggregationResults<PracticeQuestionUnwind> results = mongoOperations.aggregate(aggregation, "PracticeQuestion",
PracticeQuestionUnwind.class);
return results.getMappedResults();
2) You need to cretae a class(Because unwind operation has changed the class structure) like below :
public class PracticeQuestionUnwind {
private String userId;
private Question questions;
This will give you only those result which matches the provide userId and questionId
Result for userId: 1 and questionId : 111 :
{
"userId": "1",
"questions": {
"questionId": "111",
"type": "optional"
}
}
i too had similar issue. for that i added $ before the nested class attributes.
try below query
#Query(value = "{ 'userId' : ?0, 'questions.$questionID' : ?1 }") List<PracticeQuestion> findPracticeQuestionByUserIdAndQuestionsQuestionID(int userId, int questionID);