Grails 3.0.9 not updating object with MongoDB - mongodb

I've problems updating a domain class. I'm using Grails 3.0.9 and MongoDB (for Gorm 5.0.0.RC1)
In my build.gradle:
compile "org.grails.plugins:mongodb:5.0.0.RC1"
compile "org.mongodb:mongodb-driver:3.0.2"
compile "org.grails:grails-datastore-gorm-mongodb:5.0.0.RC1"
runtime 'org.springframework.data:spring-data-mongodb:1.8.1.RELEASE'
compile("org.grails:gorm-mongodb-spring-boot:5.0.0.RC1")
The test:
#Integration
class CompanyControllerIntegrationSpec extends Specification{
def grailsApplication
Company company
RestBuilder rest
def setupData() {
company = Company.buildWithoutSave().save(flush: true, failOnError: true)
}
def setup(){
rest = new RestBuilder()
}
def "test update a company" (){
def company2
given:
setupData()
def id = company.id
when:
RestResponse response = rest.put("http://localhost:${grailsApplication.config.server.port}/${company.companyKey}/company") {
json {
name = "newName"
description = "new Description"
}
}
company2 = Company.findById(id)
then:
response.status == 200
response.json.name == "newName"
company2.name == "newName"
company2.description == "new Description"
}
def cleanup() {
Company.collection.remove(new BasicDBObject())
}
}}
The controller:
class CompanyController extends ExceptionController{
static allowedMethods = ['update':'PUT','show':'GET',
'updateNew':'PUT','showNew':'GET']
CompanyService companyService
def update(String companyKey){
def object = request.JSON?request.JSON:params
Company companyOut = companyService.update(object, companyKey)
render text:companyOut as JSON, status:HttpStatus.OK
}
}
The service:
class CompanyService {
def securityService
def grailsApplication
public Company update(object, String companyKey) throws ForbiddenException, InvalidRequestException, NotFoundException{
Company company = findByKey(companyKey)
if (object.name!=null)
company.name = object.name
if (object.description!=null)
company.description = object.description
if (object.enterprise!=null)
company.enterprise = object.enterprise
if (object.genKey!=null)
company.companyKey = UUID.randomUUID().toString()
if (!company.save(flush:true)){
println company.errors
throw new InvalidRequestException("Some parameters are missing or are invalid: "+company.errors.fieldErrors.field)
}
return company
}
public Company findByKey(String companyKey) throws NotFoundException, ForbiddenException {
if (!companyKey){
throw new ForbiddenException("The company key has not been given")
}
Company company = Company.findByCompanyKey(companyKey)
if (!company){
throw new NotFoundException("No company exists for the given key")
}
return company
}
}
The results of the test are:
- response.status is 200
- response.json.name is "newName"
- company.name is old name ("company 1")
If I don't do the cleanup, the database still have the old value. I've followed the save method, also inside Mongo gorm classes, and I've seen that one problem is that the fields are not being marked as dirty, but don't know why.
With other Domain classes that are similar to this one, the update is done without problems and the properties are marked as dirty.

Related

Migration not starting with mongock 4.3.8

I am trying to create indexes on a mongodb collection using mongock version 4.3.8. The mongockLock and mongockChangeLog collections are eventually created, but the migration itself does not run. mongockChangeLog table is empty. Query db.BOOKS.getIndexes(); returns a single string: with an index for id (the value of name in it is equal to _id_), which is most likely generated automatically. There are no obvious errors in the logs either.
Everything else seems to be set up correctly, I don't understand why my migration is not running.
build.gradle.kts:
implementation("com.github.cloudyrock.mongock:mongodb-springdata-v3-driver:4.3.8")
implementation("com.github.cloudyrock.mongock:mongock-spring-v5:4.3.8")
implementation(platform("com.github.cloudyrock.mongock:mongock-bom:4.3.8"))
application.yml:
mongock:
change-logs-scan-package:
- com.dreamland.configuration.migration
config file:
#EnableMongock
#Configuration
#EnableMongoAuditing
class MongoConfig
Migration file:
#ChangeLog(order = "1668471203")
class IndexChangeLog {
companion object : KLogging()
#ChangeSet(order = "1668471204", id = "1668471204_create_indexes", author = "Irish")
fun createIndexes(mongockTemplate: MongockTemplate) {
val indexOps = mongockTemplate.indexOps(BookObjectDocument::class.java)
Index().on("createdAt", Sort.Direction.DESC).let { indexOps.ensureIndex(it) }.also { log(it) }
Index().on("createdBy", Sort.Direction.ASC).let { indexOps.ensureIndex(it) }.also { log(it) }
Index().on("type", Sort.Direction.DESC).let { indexOps.ensureIndex(it) }.also { log(it) }
}
private fun log(indexName: String) {
logger.info { "Index '$indexName' was created successfully" }
}
}
Document class:
#Document(collection = "BOOKS_V2")
data class BookObjectDocument(
val type: BookObjectType?,
val description: String?
) {
#Id
lateinit var id: String
#CreatedDate
lateinit var createdAt: Instant
#CreatedBy
var createdBy: String? = null
}

