I'm designing a system in which posts/discussions between users can be upgraded to become tickets. At one particular place I'm trying to create a one-to-one optional relationship but am running into certain issues. A condensed version of the entities in the spotlight is given below.
Rules:
A Post can become a Ticket if required. (optional)
A Ticket must have a Post. (mandatory)
Post.groovy
class Post {
String title
String description
String postedBy
Ticket ticket
static hasMany = [comments: Comment]
static constraints = {
title(blank:false)
description(blank:false)
postedBy(blank:false)
ticket (nullable:true,unique:true)
}
}
Ticket.groovy
class Ticket {
String title
String description
String postedBy
Post post
static hasMany = [responses: Response]
static constraints = {
title(blank:false)
description(blank:false)
postedBy(blank:false)
post (nullable:false,unique:true)
}
}
This works to some extent. I can:
Create a Post leaving the ticket attribute null If and when the post is upgraded to become a ticket
I can explicitly set the Post's ticket attribute to point to the parent ticket.
However, this mapping isn't enforced at the domain level. It leaves room for a situation where Ticket1 points to Post1, but Post1 points to Ticket2 instead.
I tried using a static hasOne = [post: Post] in the Ticket class but later learned that it mandates the presence of a static belongsTo = [ticket: Ticket] in the Post class and this becomes a mandatory 1-to-1 relationship which is not what I'm looking for.
Is there a way to achieve this 1-to-1 optional mapping in this scenario? Any pointers would be most helpful.
You could consider making a custom validator like
class Post {
// Other fields
Ticket ticket
static constraints = {
// Other constraints
ticket (nullable:true,unique:true, validator: { val, obj ->
if(val) {
return val.post == obj
}
})
}
}
Would this solve your problem?
Related
So I'm currently building a Grails application, and I am trying to allow users to create posts, and those posts have comments as well.
The association I am trying to create is something like:
A User can have 0 to many Posts, and
Posts can have 0 to many Comments
I tried to set this up with my domain classes as the following:
class User implements Serializable{
static hasMany = [created_posts: Post]
String username
String password
}
class Post {
static hasMany = [comments: Comment]
String description
}
class Comment {
String comment_body
static belongsTo = [post: Post]
}
I tried playing with the mappedBy property as well, like the following:
class User implements Serializable{
static hasMany = [created_posts: Post]
static mappedBy = [created_posts: 'creator']
String username
String password
}
class Post {
static hasMany = [comments: Comment]
static belongsTo = [creator: User]
String description
}
class Comment {
String comment_body
static belongsTo = [post: Post]
}
But it would still not work. The code I am using to create this is the following:
Post new_post = new Post(description: "testing").save()
User user = User.get(springSecurityService.principal.id)
user.addToCreated_posts(new_post).save()
Unfortunately, Grails cannot seem to execute the last line, and sometimes it would not compile at all, giving a :bootRun error.
I am unsure of how to create the proper associations between the Users, Posts, and Comments. Any help is welcome and greatly appreciated!
EDIT: my current code: (compiles, but does not save to user's set)
class User implements Serializable{
static hasMany = [createdPosts: Post]
static mappedBy = [createdPosts: 'creator']
String username
String password
}
class Post {
static hasMany = [comments: Comment]
static belongsTo = [creator: User]
String description
static constraints = { creator nullable: true }
}
class Comment {
String comment_body
static belongsTo = [post: Post]
}
The code I am using in my controller:
User user = User.get(springSecurityService.principal.id)
Post new_post = new Post(description: "testing description", creator: user).save()
user.save()
System.out.println("post saved to user? " + user.getCreatedPosts())
Output is:
post saved to user? []
The issue is more than likely the fact that the save() function does not return the object that was saved.
Initially, I changed your code to:
Post new_post = new Post(description: "testing")
new_post.save()
User user = User.get(springSecurityService.principal.id)
user.addToCreated_posts(new_post).save()
Which did compile. But, there is one issue remaining - instead of running user.addToCreated_posts, you'll have to set the property creator of new_post, since you did not explicitly specify that it can be null when you created your Post domain class. Therefore, the call to save() will silently fail.
If you change the code to:
User user = User.get(springSecurityService.principal.id)
Post new_post = new Post(description: "testing", creator: user)
new_post.save()
Your code should work.
It is also probably a good idea to replace all of your calls to save() with save(failOnError: true), so that when a save fails, an exception is thrown. You can catch this exception, and extract error messages from the exception. Usually, the error messages are related to failed validations for the domain object.
This was tested on Grails 3.2.7, using H2 (the default, in-memory database packaged with Grails).
EDIT Here's an updated snippet based on the update that was made to the question.
You are running:
User user = User.get(springSecurityService.principal.id)
Post new_post = new Post(description: "testing description", creator: user).save()
user.save()
System.out.println("post saved to user? " + user.getCreatedPosts())
I would change this to:
User user = User.get(springSecurityService.principal.id)
Post new_post = new Post(description: "testing description", creator: user)
new_post.save(failOnError: true)
System.out.println("post saved to user? " + user.getCreatedPosts())
The reason I would do this is because you aren't saving the user - you are saving the post. When you get created posts for a user, Grails searches the database for posts where the creator_id column is set to the id of the user you called the method from.
Also, I would make sure that you did not remove the following line from User.groovy:
static hasMany = [createdPosts: Post]
static mappedBy = [createdPosts: "creator"]
You will absolutely need the following in Post.groovy:
static belongsTo = [creator: User]
This way, Grails knows how it is supposed to look up Posts for a user.
Once you make these changes, make sure you start with a fresh DB - for H2 in memory, this should be done automatically, so long as you are using the development environment - and restart your server.
These are my domain objects
User{
hasMany = {roles: UserRole}
}
UserRole{
User user
Role role
}
Role{
String authority
}
I need to find users based on their Role. For that I am trying to use the following criteria:
def role_admin = Role.findByAuthority('ROLE_ADMIN')
def criteria = new DetachedCriteria(User).build {
roles{
role{
idEq(role_admin.id)
}
}
}
result.users = criteria.list(params)
result.total = criteria.count()
The above will always return one result, even though I have verified by looking at the database directly that there should be more results. The params passed to list are correct, but I tried removing them just to be sure. I can't see what is wrong with the above, any suggestions ?
I also tried this
roles{
role{
eq("authority","ROLE_ADMIN")
}
}
But it is throwing exception:
Unknown column 'role_alias2_.authority' in 'where clause'
this works for me:
def criteriaUsers = UserRole.createCriteria()
def users = criteriaUsers.listDistinct{
eq("role", role_admin)
}.user
Side note: from the nomenclature of your classes it looks like you are using spring-security-core plugin. In my humble opinion the hasMany = [roles: UserRole] is redundant as the association User - Role is already being modeled in the UserRole class.
My web interface has an ajax call to update a photo's caption. A post sends the caption and the publicId of the photo to a service.
The service has
Photo photo = Photo.findByPublicId(params.publicId)
photo.caption = params.caption
photo.save()
However I have read in Burt Beckwith's grails book this is not secure. As-is a hacker could post any publicId to my service and update the
caption of a photo that doesn't not belong to their session. I need some GORM advice on how to write the update query to update only photos belonging
to the current user's session. Due to the number of joins involved I am lost. I am familiar with getting the profile/user:
User user = User.load(springSecurityService.principal.id)
Profile profile = Profile.findByUser(user, [lock:true])
but not the one query that would join everything for the entire update, instead of Profile.findByUser(user, [lock:true]).photoAlbum.getPhotoWherePublicId(publicId) or something that seems it would make 4 different sql calls.
The domain schema I have with the hierarchy in question is :
//user from springsecurity for session/login management
class User {
//no reference to profile
}
class Profile {
PhotoAlbum photoAlbum
User user //reference to user
static constraints = {
photoAlbum(nullable:true)
}
}
class PhotoAlbum {
static hasMany = [photos:Photo]
static belongsTo = [profile:Profile]
}
class Photo {
static belongsTo = PhotoAlbum
String caption
String publicId
}
Maybe with a criteria or namedQuerie this could be done.
Something like this may work:
First make a small change to your Photo class
class Photo {
PhotoAlbum photoAlbum
static belongsTo = [photoAlbum: PhotoAlbum]
String caption
String publicId
}
and try with this criteria
Photo.withCriteria{
eq 'id',params.publicId
photoAlbum {
eq 'profile',profile
}
}
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.
I'm having an issue with grails. I have a domain that looks like:
class Book {
static belongsTo = Author
String toString() { title }
Author bookAuthor
String title
String currentPage
static constraints = {
bookAuthor()
title(unique:true)
currentPage()
}
}
The main thing to note is that I have title(unique:true) to avoid from adding the same book twice. However, this is causing issues. In the controller I have created:
def populate = {
def bookInstance = new Book()
def dir = 'C:/currentBooks.txt'
def bookList
bookList = readFile(dir) //read file and push values into bookList
int numOfBooks = bookList.size()
numOfBooks.times {
bookInstance.setBookAuthor(bookList.author[it])
bookInstance.setTitle(bookList.title[it])
bookInstance.setCurrentPage(bookList.title[it])
bookInstance.save()
}
}
I call populate to read a file and populate the database with new Books. The problem is that I want to update it with new values. For instance, lets say that the book already exists in the database but I have read farther into the book and want to change the currentPage so the data is changed in the file and populate is called but doesn't update the page because the title already exists.
Can someone explain how to update the results with the new values?
First of all, you need a key for your Book domain object. You have the title marked as unique, which suggests you want to use that to uniquely identify a Book. I'd recommend against that (what happens when two books have the same title?) and use the id grails provides by default. That means you'll have to store the id in your currentBooks.txt in addition to your other fields.
Once you've got an id, you can try loading an existing record from the database. If not, create a new one. For Example:
def dir = 'C:/currentBooks.txt'
def bookList
bookList = readFile(dir) //read file and push values into bookList
int numOfBooks = bookList.size()
numOfBooks.times {
def bookInstance.get(bookList.id[it])
if (!bookInstance) {
bookInstance = new Book()
}
bookInstance.setBookAuthor(bookList.author[it])
bookInstance.setTitle(bookList.title[it])
bookInstance.setCurrentPage(bookList.title[it])
bookInstance.save()
}
Alternatively, you could use the title as the id. This is a bad idea as indicated above, but it saves having to keep track of a separate id and change the format of currentBooks.txt. With Book defined as below, you could call Book.get(bookList.title[it]):
class Book {
static belongsTo = Author
String toString() { title }
Author bookAuthor
String title
String currentPage
static constraints = {
bookAuthor()
title(unique:true)
currentPage()
}
static mapping = {
id name: 'title', generator: 'assigned'
}
}