How to set uniqueness at DB level for a one-to-man

2019-03-21 21:04发布

问题:

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:

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>


回答2:

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.



回答3:

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
    }
}


回答4:

Try this:

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



回答5:

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.