Correctly commit to database using #Transactional in spring-data

I have a test function, that should perform the following task
insert data to db
query db and verify data is as expected
The problem is that in my test, the data has not been committed to the db, like it's stuck in some transactional step, how can I surely commit the data before the second query is executed.
This is a part of my test function, #Rollback(false) is just for development phase.
#Test
#Rollback(false)
....
reportJobManager.saveOutput(savedDef, pipeline, results, null)
reportJobManager.retryRetention(savedDef, listOf(csvDeliverbale))
saveOutput func. sample code
#Transactional
fun saveOutput() {
if (deliverable.type.name == "DATA_RETENTION_RESULT") {
finishedPipeline.postProcessors.forEach {
//it(definition, dbDeliverable)
val dbRetention = ReportRetention(
deliverable = dbDeliverable,
definition = definition,
retryCount = 1L
)
val retentionUploadSaved = retentionRepository.save(dbRetention)
if (retentionUploadSaved.id == null) {
throw IllegalStateException("Retention upload was not saved!")
}
}
}
}
retryRetention func code
fun retryRetention(definition: ReportDefinition, listOfDeliverables: List<Deliverable>) {
retentionRepository.findAll().forEach {
if (it.state.name == "NOT_UPLOADED" && it.retryCount!!.toInt() < 5) {
if (it.deliverable?.success == true) {
it.state = RetentionUploadStatus.UPLOADED
println("RetentionUploadStatus->UPLOADED")
} else {
val schemaService = SchemaServiceImpl()
val schemas = schemaService.initializeSchemas(definition, emptyMap())
val parameters = definition.parameterPolicy.policy(schemas.parametersSchema)
val delivery = deliveryPolicyService.policy<Deliverable>(ValidDeliveryPolicy.RETENTION_ONLY, schemas.deliverySchema)
val deliveryFunction = delivery.createDeliveryStep()
deliveryFunction(parameters, listOfDeliverables)
it.retryCount = it.retryCount!!.plus(1L)
}
retentionRepository.save(it)
}
}
}
If you have a method saveOutput() with #Transactional annotation, you need to add #Transactional above every other method that is calling saveOutput() for the transaction to actually commit.

Update Mongo document field from Grails app

