views:

60

answers:

5

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

+1  A: 

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.

Rob Hruska
Thx for your answer. I have started with the 'alternate solution' i.e. adding a Tag Domain class. But again, even if the Set of tags is unique per tag, I didn't find a way to have the uniqueness translated into the db schma like unique(article_id , tags_id). Any idea here?
fabien7474
"adding the unique key to the join table there using constraints" => seems to be the solution. Do you have an example?
fabien7474
Updated with an example.
Rob Hruska
I see. That way it will work, but it is quite ugly just for DB constraint uniqueness. I have created the Tag domain class, but not the ArticleTag class. Maybe as yuo suggest, Liquibase is a solution.
fabien7474
+1  A: 

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
    }
}
robbbert
Robbert, very nice answer. This is exactly what I have done. But unfortunately, as specified in your EDIT comment, I don't have a way to get unique(article_id , tags_id) constraint.
fabien7474
A: 

Try this:

http://johnrellis.blogspot.com/2009/09/grails-constraints-across-relationships.html

Gregg
Almost !! But not possible, because a Tag can have many parents! So here we have a kind of many-to-many association and we cannot define a unique constraint on article. The only way I see is to create an ArtcileTag class as suggested by Rob
fabien7474
A: 

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.

Javid Jamae
+3  A: 

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>
robbbert
Yes. It seems to me the best solution. Let me try and I'll update the results here
fabien7474
+1, these are more ideal solutions than mine.
Rob Hruska
It works. Thanks a lot.
fabien7474
Glad it worked (just by hints), thanks for the appreciation and points, and kudos to all participants. This problem's solution, apparently, had been work in progress. - Cheers! :o)
robbbert