Recently, I implemented soft delete for most of my entities in the app (at least the ones I needed).
The implementation looks like this:
Goal.groovy
class Goal {
String definition;
Account account;
boolean tmpl = false;
String tmplName;
Goal template
Timestamp dateCreated
Timestamp lastUpdated
Timestamp deletedAt
static belongsTo = [
account: Account,
template: Goal
]
static hasMany = [perceptions: Perception, sessions: RankingSession]
static mapping = {
autoTimestamp true
table 'goal'
definition type: 'text'
tmplName column: '`tmpl_name`'
perceptions sort:'title', order:'asc'
dateCreated column: 'date_created'
lastUpdated column: 'last_updated'
deletedAt column: 'deleted_at'
}
...
def beforeDelete() {
if (deletedAt == null) {
Goal.executeUpdate('update Goal set deletedAt = ? where id = ?', [new Timestamp(System.currentTimeMillis()), id])
}
return false
}
...
Perception.groovy
class Perception {
String title
String definition
Goal goal
Timestamp dateCreated
Timestamp lastUpdated
Timestamp deletedAt
static hasMany = [left: Rank, right: Rank]
static mappedBy = [left: "left", right: "right"]
static belongsTo = [goal: Goal]
static namedQueries = {
notDeleted {
isNull 'deletedAt'
}
}
static mapping = {
autoTimestamp true
table 'perception'
definition type: 'text'
dateCreated column: 'date_created'
lastUpdated column: 'last_updated'
deletedAt column: 'deleted_at'
}
static constraints = {
title blank: false, size: 1..255
definition nullable: true, blank: true, size: 1..5000
goal nullable: false
lastUpdated nullable: true
deletedAt nullable: true
}
/**
* before delete callback to prevent physical deletion
*
* #return
*/
def beforeDelete() {
if (deletedAt == null) {
Perception.executeUpdate('update Perception set deletedAt = ? where id = ?', [new Timestamp(System.currentTimeMillis()), id])
}
return false
}
}
Rank.groovy
class Rank {
Perception left
Perception right
Integer leftRank
Integer rightRank
RankingSession session
static belongsTo = [session: RankingSession]
static mapping = {
table 'rank'
}
static constraints = {
leftRank range: 0..1, nullable: true
rightRank range: 0..1, nullable: true
left nullable: false
right nullable: false
session nullable: false
}
}
My problem happens on deletion (logical deletion). I perform the deletion through service class the following way:
GoalService.groovy
#Transactional
class GoalService {
/**
* Deletes goal
*
* #param goal
* #return
*/
def deleteGoal(Goal goal) {
if (goal.tmpl == true) {
throw new ValidationException("Provided object is a template!")
}
def perceptions = Perception.notDeleted.findAllByGoal(goal)
for (perception in perceptions) {
perception.delete()
}
goal.delete()
}
}
I have a use-case, which work under one condition and trows an exception under another.
#1 Existing Goal with number of assigned perceptions to it. Deletion works as expected: The Goal and Perceptions are marked as deleted.
#2 Goal with Perceptions + Number of Rank objects linked to Perceptions.
When I am trying to delete such a goal, I am getting an exception:
Error 2015-03-23 14:52:10,294 [http-nio-8080-exec-9] ERROR spi.SqlExceptionHelper - Column 'left_id' cannot be null
| Error 2015-03-23 14:52:10,357 [http-nio-8080-exec-9] ERROR errors.GrailsExceptionResolver - MySQLIntegrityConstraintViolationException occurred when processing request: [POST] /triz/rrm/goal/1/delete - parameters:
SYNCHRONIZER_TOKEN: 57fda8f2-8025-45e0-ac60-592234f54ef1
SYNCHRONIZER_URI: /triz/rrm/goals
Column 'left_id' cannot be null. Stacktrace follows:
Message: Column 'left_id' cannot be null
Line | Method
->> 411 | handleNewInstance in com.mysql.jdbc.Util
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| 386 | getInstance in ''
| 1041 | createSQLException in com.mysql.jdbc.SQLError
| 4237 | checkErrorPacket in com.mysql.jdbc.MysqlIO
| 4169 | checkErrorPacket . in ''
| 2617 | sendCommand in ''
| 2778 | sqlQueryDirect . . in ''
| 2834 | execSQL in com.mysql.jdbc.ConnectionImpl
| 2156 | executeInternal . in com.mysql.jdbc.PreparedStatement
| 2441 | executeUpdate in ''
| 2366 | executeUpdate . . in ''
| 2350 | executeUpdate in ''
| 129 | doCall . . . . . . in triz.rrm.RrmGoalController$_delete_closure5
| 127 | delete in triz.rrm.RrmGoalController
I've already tried everything, including:
physically dropped all constraints
used "cascade: 'save-update'" on relationships
Nothing helps, the only thing I can understand is the fact that it is related to cascading, but why is GORM trying to cascade 'delete' if in reality I am updating the objects?
You are getting the exception because Rank has a reference to Perception, but you did not specify belongsTo on either side.
You have this in Rank:
Perception left
That is why you are getting Column 'left_id' cannot be null. Stacktrace follows....
So to solve the problem either delete rank objects having r/ship with each perception you are deleting or specify belongsTo = [left: Perception] with in Rank.
Related
In our legacy system(MSSQL) we use int as the type of the id and I don't want to interfere with that so I change the type like this:
static mapping = {
id column: "id", type: 'int'
But then in the bootstrap where I create some sample records, it no longer works.
Error message:
Configuring Spring Security UI ...
... finished configuring Spring Security UI
2017-03-04 10:31:20.381 ERROR --- [ main] o.h.p.access.spi.SetterMethodImpl : HHH000123: IllegalArgumentException in class: com.buffer.ProdBuffer, setter method of property: id
2017-03-04 10:31:20.385 ERROR --- [ main] o.h.p.access.spi.SetterMethodImpl : HHH000091: Expected type: java.lang.Long, actual value: java.lang.Integer
2017-03-04 10:31:20.419 ERROR --- [ main] o.s.boot.SpringApplication : Application startup failed
org.springframework.orm.hibernate5.HibernateSystemException: IllegalArgumentException occurred while calling setter for property [com.buffer.ProdBuffer.id (expected type = java.lang.Long)]; target = [com.buffer.ProdBuffer : (unsaved)], property value = [1] setter of com.buffer.ProdBuffer.id; nested exception is IllegalArgumentException occurred while calling setter for property [com.buffer.ProdBuffer.id (expected type = java.lang.Long)]; target = [com.buffer.ProdBuffer : (unsaved)], property value = [1]
at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:296)
at org.grails.orm.hibernate.GrailsHibernateTemplate.convertHibernateAccessException(GrailsHibernateTemplate.java:661)
at org.grails.orm.hibernate.GrailsHibernateTemplate.doExecute(GrailsHibernateTemplate.java:247)
at org.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:187)
It look's like I missed something... do I need to do something more than just change the type in "static mapping" ?
A part of the class:
class ProdBuffer {
String status
String product
String length
int productNo
static mapping = {
table 'LOBuffertv2'
id column: "id", type: 'integer'
product column: "Produkt", sqltype: "char", length: 150
length column: "Length", sqltype: "char", length: 25
productNo column: "ProductNo"
}
static constraints = {
status(inList:["Preliminary","Activ","Finished","Cancelled"])
status nullable: true
product nullable: true
length nullable: true
productNo nullable: true
}
}
I would suggest you to alter the tables in the DB to handle ids as BIGINT(20). And remove the id from domain class definitions and from mappings. Gorm will do the rest of job for you.
I found the solution of the problem in this link:Mapping ID to another type
I just declared id in the class explicitly as an int.
As I could understand in the link, I even not need to declare the type in the mapping.
class PBuff {
int id
String sawMill
String status
String product
String length
static mapping = {
id column: 'id', type: 'integer'
}
static constraints = {
status(inList:["Preliminary","Activ","Finished","Cancelled"])
sawMill nullable: true
status nullable: true
product nullable: true
length nullable: true
}
}
This is a basic mock of a web service where a Patient object is checked for eligibility. I have included the console output to show the test failing.
When I run this test in the groovyConsole it passes.
Basic Patient Class:
class Patient {
String firstName, lastName
Date dateOfBirth
}
Create the patient object:
Patient p1 = new Patient(
firstName: 'Mike', lastName: 'Smith',
dateOfBirth: new GregorianCalendar(1968, Calendar.AUGUST, 23).time
).save(flush: true, failOnError: true)
Mock setup:
This test normally queries a web service. I need to mock the service due to firewall issues. I build a tree that replicates the service call.
def eligibilityMock(Patient pat) {
def tree = { -> return [:].withDefault{ tree() } }
def member = tree()
if (pat.firstName == "Mike" && pat.lastName == "Smith") {
member.memberInfo.memberDetails.value.memberFirstName.value = 'Mike'
}
member
}
I call the mocked service
//def memberInfo = eligibilityMock(p1)
def member = humanaEligibilityMock(p1).memberInfo
I expect the firstName to match
assert p1.firstName == memberInfo.memberDetails?.value?.memberFirstName?.value
It seem the values match up, however the assertion fails.
p1.firstName == memberInfo.memberDetails?.value?.memberFirstName?.value
| | | | | | |
Mike | | | | [value:[:]] [:]
| | | [memberFirstName:[value:[:]]]
| | [value:[memberFirstName:[value:[:]]]]
| [memberInfo:[memberDetails:[value:[memberFirstName:[value:Mike]
false
I have created a domain class as given below, which contains an int and list of Integer properties.
class User {
int UserId
List<Integer> UserFriendsId
static constraints = {
}
User() {
this.UserId = 21
this.UserFriendsId=[1,2,3]
}
}
The table generated for this domain class while saving is as follows
mysql> select * from user;
+----+---------+---------------------+
| id | version | UserId |
+----+---------+---------------------+
| 1 | 0 | 21 |
| 2 | 0 | 21 |
| 3 | 0 | 21 |
+----+---------+---------------------+
3 rows in set (0.00 sec)
column for userFriendsId (ie: for list of integers) is not generated in this table user.
so how can solve this issue or can add list of integer in grails domain class.
The UserFriendsId List should be mapped as a GORM basic collection type and not simply be a list in the User domain class:
class User {
int userId
static hasMany = [userFriendsIds: Integer]
static mapping = {
userFriendsIds joinTable: [name: "user_id", column: "friend_id", type: Integer]
}
static constraints = {
}
User() {
}
}
Why not just make UserFriendsId a comma separated String?
class User {
int UserId
String UserFriendsId
static constraints = {
}
User() {
this.UserId = 21
this.UserFriendsId = "1,2,3"
}
}
Then:
for (userId in UserFriendsId.get(21).split(','))
{
println userId.toInteger()
/// Or do whatever ...
}
Here is the code to take a list of tags generated by params and passed into the service from the controller. I'm trying to filter the list of eateries to ONLY contain eateries with matching tag properties. I'm new to programming and grails so please forgive any silly mistakes:
class Eatery {
String name
String phone
Date lastVisited
List<Tag> tags
static hasOne = [location: Location]
static hasMany = [tags: Tag];
static constraints = {
name nullable: false, blank: false
phone nullable: true, blank: true
location nullable: true, blank: true
tags nullable: true, blank: true
lastVisited nullable: true, blank: true
}
public String toString() {
return name
}
}
Here is the service method:
Eatery getRandomEatery(List<Tag> tagList) {
List<Eatery> eateryList = Eatery.findAllByTags(tagList)
Random random = new Random()
Integer n = eateryList.size()
Integer selection = Math.abs(random.nextInt(n))
Eatery eatery = eateryList.get(selection)
return eatery
}
And here is the error:
Class
org.h2.jdbc.JdbcSQLException
Message
Parameter "#1" is not set; SQL statement: select this_.id as id2_0_, this_.version as version2_0_, this_.last_visited as last3_2_0_, this_.name as name2_0_, this_.phone as phone2_0_ from eatery this_ where this_.id=? [90012-164]
Around line 16 of grails-app/services/grubspot/RandomizerService.groovy
13://14:// }15: List<Eatery> eateryList = Eatery.findAllByTags(tagList)16: Random random = new Random()17: Integer n = eateryList.size()18: Integer selection = Math.abs(random.nextInt(n))19: Eatery eatery = eateryList.get(selection)
Change
List<Eatery> eateryList = Eatery.findAllByTags(tagList)
to
List<Eatery eateryList = Eatery.executeQuery("""
select e
from Eatery e left join e.tags as t
where t in (:tagList)""", [tagList: tagList])
I think you could make the query with a criteria with something similar to...
List<Eatery> eateryList = Eatery.withCriteria{
tags {
"in" "id", tagList.id
}
}
haven't tested it, but should give you an idea
I am developing a GRAILS application (I'm new to GRAILS and inherited the project from a previous developer). I'm slowly getting a small grasp for how GRAILS operates and the use of DOMAIN classes, hibernate etc. The MySQL db is hosted on Amazon and we're using ElasticCache.
Do any of you more knowledgeable folks know how I can go about converting the following SQL statement into domain classes and query criteria.
if(params?.searchterm) {
def searchTerms = params.searchterm.trim().split( ',' )
def resultLimit = params.resultlimit?: 1000
def addDomain = ''
if (params?.domainname){
addDomain = " and url like '%${params.domainname}%' "
}
def theSearchTermsSQL = ""
/*
* create c.name rlike condition for each search term
*
*/
searchTerms.each{
aSearchTerm ->
if( theSearchTermsSQL != '' ){
theSearchTermsSQL += ' or '
}
theSearchTermsSQL += "cname rlike '[[:<:]]" + aSearchTerm.trim() + "[[:>:]]'"
}
/*
* build query
*
*/
def getUrlsQuery = "select
u.url as url,
c.name as cname,
t.weight as tweight
from
(category c, target t, url_meta_data u )
where
(" + theSearchTermsSQL + ")
and
t.category_id = c.id
and t.url_meta_data_id = u.id
and u.ugc_flag != 1 " + addDomain + "
order by tweight desc
limit " + resultLimit.toLong()
/*
* run query
*
*/
Sql sqlInstance = new Sql( dataSource )
def resultsList = sqlInstance.rows( getUrlsQuery )
}
The tables are as follows (dummy data):
[Category]
id | name
-----------
1 | small car
2 | bike
3 | truck
4 | train
5 | plane
6 | large car
7 | caravan
[Target]
id | cid | weight | url_meta_data_id
----------------------------------------
1 | 1 | 56 | 1
2 | 1 | 76 | 2
3 | 3 | 34 | 3
4 | 2 | 98 | 4
5 | 1 | 11 | 5
6 | 3 | 31 | 7
7 | 5 | 12 | 8
8 | 4 | 82 | 6
[url_meta_data]
id | url | ugc_flag
---------------------------------------------
1 | http://www.example.com/foo/1 | 0
2 | http://www.example.com/foo/2 | 0
3 | http://www.example.com/foo/3 | 1
4 | http://www.example.com/foo/4 | 0
5 | http://www.example.com/foo/5 | 1
6 | http://www.example.com/foo/6 | 1
7 | http://www.example.com/foo/7 | 1
8 | http://www.example.com/foo/8 | 0
domain classes
class Category {
static hasMany = [targets: Target]
static mapping = {
cache true
cache usage: 'read-only'
targetConditions cache : true
}
String name
String source
}
class Target {
static belongsTo = [urlMetaData: UrlMetaData, category: Category]
static mapping = {
cache true
cache usage: 'read-only'
}
int weight
}
class UrlMetaData {
String url
String ugcFlag
static hasMany = [targets: Target ]
static mapping = {
cache true
cache usage: 'read-only'
}
static transients = ['domainName']
String getDomainName() {
return HostnameHelper.getBaseDomain(url)
}
}
Basically, a url from url_meta_data can be associated to many categories. So in essence what I'm trying to achieve should be a relatively basic operation...to return all the urls for the search-term 'car', their weight(i.e importance) and where the ugc_flag is not 1(i.e the url is not user-generated content). There are 100K + of records in the db and these are imported from a third-party provider. Note that all the URLs do belong to my client - not doing anything dodgy here.
Note the rlike I've used in the query - I was originally using ilike %searchterm% but that would find categories where searchterm is part of a larger word, for example 'caravan') - unfortunately though the rlike is not going to return anything if the user requests 'cars'.
I edited the code - as Igor pointed out the strange inclusion originally of 'domainName'. This is an optional parameter passed that allows the user to filter for urls of only a certain domain (e.g. 'example.com')
I'd create an empty list of given domain objects,
loop over the resultsList, construct a domain object from each row and add it to a list of those objects. Then return that list from controller to view. Is that what you're looking for?
1) If it's a Grails application developed from a scratch (rather than based on a legacy database structure) then you probably should already have domain classes Category, Target, UrlMetaData (otherwise you'll have to create them manually or with db-reverse-engineer plugin)
2) I assume Target has a field Category category and Category has a field UrlMetaData urlMetaData
3) The way to go is probably http://grails.org/doc/2.1.0/ref/Domain%20Classes/createCriteria.html and I'll try to outline the basics for your particular case
4) Not sure what theDomain means - might be a code smell, as well as accepting rlike arguments from the client side
5) The following code hasn't been tested at all - in particular I'm not sure how disjunction inside of a nested criteria works or not. But this might be suitable a starting point; logging sql queries should help with making it work ( How to log SQL statements in Grails )
def c = Target.createCriteria() //create criteria on Target
def resultsList = c.list(max: resultLimit.toLong()) { //list all matched entities up to resultLimit results
category { //nested criteria for category
//the following 'if' statement and its body is plain Groovy code rather than part of DSL that translates to Hibernate Criteria
if (searchTerms) { //do the following only if searchTerms list is not empty
or { // one of several conditions
for (st in searchTerms) { // not a part of DSL - plain Groovy loop
rlike('name', st.trim())) //add a disjunction element
}
}
}
urlMetaData { //nested criteria for metadata
ne('ugcFlag', 1) //ugcFlag not equal 1
}
}
order('weight', 'desc') //order by weight
}
Possibly the or restriction works better when written explicitly
if (searchTerms) {
def r = Restrictions.disjunction()
for (st in searchTerms) {
r.add(new LikeExpression('name', st.trim()))
}
instance.add(r) //'instance' is an injected property
}
Cheers,
Igor Sinev