Grails validation fails after interchanging unique attribute values
Hi, I am trying to create an interface where users can create some custom enumeration with translations for different languages. For example the user can create an enumeration "Movie Genre". For this enumeration there might be an enumeration-value "Comedy" for which there might exist one ore more enumeration-value-translations for several languages.
As there must only be one translation for a specific language, I added a unique constraint to the enumeration-value-translation domain class. These are my domain classes right now:
class Enumeration {
String label
List<EnumerationValue> enumerationValues = new ArrayList<EnumerationValue>()
static hasMany = [ enumerationValues: EnumerationValue ]
static constraints = {
label(nullable: false, blank: false)
enumerationValues(nullable: true)
}
}
class EnumerationValue {
String label
List<EnumerationValueTranslation> enumerationValueTranslations = new ArrayList<EnumerationValueTranslation>()
static belongsTo = [ enumeration: Enumeration ]
static hasMany = [ enumerationValueTranslations: EnumerationValueTranslation ]
static constraints = {
label(nullable: false, blank: false, unique: 'enumeration')
enumeration(nullable: false)
enumerationValueTranslations(nullable: false)
}
}
class EnumerationValueTranslation {
String value
Language language
static belongsTo = [ enumerationValue: EnumerationValue ]
static constraints = {
value(nullable: false, blank: false)
language(nullable: true, unique: 'enumerationValue')
enumerationValue(nullable: false)
/* unique constraint as mentioned in description text */
language(unique: 'enumerationValue')
}
}
This works pretty fine so far. My problem occures when I update two enumeration-value-translations of the same enumeration-value in a way that the languages interchange. For example I have an
enumeration-value: "Comedy"
and some translations where the language is "accidentally" mixed up
translations for "Comedy"
language: german, value: "Comedy"
language: english, value "Komödie"
if the user recognizes that he mixed up the language, he might want to swap the languages and save the enumeration again. And this is where my error occures, because after swapping the languages the enumeration-value-translations unique constraint validates to false.
To debug this i simply tryed to print out the error causing translations before and after i processed the params, so:
Enumeration enumeration = Enumeration.get(params['id']);
println "before:"
enumeration.enumerationValues.each() { enumValue ->
enumValue.enumerationValueTranslations.each() { enumValueTr ->
println enumValueTr;
if(!enumValueTr.validate()) {
// print errors...
}
}
}
// swap languages:
// (this are the lines of codes that are actually executed, and cause the
// error. The actual processing of params looks different of course...)
// sets the language of "Comedy" to English
EnumerationValueTranslation.get(5).language = Language.get(1);
// sets the language of "Komödie" to German
EnumerationValueTranslation.get(6).language = Language.get(2);
println "after:"
enumeration.enumerationValues.each() { enumValue ->
enumValue.enumerationValueTranslations.each() { enumValueTr ->
println enumValueTr;
if(!enumValueTr.validate()) {
// print errors...
}
}
}
wich results to:
before:
EnumerationValueTranslation(value: Fantasy, language: en_US, enumerationValue: Fantasy)
EnumerationValueTranslation(value: Phantasie, language: de_DE, enumerationValue: Fantasy)
EnumerationValueTranslation(value: Comedy, language: de_DE, enumerationValue: Comedy)
EnumerationValueTranslation(value: Komödie, language: en_US, enumerationValue: Comedy)
after:
EnumerationValueTranslation(value: Fantasy, language: en_US, enumerationValue: Fantasy)
EnumerationValueTranslation(value: Phantasie, language: de_DE, enumerationValue: Fantasy)
EnumerationValueTranslation(value: Comedy, language: en_US, enumerationValue: Comedy)
validation fails: Property [language] of class [Translation] with value [Language(code: en_US)] must be unique
EnumerationValueTranslation(value: Komödie, language: de_DE, enumerationValue: Comedy)
validation fails: Property [language] of class [Translation] with value [Language(code: de_DE)] must be unique
at this state i havend deleted, or saved (or flushed in any way) anything - this is just the result after altering the objects. And as you can see, there really is no inconsistency in the actual data and the validation should'nt fail.
Might there be a mistake in the way i change the translations? I just fetched them by ID and simply updated the language - i tryed that out in a minimalistic example and it worked there...
It also works if i just create a deep copy of all enumeration-values and enumeration-value-translations and store that instead (which means that the validation really should'nt fail), but i think this is really not the way it should be done...
Another strange thing is, that the validation only fails if I iterate through the data. If i dont touch the data at all, no error occures, but the data isn't saved too, meaning that the folowing lines are causing the validations to be evaluated at all:
enumeration.enumerationValues.each() { ev ->
ev.enumerationValueTranslations.each() { evt ->
}
}
thats why i strongly believe that there must be some non-trivial problem... please let me know if there is anything else you need to know.
thanks for any help
Let me take another try :)
I'm looking at UniqueConstraint.processValidate(), and can suppose that its logic does not consider the exchange case.
Particularly, the code
boolean reject = false;
if (id != null) {
Object existing = results.get(0);
Object existingId = null;
try {
existingId = InvokerHelper.invokeMethod(existing, "ident", null);
}
catch (Exception e) {
// result is not a domain class
}
if (!id.equals(existingId)) {
reject = true;
}
}
else {
reject = true;
}
should iterate the obtained results and verify that the field value STILL violates uniqueness. In case of exchange, the other instance should be picked from a cache and have a new field value.
So I'd suggest you create an own descendant of UniqueConstraint and use it, unless anyone's going to patch Grails.
Related
I'm struggling to find the correct syntax to use in a Serilog filter expression to find a particular key/value pair in a dictionary within the event properties.
This is a contrived example, but illustrates the issue.
The relevent setup looks like this:
string exceptionFilter = "SomeDictionary.Other = 'Nope'";
LoggerConfiguration config = new LoggerConfiguration()
.Destructure.JsonNetTypes()
.MinimumLevel.ControlledBy(LevelSwitch)
.Enrich.FromLogContext()
.Enrich.With(new ExceptionEnricher())
.Filter.ByExcluding(exceptionFilter)
.WriteTo.MSSqlServer(
connectionString: ConnectionString,
sinkOptions: SinkOptions,
columnOptions: ColumnOptions
);
Logger = config.CreateLogger();
Then I log an event like this:
Log.Fatal(
ex,
"Real Bad {#SomeDictionary}",
new Dictionary<string, string>() {
{"Test", "Test"},
{"Other", "Nope"}
}
)
What should the expression to exclude SomeDictionary['Other'] = 'Nope' be?
If I pass in the event details as an object:
Log.Fatal(
ex,
"Real Bad {#SomeObject}",
new {
Test = "Test",
Other = "Nope"
}
)
Then the following expression works as expected:
SomeObject.Other = 'None'
Unfortunately, I am enriching our logs with a number of dictionaries that I need to be able to filter by and that syntax doesn't seem to work with them.
EDIT
If I serialize the log event properties, it appears that the issue is probably due to escaped quotation marks within the dictionary keys:
"SomeDictionary": {
"Elements": {
"\"Test\"": {
"Value": "Test"
},
"\"Other\"": {
"Value": "Nope"
}
}
}
Still not sure what I need to do to be able to filter on one of the elements.
Resolution
I switched from using Serilog.Filters.Expressions to Serilog.Expressions and the issue resolved itself. Now I can filter on dictionaries using expected syntax:
SomeDictionary['Other'] = 'Nope'
I'd like to know which is better to use, a database view or simply persist fields in the database.
For instance, I'm initializing win_count in the following view vw_stats:
...
CASE
WHEN game.in_decision = true AND game.user_score > game.opponent_score THEN 1
ELSE 0
END AS win_count,
...
Which is then mapped to a domain class:
package com.x
class Stat {
//fields here...
static mapping = {
table 'vw_stats'
version false
}
}
Or, should I persist the field winCount in the database using this domain class and manipulate it before saving?
package com.x
class Game {
//fields here...
Short winCount = 0
static constraints = {
//constraints here...
winCount nullable: false, range: 0..99
}
def beforeInsert(){
this.beforeUpdate()
}
def beforeUpdate(){
//other manipulations here...
if inDecision and userScore > opponentScore
winCount = 1
}
}
The issue with the view I find, is that it will generate a table when running the application and then I have to manually remove the table and run the code to generate the view.
Update #1
There might be an IO cost saving by persisting them in the database instead of the view?
Update #2
Forgot to mention, I should be able to apply an aggregate function on the resulting field in a service.
A third approach is to use a derived property. Like the view, the value is calculated on the fly.
package com.x
class Game {
//fields here...
Short winCount = 0
static constraints = {
//constraints here...
winCount nullable: false, range: 0..99
}
static mapping = {
winCount formula: 'CASE WHEN in_decision = true AND user_score > opponent_score THEN 1 ELSE 0 END'
}
}
Not fully understanding the exact application you are trying to achieve, have you thought about using transients and letting the domain instance calculate it when it is actually needed? This will avoid pre-calculating data that may not even be used.
I have a domain class
class Url {
UUID id
String url
static hasMany = [
indications:UrlIndication
]
...
}
And
class UrlIndication {
UUID id
String name
static belongsTo = Url
...
}
I want to choose urls so that it has all the necessary UrlIndication elements in a given list indicationsId.
For that I use an association and criteria like this one:
indications {
and {
indicationsId.each{
indication->
eq ('id',UUID.fromString(indication as String))
}
}
}
However, all I got is an empty result. Can you suggest any modifications/ other methods so that I can do this? Thanks in advance
Your query returned an empty list because it's the equivalent of the expression (pseudo-code): if 1 = 1 and 1 = 2 and 1 = 3
Such an expression would always be false. in or inList would not work for the reason #innovatism described.
In theory, Criteria's eqAll() or HQL's = ALL would work. But, I don't know for sure because I could not get either one to work.
What will work is to use inList to return a subset of Urls: those which contain at least one of the UrlIndication IDs. Then use Groovy's containsAll() to finish the job.
def ids = indicationsId.collect { UUID.fromString(it as String) }
Url.createCriteria()
.buildCriteria {
indications {
inList 'id', ids
}
}
.setResultTransformer(org.hibernate.Criteria.DISTINCT_ROOT_ENTITY)
.list()
.findAll {
it.indications.id.containsAll(ids)
}
Since the query has the potential to return duplicate Url instances, the ResultTransformer is set to return a unique list.
Finally, findAll() is used along with containsAll() to filter the list further.
Using eqAll (maybe)
Something like the following might work. Something funky is going on with Grails' HibernateCriteriaBuilder that causes the eqAll method to look up properties in the root entity; completely ignoring the sub criteria. So the following uses Hibernate directly. It didn't work for me, but it's as close as I could get. And it gave me a head-ache!
Url.createCriteria().buildCriteria {}
.createCriteria('indications', 'i')
.add(org.hibernate.criterion.Property.forName('i.id').eqAll(org.hibernate.criterion.DetachedCriteria.forClass(UrlIndication)
.add(org.hibernate.criterion.Restrictions.in('id', ids))
.setProjection(org.hibernate.criterion.Property.forName('id'))
))
.setResultTransformer(org.hibernate.Criteria.DISTINCT_ROOT_ENTITY)
.list()
The problem I had is I could not get Restrictions.in to work. Restrictions.eq works fine.
the in clause should do:
indications {
'in' 'id', indicationsId.collect{ UUID.fromString indication.toString() }
}
Having the following domain class:
class Word {
Map translations
}
And instances in BootStrap:
def word1 = new Word().with{
translations = [en:"game"]
save(failOnError: true, flush: true)
}
def word2 = new Word().with{
translations = [en:"life"]
save(failOnError: true, flush: true)
}
What is the groovy way to get all words, where translation starts with startPart in some locale? For example:
def listWordsStartsWith(startPart, locale, params){
def regexp = startPart+'%'
def query = Word.where {
//translations[locale] =~ regexp
}
def words = query.list(params)
words
}
I don't think this is possible with GORM using a collection value for the translations field. I'd suggest the following alternate mapping:
class Word {
static hasMany = [translations: Translation]
}
class Translation {
static belongsTo = Word
String key
String value
}
Then the query would be something like this:
Word.createCriteria().list {
translations {
like('key', startPart+'%')
}
}
The where wethod relies on Groovy SQL, where you have a very small subset of Groovy. It maps the SQL commands, but allow IDE controls on the properties and well formed syntax.
Unfortunately, you cannot write all the Groovy inside (no function, few operators, no map, etc.).
Check the documentation to see what is accepted.
My problem is simple but I could not find any GORM syntax for this.
Consider the following class:
class Article {
String text
static hasMany = [tags: String]
static constraints= {
tags(unique: true) //NOT WORKING
}
}
I want to have one unique tag name per article defined in my constraints but I cannot make it with the above syntax.
Clearly I need in DB schema something like:
create table article_tags (article_id bigint, tags_string varchar(255), unique (article_id , tags_string))
How can I do that?
PS: I am also stuck for setting constraints on tag minimum and maximum size
FYI, you can also use a custom validator in domain classes:
static constraints = {
tags(validator: {
def valid = tags == tags.unique()
if (!valid) errors.rejectValue(
"tags", "i18n.message.code", "default message")
return valid
})
At the database level, you can customize DDL generation by having the following code in grails-app/conf/hibernate/hibernate.cfg.xml:
<hibernate-mapping>
<database-object>
<create>
ALTER TABLE article_tags
ADD CONSTRAINT article_tags_unique_constraint
UNIQUE(article_id, tags_string);
</create>
<drop>
ALTER TABLE article_tags
DROP CONSTRAINT article_tags_unique_constraint;
</drop>
</database-object>
</hibernate-mapping>
Initially I looked at the joinTable mapping to see if it would support a unique key, but it won't.
The best solution I can think of is the following combination:
Manually run the SQL statement to add the unique constraint. If you have some sort of database management tool (e.g. Liquibase), that would be the ideal place.
Explicitly declare the association as a Set. This should avoid Hibernate ever running into the unique constraint, anyway.
class Article {
static hasMany = [tags: String]
Set<String> tags = new HashSet<String>()
}
An alternate solution would be to explicitly declare your child domain (Tag) and set up a many-to-many relationship, adding the unique key to the join table there using constraints. But that's not really a great solution, either. Here's a primitive example:
class Article {
static hasMany = [articleTags: ArticleTag]
}
class Tag {
static hasMany = [articleTags: ArticleTag]
}
class ArticleTag {
Article article
Tag tag
static constraints = {
tag(unique: article)
}
}
With this, though, you have to explicitly manage the many-to-many relationship in your code. It's a bit inconvenient, but it gives you full control over the relationship as a whole. You can find out the nitty gritty details here (the Membership class in the linked example is akin to the ArticleTag in mine).
Perhaps one of the gurus more familiar with GORM will chime in with a more elegant solution, but I can't find anything in the docs.
EDIT: Note that this approach does not consider a unique(article_id , tags_id) constraint. It also raises an issue with two Articles having the same tags. - Sorry.
While this is not officially documented (see the relevant parts of the Grails Reference Documentation here and here) constraints on one-to-many associations are simply ignored by GORM. This includes unique and nullable constraints, and probably any.
This can be proved by setting dbCreate="create" and next, by looking at the database schema definition. For your Article sample and the PostgreSQL database, this would be:
CREATE TABLE article_tags
(
article_id bigint NOT NULL,
tags_string character varying(255),
CONSTRAINT fkd626473e45ef9ffb FOREIGN KEY (article_id)
REFERENCES article (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT article0_tags_article0_id_key UNIQUE (article_id)
)
WITH (
OIDS=FALSE
);
As can be seen above, there are no constraints for the tags_string column.
In contrast to constraints on association fields, constraints on "normal" instance fields of domain classes do work as expected.
Thus, we'll want to have some kind of Tag, or TagHolder, domain class, and we'd need to find a pattern that still provides the Article with a clean public API.
First, we're introducing the TagHolder domain class:
class TagHolder {
String tag
static constraints = {
tag(unique:true, nullable:false,
blank:false, size:2..255)
}
}
and associate it with the Article class:
class Article {
String text
static hasMany = [tagHolders: TagHolder]
}
In order to provide a clean public API, we're adding the methods String[] getTags(), void setTags(String[]. That way, we can also call the constructor with named parameters, like, new Article(text: "text", tags: ["foo", "bar"]). We're also adding the addToTags(String) closure, which mimicks GORM's corresponding "magic method".
class Article {
String text
static hasMany = [tagHolders: TagHolder]
String[] getTags() {
tagHolders*.tag
}
void setTags(String[] tags) {
tagHolders = tags.collect { new TagHolder(tag: it) }
}
{
this.metaClass.addToTags = { String tag ->
tagHolders = tagHolders ?: []
tagHolders << new TagHolder(tag: tag)
}
}
}
It's a workaround, but there's not too much coding involved.
A drawback, we're getting an additional JOIN table. Nevertheless, this pattern allows for applying any available constraints.
Finally, a test case could look like this one:
class ArticleTests extends GroovyTestCase {
void testUniqueTags_ShouldFail() {
shouldFail {
def tags = ["foo", "foo"] // tags not unique
def article = new Article(text: "text", tags: tags)
assert ! article.validate()
article.save()
}
}
void testUniqueTags() {
def tags = ["foo", "bar"]
def article = new Article(text: "text", tags: tags)
assert article.validate()
article.save()
assert article.tags.size() == 2
assert TagHolder.list().size() == 2
}
void testTagSize_ShouldFail() {
shouldFail {
def tags = ["f", "b"] // tags too small
def article = new Article(text: "text", tags: tags)
assert ! article.validate()
article.save()
}
}
void testTagSize() {
def tags = ["foo", "bar"]
def article = new Article(text: "text", tags: tags)
assert article.validate()
article.save()
assert article.tags.size() == 2
assert TagHolder.list().size() == 2
}
void testAddTo() {
def article = new Article(text: "text")
article.addToTags("foo")
article.addToTags("bar")
assert article.validate()
article.save()
assert article.tags.size() == 2
assert TagHolder.list().size() == 2
}
}
Try this:
http://johnrellis.blogspot.com/2009/09/grails-constraints-across-relationships.html
The only way I've found to do this is to write a custom constraint and do a database check for the duplication. I don't think there is a built-in way to use a GORM constraint to accomplish this.