Как установить уникальность на уровне БД для ассоциации «один ко многим»? - PullRequest
3 голосов
/ 28 октября 2010

Моя проблема проста, но я не смог найти синтаксис 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 ]

5 голосов
/ 28 октября 2010

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

    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>
2 голосов
/ 28 октября 2010

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

1 голос
/ 28 октября 2010

РЕДАКТИРОВАТЬ: Обратите внимание, что этот подход не учитывает ограничение 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 domain:

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
    }
}
0 голосов
/ 28 октября 2010

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

0 голосов
/ 28 октября 2010
...