I have a requirement where I have to check in the database if the value of a boolean variable(crawled) is false. If so I have to set it to true. I am finding the record based on the value of a string variable(website). I referenced this link but it didn't help. Please tell me what I am doing wrong.
I tried this:
def p = Website.findByWebsite(website);
if(p['crawled'] == true) {
println "already crawled"
} else {
mongo.website.update( p, new BasicDBObject( '$set', new BasicDBObject( 'crawled', 'false' ) ) )
println "updated object"
}
It gives me an error No such property: mongo for class: cmsprofiler.ResourceController
My domain class is as follows:
class Website{
String website
User user
Boolean crawled
static belongsTo = [user: User]
static constraints = {
website( url:true, unique: ['user'])
}
static hasMany = [resource: Resource]
static mapping = {resource cascade:"all-delete-orphan" }
}
you should be using
Website.mongo.update(...)
or let the framework inject it:
class ResourceController {
def mongo
def list(){
mongo.website.update(...)
}
}
This worked for me. Posting it here in case anyone else has similar requirement.
#Grab(group='com.gmongo', module='gmongo', version='0.9.3')
import com.gmongo.GMongo
#Transactional(readOnly = true)
class ResourceController {
def mongo = new GMongo()
def db = mongo.getDB("resources")
def p = Website.findByWebsite(website)
if(p['crawled']==false){
db.website.update([crawled:false],[$set:[crawled:true]])
}

Grails MongoDb: saving map values with domain as value fails

I have 2 domain classes, User and Dog (for example)
class User {
String id
Map<String, Dog> dogs
}
class Dog {
String name
}
My Controller get as an input a json
{"key" : "dogKey", "userId" : "someId", "dogName" : "dog"}
def addDog(){
String key = request.JSON.key
User user = User.get(request.JSON.userId)
String dogName = request.JSON.dog
...
if(! user.dogs){
user.dogs = new HashMap<>(1)
}
user.dogs.put(key, new Dog(name: dogName))
user.save(flush: true)
}
after running the user data # Mongo is:
user:
{ _id:....,
dogs: {
"dogKey": null
}...
}
can someone please explain me what i'm missing?
Thanks!
Roy
may be dog reference is not save in database
def addDog(){
String key = request.JSON.key
User user = User.get(request.JSON.userId)
String dogName = request.JSON.dog
...
if(! user.dogs){
user.dogs = new HashMap<>(1)
}
Dog dog = new Dog(name: dogName)
dog.save(flush:true)
user.dogs.put(key, dog)
user.save(flush: true)
}

return/break a function from the derived base class

In my REST API service layer, I have a class ProductService.
The following logic exists in all my functions: Do Validate, if validation fails i throw invalid exception, if passes, i continue to the next try-catch and throw general-error in case of failure:
def addProduct(request:AddProductRequest): BaseResponse[String] =
{
try
{
request.validate
}
catch
{
case ex: Exception => {
Logger.error("Failed to add product, Validation failed", ex);
val errorResponse:ErrorResponse[String] = new ErrorResponseList().InvalidParameters
errorResponse.addMessage(ex.getMessage)
return errorResponse
}
}
try
{
val addedProductId = productRepository.addProduct(request.language, request.tenantId, request.product)
DTOResponse(addedProductId)
}
catch
{
case ex: Exception => {
Logger.error("Failed to add product to tenant Id="+request.tenantId+" language="+request.language, ex);
val errorResponse:ErrorResponse[String] = new ErrorResponseList().GeneralError
errorResponse.addMessage(ex.getMessage())
return errorResponse
}
}
}
Now, instead of repeating the request.validate with the same try and catch for all functions, i created a base class with the following function:
abstract class ServiceBase {
def validate[T](request:BaseRequest)
{
try
{
request.validate
}
catch
{
case ex: Exception => {
Logger.error("Validation failed", ex);
val errorResponse:ErrorResponse[String] = new ErrorResponseList().InvalidParameters
errorResponse.addMessage(ex.getMessage)
return errorResponse
}
}
}
So now, my addProduct(..) will look like:
validate(request)
..the rest of the code - the 2nd try-catch
This saves alot of lines.
The problem is that if validation fails, it will never return. I get the following errors in ServiceBase:
Multiple markers at this line
- enclosing method validate has result type Unit: return value discarded
- enclosing method validate has result type Unit: return value discarded
- a pure expression does nothing in statement position; you may be omitting necessary
parentheses
validate has no return type (and thus defaults to returning Unit), in ServiceBase your signature for validate should look like this:
def validate[T](request:BaseRequest): BaseResponse[String] =
(assuming you want to return a BaseResponse[String])
this may be useful to someone, someday, functional programming.. Did we say ^_^
Changed the ServiceBase validate to:
def validate[T](request:BaseRequest):Option[BaseResponse[T]] =
{
try
{
request.validate
None
}
catch
{
case ex: Exception => {
Logger.error("Validation failed", ex);
val errorResponse:ErrorResponse[T] = new ErrorResponseList().InvalidParameters
errorResponse.addMessage(ex.getMessage)
return Some(errorResponse)
}
}
}
And now i do:
def getProducts(request:GetProductsRequest) :BaseResponse[ProductSearchResults] =
{
validate[ProductSearchResults](request).getOrElse(
{
try
{
val products = productRepository.getProducts(request.language,request.tenantId,request.productIds)
val foundProducts = for (product <- products) yield (product.id)
val notFoundProducts = request.productIds filterNot (foundProducts.toSet)
val responseWrapper = new ProductSearchResults(products, notFoundProducts)
DTOResponse(responseWrapper)
}
catch
{
case ex: Exception => {
Logger.error("Failed to get products from tenant Id="+request.tenantId+" language="+request.language, ex);
val errorResponse:ErrorResponse[ProductSearchResults] = new ErrorResponseList().GeneralError
errorResponse.addMessage(ex.getMessage())
return errorResponse
}
}})
}