概述

在这个教程中,我们将学习如何在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类,为storeIdnumber字段定义一个唯一索引:

@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不同。这种方法只会导致当我们尝试插入具有相同storeIdnumber值的客户时抛出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上找到。