Как установить уникальность на уровне БД для ассоциации "один ко многим"?

Моя проблема проста, но я не мог найти синтаксис 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: я также застрял для установки ограничений на минимальный и максимальный размер тега

5 ответов

Решение

К вашему сведению, вы также можете использовать собственный валидатор в классах домена:

    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>

Первоначально я посмотрел на joinTable отображение, чтобы увидеть, будет ли он поддерживать unique ключ, но это не так.

Лучшее решение, которое я могу придумать, это следующая комбинация:

  • Вручную запустите оператор SQL, чтобы добавить ограничение уникальности. Если у вас есть какой-то инструмент управления базами данных (например, Liquibase), это было бы идеальным местом.

  • Явно объявить ассоциацию как Set, Это должно избегать Hibernate, когда-либо сталкивающихся с уникальным ограничением.

    class Article {
        static hasMany = [tags: String]
        Set<String> tags = new HashSet<String>()
    }
    

Альтернативным решением было бы явно объявить ваш дочерний домен (Tag) и установить отношение "многие ко многим", добавив unique ключ к соединительной таблице там с помощью constraints, Но это тоже не очень хорошее решение. Вот примитивный пример:

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, предложит более изящное решение, но я не могу найти ничего в документации.

РЕДАКТИРОВАТЬ: Обратите внимание, что этот подход не учитывает unique(article_id , tags_id) ограничение. Это также поднимает проблему с двумя Article с одинаковыми тегами. - Сожалею.

Хотя это официально не задокументировано (см. Соответствующие части Справочной документации Grails здесь и здесь), ограничения на связи один-ко-многим просто игнорируются GORM. Это включает unique а также nullable ограничения, и, вероятно, любые.

Это можно доказать, установив 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 колонка.

В отличие от ограничений на поля ассоциации, ограничения на "нормальные" поля экземпляров классов доменов работают должным образом.

Таким образом, мы хотим иметь какой-то Tag, или же TagHolder, класс домена, и нам нужно найти шаблон, который по-прежнему обеспечивает 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
    }
}

Единственный способ сделать это - написать пользовательское ограничение и выполнить проверку базы данных на предмет дублирования. Я не думаю, что есть встроенный способ использовать ограничение GORM для достижения этой цели.

Другие вопросы по тегам