如何在数据库级别为一对多关联设置唯一性?

发布于 2024-09-29 07:41:27 字数 474 浏览 12 评论 0 原文

我的问题很简单,但我找不到任何 GORM 语法。

考虑以下类:

class Article {
  String text

  static hasMany = [tags: String]

  static constraints= {
    tags(unique: true) //NOT WORKING
  }

}

我想在我的约束中定义每篇文章一个唯一的标签名称,但我无法使用上述语法来实现它。 显然,我在数据库模式中需要类似的东西:

create table article_tags (article_id bigint, tags_string varchar(255), unique (article_id , tags_string))

我该怎么做?

PS:我也陷入了对标签最小和最大尺寸设置限制的困境

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

第几種人 2024-10-06 07:41:28

仅供参考,您还可以在域类中使用自定义验证器

    static constraints = {
    tags(validator: { 
        def valid = tags == tags.unique()
        if (!valid) errors.rejectValue(
            "tags", "i18n.message.code", "default message")
        return valid
    })

在数据库级别,您可以自定义 DDL 生成以下代码位于 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>

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>
溺ぐ爱和你が 2024-10-06 07:41:28

最初我查看了 joinTable 映射以查看它是否支持 unique 键,但事实并非如此。

我能想到的最好的解决方案是以下组合:

  • 手动运行SQL语句添加唯一约束。如果您有某种数据库管理工具(例如 Liquibase),那将是理想的地方。

  • 将关联显式声明为Set。无论如何,这应该可以避免 Hibernate 遇到唯一约束。

    类文章 {
        静态 hasMany = [标签:字符串]
        设置<字符串>标签 = new HashSet()
    }
    

另一种解决方案是显式声明您的子域 (Tag) 并设置多对多关系,使用 unique 键将 unique 键添加到连接表中代码>约束。但这也不是一个很好的解决方案。这是一个原始示例:

class Article {
    static hasMany = [articleTags: ArticleTag]
}

class Tag {
    static hasMany = [articleTags: ArticleTag]
}

class ArticleTag {
    Article article
    Tag tag
    static constraints = {
        tag(unique: article)
    }
}

但是,有了这个,您必须显式管理代码中的多对多关系。这有点不方便,但它让你可以完全控制整个关系。您可以在此处找到详细信息(链接示例中的 Membership 类类似于我的 ArticleTag )。

也许一位更熟悉 GORM 的大师会提出一个更优雅的解决方案,但我在文档中找不到任何内容。

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.

青丝拂面 2024-10-06 07:41:28

编辑:请注意,此方法考虑unique(article_id,tags_id)约束。它还引发了两个具有相同标签的 Article 问题。 - 抱歉。

虽然这没有正式记录(请参阅 Grails 参考文档的相关部分此处这里)一对多关联的约束被 GORM 简单地忽略。这包括唯一可为空约束,以及可能的任何约束。

这可以通过设置 dbCreate="create" 以及接下来查看数据库模式定义来证明。对于您的 Article 示例和 PostgreSQL 数据库,这将是:

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
);

如上所示,tags_string 列没有任何限制。

与关联字段的约束相反,域类的“正常”实例字段的约束确实按预期工作。

因此,我们需要某种 TagTagHolder 域类,并且我们需要找到一种仍然提供 Article 的模式 具有干净的公共 API

首先,我们引入 TagHolder 域类:

class TagHolder {
    String tag

    static constraints = {
        tag(unique:true, nullable:false, 
            blank:false, size:2..255)
    }
}

并将其与 Article 类关联:

class Article {
    String text

    static hasMany = [tagHolders: TagHolder]
}

为了提供干净的公共 API,我们添加方法 String[] getTags(), void setTags(String[]。这样,我们还可以使用命名参数调用构造函数,例如,new Article(text: " text", Tags: ["foo", "bar"])。我们还添加了 addToTags(String) 闭包,它模仿了 GORM 相应的“魔术方法”。

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

这是一个解决方法,但不涉及太多编码。
一个缺点是,我们得到了一个额外的 JOIN 表。然而,这种模式允许应用任何可用的约束。

最后,测试用例可能如下所示:

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

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
    }
}
北城半夏 2024-10-06 07:41:28

我发现执行此操作的唯一方法是编写自定义约束并对数据库进行重复检查。我认为没有内置的方法可以使用 GORM 约束来完成此任务。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文