Как установить уникальность на уровне БД для ассоциации "один ко многим"?
Моя проблема проста, но я не мог найти синтаксис 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 для достижения этой цели.