概述
在这个教程中,我们将学习如何在MongoDB中定义一个唯一字段。唯一字段是数据库设计的关键组成部分,它们同时保证一致性并提升性能,防止不应该存在的重复值。
1. 配置
与关系型数据库不同,MongoDB不提供创建约束的选项。因此,我们的唯一选择是创建唯一索引。然而,在Spring Data中,自动索引创建默认被关闭。首先,让我们在application.properties
中启用它:
spring.data.mongodb.auto-index-creation=true
配置后,如果索引尚未存在,它们将在启动时自动创建。但我们需要记住,一旦我们已经有重复的值,就无法再创建唯一索引。这会导致应用程序启动时抛出异常。
2. @Indexed
注解
@Indexed
注解允许我们标记具有索引的字段。由于我们启用了自动索引创建,我们无需自己创建。默认情况下,索引不是唯一的。因此,我们需要通过unique
属性来开启。让我们通过创建第一个示例来看看它的用法:
@Document
public class Company {
@Id
private String id;
private String name;
@Indexed(unique = true)
private String email;
// getters and setters
}
注意,我们仍然可以使用@Id
注解,它完全独立于我们的索引。这就是我们拥有一个唯一字段的全部所需。结果,如果我们插入具有相同email
的多个文档,将导致DuplicateKeyException
:
@Test
public void givenUniqueIndex_whenInsertingDupe_thenExceptionIsThrown() {
Company a = new Company();
a.setName("Name");
a.setEmail("[email protected]");
companyRepo.insert(a);
Company b = new Company();
b.setName("Other");
b.setEmail("[email protected]");
assertThrows(DuplicateKeyException.class, () -> {
companyRepo.insert(b);
});
}
这种方法在我们想要强制唯一性但仍希望自动生成唯一ID字段时非常有用。
2.1. 标记多个字段
我们也可以将注解添加到多个字段上。让我们继续创建第二个示例:
@Document
public class Asset {
@Indexed(unique = true)
private String name;
@Indexed(unique = true)
private Integer number;
}
请注意,我们没有在任何字段上显式设置@Id
。MongoDB仍然会自动为我们设置一个名为“_id”的字段,但它对我们的应用程序不可见。但是,我们不能在一个同时标记为unique
的@Indexed
注解和@Id
的字段上使用它。当应用程序尝试创建索引时,这将抛出异常。
此外,现在我们有两个唯一字段。请注意,这并不意味着这是一个复合索引。因此,如果任何字段的值有相同的多次插入,都将导致重复键。让我们测试一下:
@Test
public void givenMultipleIndexes_whenAnyFieldDupe_thenExceptionIsThrown() {
Asset a = new Asset();
a.setName("Name");
a.setNumber(1);
assetRepo.insert(a);
assertThrows(DuplicateKeyException.class, () -> {
Asset b = new Asset();
b.setName("Name");
b.setNumber(2);
assetRepo.insert(b);
});
assertThrows(DuplicateKeyException.class, () -> {
Asset b = new Asset();
b.setName("Other");
b.setNumber(1);
assetRepo.insert(b);
});
}
如果我们想让组合值形成唯一的索引,就需要创建复合索引。
2.2. 使用自定义类型作为索引
同样,我们可以为自定义类型的字段添加注解,这样就可以实现复合索引的效果。让我们从一个SaleId
类开始,代表我们的复合索引:
public class SaleId {
private Long item;
private String date;
// getters and setters
}
现在,让我们创建Sale
类以利用它:
@Document
public class Sale {
@Indexed(unique = true)
private SaleId saleId;
private Double value;
// getters and setters
}
每次我们尝试添加新的Sale
,如果其SaleId
相同,我们将得到重复键。让我们测试一下:
@Test
public void givenCustomTypeIndex_whenInsertingDupe_thenExceptionIsThrown() {
SaleId id = new SaleId();
id.setDate("2022-06-15");
id.setItem(1L);
Sale a = new Sale(id);
a.setValue(53.94);
saleRepo.insert(a);
assertThrows(DuplicateKeyException.class, () -> {
Sale b = new Sale(id);
b.setValue(100.00);
saleRepo.insert(b);
});
}
这种方法的优点是将索引定义分开。这使得我们可以根据需要在SaleId
中添加或删除新字段,而无需重新创建或更新索引。它也类似于复合键。但是,索引与键不同,因为它们可以有一个空值。
3. @CompoundIndex
注解
要使用多个字段创建一个不依赖于自定义类的独特索引,我们必须创建复合索引。为此,我们直接在类上使用@CompoundIndex
注解。这个注解包含一个def
属性,我们将使用它来包含所需的字段。让我们创建一个Customer
类,为storeId
和number
字段定义一个唯一索引:
@Document
@CompoundIndex(def = "{'storeId': 1, 'number': 1}", unique = true)
public class Customer {
@Id
private String id;
private Long storeId;
private Long number;
private String name;
// getters and setters
}
这与在多个字段上使用@Indexed
不同。这种方法只会导致当我们尝试插入具有相同storeId
和number
值的客户时抛出DuplicateKeyException
:
@Test
public void givenCompoundIndex_whenDupeInsert_thenExceptionIsThrown() {
Customer customerA = new Customer("Name A");
customerA.setNumber(1l);
customerA.setStoreId(2l);
Customer customerB = new Customer("Name B");
customerB.setNumber(1l);
customerB.setStoreId(2l);
customerRepo.insert(customerA);
assertThrows(DuplicateKeyException.class, () -> {
customerRepo.insert(customerB);
});
}
使用这种方法,我们无需为了索引而创建另一个类的优势。此外,我们可以在复合索引定义中添加@Id
注解。然而,与@Indexed
不同,这不会导致异常。
4. 总结
在这篇文章中,我们了解了如何为文档定义唯一字段。因此,我们了解到我们唯一的选择是使用唯一索引。同时,借助Spring Data,我们可以轻松地配置应用程序自动创建索引。并且,我们看到了使用@Indexed
和@CompoundIndex
注解的各种方法。
如往常一样,源代码可以在GitHub上找到。