Grails: sort order on one-to-many relationship not working - grails

I've got a simple bi-directional one-to-many mapping, as follows, with a default sort order specified on the owning side. However, the sort order doesn't seem to be getting applied? I'm using Grails v2.0.1 (I've now replicated this example with v1.3.7).
package playground
class User {
String name
static hasMany = [ posts : Post ]
static mapping = {
posts sort:'position'
}
}
and
package playground
class Post {
int position = 1
String message
static belongsTo = [ user : User ]
}
this is the integration test code I'm using to exercise it ...
def User user = new User(name:'bob')
user.addToPosts(new Post(position:2, message:'two'))
user.addToPosts(new Post(position:3, message:'three'))
user.addToPosts(new Post(position:1, message:'one'))
assertTrue user.validate()
assertFalse user.hasErrors()
assertNotNull user.save()
for (post in user.posts) {
log.debug "Post message> ${post.message}"
}
Please put me out of my misery, it's presumably something obvious but I can't see it! Thanks.

use this code:
package playground
class User {
String name
static hasMany = [ posts : Post ]
static mapping = {
posts sort:'position' order:'desc'//order:'asc'
}
}

Turns out this is a bit of odd edge-case behaviour, that's really a result of the way the test is written. Basically, everything is happening in the scope of a single Hibernate session/txn (see conversations above). So the test is fetching back the object graph it just created (with the out of order Set), rather than fetching data from the database.
If you force a separate transaction then you get the behaviour you are looking for and the 'order by' works as expected. E.g.
User.withNewSession{ session ->
def User foundUser = User.get(user.id);
for (post in foundUser.posts) {
println "Post message> ${post.message}"
}
}
Code courtesy of ndtreviv.

If you use a list instead of a Set (the default) the framework will maintain the order for you.
List posts = new ArrayList()
static hasMany = [ posts : Post ]

Owens answer got me out of the confusion i was in. i was trying to the the ordering defined on a relationship between users (1) and posts (many), but when i wrote the initial tests they were failing as using User.get (u.id) was in the same session - and so was just reading out of the cache and they came back in the order i'd written tem not newest first as i'd expected.
I then rewrote the test across two sessions and low and behold in the second session this time returned the posts in desc order.
So you just have to be careful. if you are in the same original session that creates the posts then you have to use User.get (u.id).posts.sort().
All these little gotchas with not understanding properly how the session cache and underlying DB work in the scope of the same session/transaction. makes your brain ache sometimes.
whilst we are noting things - this errored in integration test in 3.2.5, but i spotted a thread by Jeff and the fix that had gone. So i upgraded to grails 3.2.6 last night and this test now works
void "test query"() {
given:"a user and where query for users, and posts "
User u
when: "create a post for user "
User.withNewSession { session ->
u = new User(username: 'will')
u.save(flush: true, failOnError: true)
Post p1 = new Post(comment: [food: "bought coffee and cake "])
Post p2 = new Post(comment: [dinner: "bought wine and dinner"])
Post p3 = new Post(comment: [view: "spectacular view of lake and sunset"])
u.addToPosts(p1)
u.addToPosts(p2)
u.addToPosts(p3)
u.save(flush: true)
if (u.hasErrors())
println "error saving posts on user u : ${u.errors}"
def postList = User.get(u.id).posts
postList.each { println "query via user.list using same session > $it.dateCreated : $it.comment" }
Post.findAll().each { println "query via Post using same session > $it.dateCreated : $it.comment" }
}
//because still in same session it just returns the order from the 1st level cache - so force a
//new session and let the DB do the sort
def lookupPosts
User.withNewSession{ session ->
User uNew = User.get(1)
assert uNew
lookupPosts = uNew.posts
lookupPosts.each {println "query via user in new session > $it.dateCreated : $it.comment" }
}
then: " check post was added"
!u.hasErrors ()
lookupPosts.size() == 3
lookupPosts[1].comment.dinner == "bought wine and dinner"
}

Related

How can I use Spring Data Neo4j to create a Node with new Relationships to existing Nodes

