Details
-
Type:
Improvement
-
Status:
Closed
-
Priority:
Major
-
Resolution: Fixed
-
Affects Version/s: 0.5.6
-
Fix Version/s: 1.0-RC2
-
Component/s: Persistence
-
Labels:None
Description
null should be assign to a domain attribute when it is nullable from a blank user input
Issue Links
- relates to
-
GRAILS-9276
Difficult to store an empty string in a nullable field
-
Activity
- All
- Comments
- Work Log
- History
- Activity
- Git Commits
What do you mean by blank user input here?
Blank HTML forms do not include their attributes when posting to a URL, so this is a non-issue.
If you mean:
def x = new SomeDomainClass()
x.someProperty = ""
...and that you expect someProperty to be null, I would say that is fundamentally wrong.
You jogged my memory on the blank attributes not being transmitted... but I can't seem to reproduce that. I created a basic HTML form with no values (one a select with a blank value), the other a input text where I delete the value in the browser before hitting submit. I tried with GET and POST, and with POST I tried AJAX submission as well (using the very latest Yahoo and with the dojo that comes with grails). Every time I'm getting it through and with no value; I believe that ends up being an empty string
I agree that setting a value to the empty string directly on the domain should probably not be coerced to a null; though I am open to that idea. I do think that a blank value should be null, not an empty string on the params.
The blank value is troublesome for me at the moment because I'd like to update my domain object's value to null. I wonder how rails does this off hand; I forget. I'm using the model.properties = params method now, so it is grails and/or SpringMVC that is doing the params to domain object binding. If I can't use it anymore... I guess I'll have to re-implement it, more or less. I have developed a scaffolding abstract base class and I like to generalize as much as possible. My domain models are numerous. Forcing every controller to manually build-out fill in the domain model from the params to then save would be lots of code that I don't think I should have to do.
Thanks for your time, Marc
Hmm well I don't think we should be getting blanks in params, maybe I'm hallucinating.
I will check tomorrow.
For now, can't you add this to your controllers / beforeinterceptor:
def newparams = [:]
params.each() { k, v ->
if (value instanceof String) {
if (!value.trim().length()) {
return
}
}
newparams[k] = v
}
params.clear()
params.putAll(newparams)
Somethign like that should filter out all the blank inputs for you prior to binding.
That's a nice try but it doesn't work; I tried this already. When you do model.properties = params it appears as if grails/SpringMVC is not using it; presumably it's going to the request object to get the parameters. To test this, just do params.clear() and watch grails go save the original parameters any way. Very annoying and unexpected.
So, either I do generic data binding logic myself (seems hard) or I patch grails (seems easier) but problematic.
I just found a work-around. Basically, you need to avoid using the params instance directly for data binding since GrailsDataBinder has different behavior if it gets that. You need to feed it some other map (HashMap is convenient). Also, in your code, you skipped over the blanks... but in my case, I want the map to contain a null value so that it will set the corresponding property on the model to null.
If we change this behaviour then we'll have some other guy raising an issue saying that we should assign blank to nullable objects when there is a blank string. It is neither here nor there
Presently, one cannot use grails built-in binding if there's a property that won't accept the empty string. If someone wants empty strings, then let constraints indicate that is so and then bind accordingly. You don't have to pick one side or the other Graeme, let constraints indicate desired behavior.
BTW, IMO, I think the blank constraint should be false by default, and nullable true.
In Grails SVN head Grails' data binder now uses the params object and not the original request so Marc's code will work with SVN head as your workaround
If I implement Marc's solution in my controller class (ShoutController)I always get "object references an unsaved transient instance - save the transient instance before flushing: Shout" errrors, though the data will still be saved to the database. Not too sure why this happens. I also tried to switch from the params object to another HashMap defined inside the Controller but the result remained the same. ![]()
[55113] StackTrace Sanitizing stacktrace:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Shout
at org.hibernate.engine.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:219)
at org.hibernate.type.EntityType.getIdentifier(EntityType.java:397)
at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:225)
at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:235)
at org.hibernate.type.TypeFactory.findDirty(TypeFactory.java:597)
at org.hibernate.persister.entity.AbstractEntityPersister.findDirty(AbstractEntityPersister.java:3123)
at org.hibernate.event.def.DefaultFlushEntityEventListener.dirtyCheck(DefaultFlushEntityEventListener.java:479)
at org.hibernate.event.def.DefaultFlushEntityEventListener.isUpdateNecessary(DefaultFlushEntityEventListener.java:204)
at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:127)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:196)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:76)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:26)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
at org.springframework.orm.hibernate3.HibernateAccessor.flushIfNecessary(HibernateAccessor.java:390)
at org.codehaus.groovy.grails.orm.hibernate.support.GrailsOpenSessionInViewInterceptor.flushIfNecessary(GrailsOpenSessionInViewInterceptor.java:67)
at org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor.postHandle(OpenSessionInViewInterceptor.java:181)
at org.codehaus.groovy.grails.orm.hibernate.support.GrailsOpenSessionInViewInterceptor.postHandle(GrailsOpenSessionInViewInterceptor.java:56)
at org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter.postHandle(WebRequestHandlerInterceptorAdapter.java:61)
at org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(GrailsDispatcherServlet.java:247)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:790)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:476)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:441)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:367)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at org.mortbay.jetty.servlet.Dispatcher.forward(Dispatcher.java:268)
at org.mortbay.jetty.servlet.Dispatcher.forward(Dispatcher.java:126)
at org.codehaus.groovy.grails.web.mapping.filter.UrlMappingsFilter.doFilterInternal(UrlMappingsFilter.java:104)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:119)
at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:55)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.codehaus.groovy.grails.web.servlet.filter.GrailsReloadServletFilter.doFilterInternal(GrailsReloadServletFilter.java:155)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:54)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:96)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:183)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:138)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:365)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at org.mortbay.jetty.Server.handle(Server.java:295)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:503)
at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:841)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:639)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:210)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:379)
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:361)
at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
[55117] StackTrace Sanitizing stacktrace:
org.springframework.dao.InvalidDataAccessApiUsageException: object references an unsaved transient instance - save the transient instance before flushing: Shout; nested exception is org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Shout
at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:634)
at org.springframework.orm.hibernate3.HibernateAccessor.convertHibernateAccessException(HibernateAccessor.java:412)
at org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor.postHandle(OpenSessionInViewInterceptor.java:184)
at org.codehaus.groovy.grails.orm.hibernate.support.GrailsOpenSessionInViewInterceptor.postHandle(GrailsOpenSessionInViewInterceptor.java:56)
at org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter.postHandle(WebRequestHandlerInterceptorAdapter.java:61)
at org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(GrailsDispatcherServlet.java:247)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:790)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:476)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:441)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:367)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at org.mortbay.jetty.servlet.Dispatcher.forward(Dispatcher.java:268)
at org.mortbay.jetty.servlet.Dispatcher.forward(Dispatcher.java:126)
at org.codehaus.groovy.grails.web.mapping.filter.UrlMappingsFilter.doFilterInternal(UrlMappingsFilter.java:104)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:119)
at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:55)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.codehaus.groovy.grails.web.servlet.filter.GrailsReloadServletFilter.doFilterInternal(GrailsReloadServletFilter.java:155)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:54)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:96)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:75)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:183)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:138)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1089)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:365)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at org.mortbay.jetty.Server.handle(Server.java:295)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:503)
at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:841)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:639)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:210)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:379)
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:361)
at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Shout
[55119] StackTrace Sanitizing stacktrace:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Shout
[55119] StackTrace Sanitizing stacktrace:
org.springframework.dao.InvalidDataAccessApiUsageException: object references an unsaved transient instance - save the transient instance before flushing: Shout; nested exception is org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Shout
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: Shout
Seems your domain model object is referring to a non-persistent object but I can't know for sure without seeing your domain model and binding code. Looking at Hibernate's API for this exception was a clue.
Are you sure the changes you made to params actually had an effect? See posts from Graeme and me here... unless of course you're using some SVN head since Graeme fixed one of the issues.
The main problem in my case seems to be the self reference to the domain model but I don't understand why...
I think this doesn't have to do with this JIRA issue. Ask the mailing list. (sorry to punt your inquiry)
Graeme, you claimed in this thread in September that changes to params will be used instead of the request. I'm looking at the latest and this line:
http://svn.grails.codehaus.org/browse/grails/trunk/grails/src/web/org/codehaus/groovy/grails/web/metaclass/BindDynamicMethod.java?r=5542#l106
suggests the request object is used instead of params.
David - Sorry this is fixed now, which will provide your workaround. I'm not sure this issue is valid anymore, if we changed the behaviour then someone else would raise another saying blank values should be assigned as blank
I disagree... the constraints let you know whether the developer wants "" or null to represent no data. If nullable is true then it's nulls, otherwise it's "".
Ok, so now if a String property is nullable null is used instead of a blank string
I realize this is an old issue, but we just ran into it recently. It seems like there's a conflation of two different things in the database, whether a column is nullable and the column's default value. This fix means that the nullable constraint forces us to use both.
In our legacy database, we allow a null value, but we differentiate between a null value and an empty string, and we want the value we enter in the domain object to end up in the database. Is there a way to do so? To force grails to save an empty string on a nullable field?
Okey, so the issue has been fixed for string type properties, but what about binding associations ? let's say author.id param is blank when binding the request parameters to book instance, it won't set the author instance to null. Grails explicitly expects the param value to be 'null' - This should be consistent weather the property is of type string or association
I think this is a big deal.
I think I'll be forced to essentially write my own generic data binder at the controller because of this. But I suspect it'll be hard so there will be corner cases I can't easily address.