My use case is that I want to send a Message from one User to another, by creating a Message node in between two Users. Here is the JSON I would like to send from the client, and my Message class:
{"text":"Hello!","recipient":{"userId":"1823498e-e491-45fa-95bb-782a937a00ba"},"sent":{"userId":"6467b976-7ff6-4817-98e0-f3fbfca1413e"}}
#Node("Message")
#Data
public class Message
{
#Id #GeneratedValue private UUID messageId;
#NotNull
private String text;
#Relationship(type = "SENT",direction = Direction.INCOMING)
private User sent;
#Relationship("TO")
private User recipient;
}
But when I call Mono<Message> save(Message message); in the ReactiveNeo4jRepository, the DB unique constraint complains that Users with those usernames already exist! It seems like it's trying to reset all of the other properties for those two userIds to empty/defaults, presumably since they're not included in the JSON. So I get a constraint error when it sees two "FAKE" usernames.
2022-03-26 14:34:29.120 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.o.OutboundMessageHandler : [0xb08eba68][x.server.com:7687][bolt-128] C: RUN "MERGE (user:`User` {userId: $__id__}) SET user += $__properties__ RETURN user" {__id__="6467b976-7ff6-4817-98e0-f3fbfca1413e", __properties__={lastName: NULL, credentialsNonExpired: FALSE,userId: "6467b976-7ff6-4817-98e0-f3fbfca1413e", enabled: FALSE, firstName: NULL, password: "FAKE",accountNonExpired: FALSE, email: NULL,username: "FAKE", accountNonLocked: FALSE}} {bookmarks=["FB:kcwQx2Hl5+KYQJiGdiyzOa4EWSCQ"]}
2022-03-26 14:34:29.206 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.i.InboundMessageDispatcher : [0xb08eba68][x.server.com:7687][bolt-128] S: SUCCESS {fields=["user"], t_first=2}
2022-03-26 14:34:29.207 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.o.OutboundMessageHandler : [0xb08eba68][x.server.com:7687][bolt-128] C: PULL {n=-1}
2022-03-26 14:34:29.301 DEBUG 51863 --- [o4jDriverIO-2-4] o.n.d.i.a.i.InboundMessageDispatcher : [0xb08eba68][x.server.com:7687][bolt-128] S: FAILURE Neo.ClientError.Schema.ConstraintValidationFailed "Node(1) already exists with label `User` and property `username` = 'FAKE'"
UPDATE: I can accomplish what I need to using an ObjectMapper from Jackson and the Neo4j Driver directly, as follows. But I still would like to know how to this with SDN.
#Override
public Mono<UUID> save(Message message)
{
String cypher = "MATCH (a:User),(b:User) where a.userId = $fromUserId and b.userId = $toUserId CREATE (a)-[:SENT]->(m:Message {messageId: $messageId, text : $text})-[:TO]->(b)";
Map<String,Object> objMap = persistenceObjectMapper.convertValue(message,mapType);
return Mono.from(driver.rxSession().writeTransaction(tx -> tx.run(cypher,objMap).records())).then(Mono.just(message.getMessageId()));
}
It seems like it's trying to reset all of the other properties for those two userIds to empty/defaults
The problem you are facing is rooted in the handling of the incoming Message object.
As far as I understood this, you are directly saving the Message with the attached, de-serialized Users.
You have to fetch the Users from the database first. SDN does no pre-fetch. One way to do this is to create an additional repository for the User entity.
Given a MovieService that gets called by the controller, you would end up with something like this:
#Transactional
public Message saveNewMessage(Message newMessage) {
UUID recipientId = newMessage.getRecipient().getUserId();
UUID senderId = newMessage.getSent().getUserId();
User recipient = userRepository.findById(recipientId).orElseGet(() -> {
User user = new User();
user.setName("new recipient");
return user;
});
User sender = userRepository.findById(senderId).orElseGet(() -> {
User user = new User();
user.setName("new sender");
return user;
});
newMessage.setRecipient(recipient);
newMessage.setSent(sender);
return messageRepository.save(newMessage);
}
Tested this with:
CREATE CONSTRAINT username ON (user:User) ASSERT user.name IS UNIQUE
for 4.3 and/or for 4.4
CREATE CONSTRAINT username FOR (user:User) REQUIRE user.name IS UNIQUE
I created an example for your use case: https://github.com/meistermeier/neo4j-issues-examples/tree/master/so-71594275
There I also a user properties erasing call stack (/clearusers) that shows what I think happens in your application.
Edit: Since I usually advice not to create a repository for every entity class, you could also use the Neo4jTemplate. I have added the saveNewMessageAlternative to the example.

Grails 2.4.4 removing all items from a hasmany String relationship

I have a User class that hasMany organizations, and organizations are String UIDs.
class User implements Serializable {
...
List organizations
static hasMany = [organizations: String]
...
}
To update an User instance, I need to remove all the Strings from "organizations" before adding what the updated uids, but it doesn't work. The same organizations are resaved into the collection after I delete them and save the User instance.
I tried many ways:
// 1
user.organizations.clear()
// 2
def orgsToDelete = [] // this is to avoid concurrent modification exception
orgsToDelete += user.organizations
orgsToDelete.each { orguid ->
user.removeFromOrganizations(orguid)
}
After any of 1 or 2, the user.organizations is empty, but when I do:
user.save(flush:true)
I get the same organizations that where before the clear/removeFromOrganizations
I can't do user.organizations.each { it.delete() } because the items are not domain classes but Strings.
Another weird thing is I have a custom validator to check if the collection has any items, and it seems it doesn't gets any errors, event the organizations is empty, this is my validator:
organizations validator: { val, obj ->
if (obj.organizations.size() == 0) return false
return true
}
When I do this on the controller update action, it says hasErrors() == false
if (userInstance.hasErrors())
{
println "has errors"
respond userInstance.errors, view:'edit'
return
}
Any ideas?
Here is the controller: https://github.com/ppazos/cabolabs-ehrserver/blob/master/grails-app/controllers/com/cabolabs/security/UserController.groovy#L199-L237
Use your own domain class instead of String as collection's elements, like:
class Organization {
String name
static belongsTo = [ User ]
}
then you should be able to properly remove the children by
user.organisations.clear()
user.save()
The reason for the behaviour you a facing is, that the String instances are mapped to a table which has no back-refs to the user. That leads to the situation, that the records in that string table are not actually removed and are loaded upon the next database call.
I have read your project code and you have the DataSource bad set. You are using a H2 memory BBDD, but using the MySQL driver instead (in the development environment). It doesnt look good. Set it properly, and try again.
I have test your model, and used:
user.organizations.clear()
without any problems, even using your custom validator.
In other hand you should use a Service instead for all the business logic, not a controller. Because a service is transactional by default.
Here are the changes I did, and works:
// List organizations = []
static hasMany = [organizations: String]
static mapping = {
password column: '`password`'
// organizations lazy: false
}
Removing List organizations. Using the H2 datasource with this config at DataSource development environment:
dbCreate = "create-drop"
url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
username = "sa"
password = ""
And this code works like a charm:
def clearOrganizationFromUser(){
//creating user with timestamp
User user = new User(username: 'username' + new Date(), password: 'password', email: 'email#email.es', accountExpired: false, accountLocked: false,
passwordExpired: false)
List<String> organizations = []
10.times{
organizations << "organization $it"
}
organizations.each{ String organization ->
user.addToOrganizations(organization)
}
user.save()
println "Organizations after saving: ${user.organizations}"
//Its not neccesary but I did it for showing that organization are persisted in DDBB
User newUserFromDDBB = User.get(user.id)
//deleting organization
newUserFromDDBB.organizations.clear()
newUserFromDDBB.save()
println "---> Organizations after deleting: ${newUserFromDDBB.organizations}"
}
Hope It works. If do, please mark as solved.

grails executeUpdate flushing error in integration test

I have a service which updates a db column. The update is done using executeUpdate. In my test after the service method I'm trying to load the record. The record loads and every field is populated except for the one I just updated in the service.
To make it stranger, when I run the code normally through a browser it works perfectly. I can look in the database and see that the field is being persisted. It's only the integration test that doesn't work. It's got to be some type of hibernate session issue with the dirty field.
Here is my stripped down code. I left out the controller info. My test calls the controller, the controller calls the service.
class BasicProfile {
static hasMany = [ photos:Photo ]
}
class Photo {
BasicProfile basicProfile
String caption
String publicId
static belongsTo = [ basicPofile:profile ]
}
class PhotoService {
def updateCaption() {
...
Photo.executeUpdate("update Photo p set p.caption = ? where p.basicProfile.id = ? and p.publicId = ? ",
[newCaption, profile.id, publicId])
...
}
}
void testUpdateCaption() {
...
controller.updateCaption() //controller just calls the photoService method
//get json result from controller to load publicId
...
Photo photo = Photo.findByPublicId(publicId)
assertEquals "my new caption", photo.caption //photo.caption is null, but the rest of the photo object is populated properly from the database load
}
I've added a breakpoint on the assert so I can view the photo instance. It's a valid instance and every field is populated with the data from when it was created (prior to calling controller.updateCaption(). But after calling controller.updateCaption(), the caption field should have valid data, but it's still null (the default when the instance is created).
That's probably a cache of your domain instance, try this:
void testUpdateCaption() {
controller.updateCaption()
//force the query in a clean hibernate session
Photo.withNewSession {
def photo = Photo.findByPublicId(publicId)
assertEquals "my new caption", photo.caption
}
}

Grails 'false' unique error

I have the following domain classes (shortened version)
class TalkingThread {
static hasMany = [comments:Comment]
Set comments = []
Long uniqueHash
}
and
class Comment {
static belongsTo = [talkingThread:TalkingThread]
static hasOne = [author:CommentAuthor]
Long uniqueHash
static constraints = {
uniqueHash(unique:true)
}
}
and
class CommentAuthor {
static hasMany = [comments:Comment]
Long hash
String name
String webpage
}
the following methods
public TalkingThread removeAllComments(TalkingThread thread){
def commentsBuf = []
commentsBuf += thread.comments
commentsBuf.each{
it.author.removeFromComments(it)
thread.removeFromComments(it)
it.delete()
}
if(!thread.save()){
thread.errors.allErrors.each{
println it
}
throw new RuntimeException("removeAllComments")
}
return post
}
public addComments(TalkingThread thread, def commentDetails){
commentDetails.each{
def comment = contructComment(it,thread)
if(!comment.save()){
comment.errors.allErrors.each{ println it}
throw new RuntimeException("addComments")
}
thread.addToComments(comment)
}
return thread
}
Sometimes I need to remove all of the comments from a TalkingThread and add comments that share the same uniqueHashes. So I call the removeAllComments(..) method, and then the addComments(..) method. This causes a
Comment.uniqueHash.unique.error.uniqueHash which caused by a supposedly deleted comment and a 'fresh' comment being added.
Should I be flushing? Maybe there is something wrong with my domain classes?
Edit Expansion of question.
Maybe this is a different question, but I thought that the session has deleted all associations and objects. Therefore the session state is aware that all TalkingThread comments have been deleted. Of course this has not been reflected in the database. I also assumed that the 'saving' of new Comments would be valid given that such 'saving' is consistent with the session state. However such 'saving' would be inconsistent with the database state. Therefore, my understanding of how grails validates objects in relation to session and database state is flawed! Any help in understanding the process of validating saves with respect to session and database states would also be appreciated.
If you want to remove all the Comments from a TalkingThread then you can use Hibernate's cascade behaviour.
Add
static mapping = {
comments cascade: 'all-delete-orphan'
}
to TalkingThread and then you can call comments.clear() followed by thread.save() which will delete the comments that were in the association.
There's a good article on Grails one-to-many-relationships here. The official Grails docs on it are here.

Grails service not saving Domain Object When triggered by Message Queue

I have a grails application that has a service that creates reports. The report is defined as:
class Report {
Date createDate
String reportType
List contents
static constraints = {
}
}
The service generates a report and populates contents as a list that is returned by createCriteria.
My problem is that my service claims to be saving the Report, no errors turn up, logging says that its all there, but when I go to call show from the controller on that report, it says contents is null.
Another relevant bit, my Service is called by an ActiveMQ message queue. The message originating from my report controller.
Controller:
class ReportController {
def scaffold = Report
def show = {
def rep = Report.get(params.id)
log.info("Report is " + (rep? "not null" : "null")) //says report is not null
log.info("Report content is " + (rep.contents? "not null" : "null")) //always says report.contents is null.
redirect(action: rep.reportType, model: [results: rep.contents, resultsTotal: rep.contents.size()])
}
}
My service that creates the report:
class ReportService {
static transactional = false
static expose = ['jms']
static destination = "Report"
void onMessage(msg)
{
this."$msg.reportType"(msg)
}
void totalQuery(msg)
{
def results = Result.createCriteria().list {
//This returns exactly what i need.
}
Report.withTransaction() {
def rep = new Report(createDate: new Date(), reportType: "totalQuery", contents: results)
log.info("Validation results: ${rep.validate()}")
if( !rep.save(flush: true) ) {
rep.errors.each {
log.error(it)
}
}
}
}
Is there something obvious that I'm missing here? My thought is that since all my unit tests work, that the hibernate context is not being passed through the message queue. But that would generate Exceptions wouldn't it? I've been beating my head on this problem for days, so a point in the right direction would be great.
Thanks,
You can't define an arbitrary List like that, so it's getting ignored and treated as transient. You'd get the same behavior if you had a def name field, since in both cases Hibernate doesn't know the data type, so it has no idea how to map it to the database.
If you want to refer to a collection of Results, then you need a hasMany:
class Report {
Date createDate
String reportType
static hasMany = [contents: Result]
}
If you need the ordered list, then also add in a List field with the same name, and instead of creating a Set (the default), it will be a List:
class Report {
Date createDate
String reportType
List contents
static hasMany = [contents: Result]
}
Your unit tests work because you're not accessing a database or using Hibernate. I think it's best to always integration test domain classes so you at least use the in-memory database, and mock the domain classes when testing controllers, services, etc.

Resources