项目

MongoDB 集成

本文档解释了如何将 MongoDB 作为数据库提供程序集成到基于 ABP 的应用程序中,以及如何进行配置。

安装

Volo.Abp.MongoDB 是用于 MongoDB 集成的主要 NuGet 包。将其安装到您的项目(对于分层应用程序,安装到您的数据/基础设施层)中。您可以使用 ABP CLI 将其安装到您的项目中。在层的 .csproj 文件所在文件夹中执行以下命令:

abp add-package Volo.Abp.MongoDB

如果您尚未安装,首先需要安装 ABP CLI。有关其他安装选项,请参阅 包描述页面

然后将 AbpMongoDbModule 模块依赖项添加到您的 模块 中:

using Volo.Abp.MongoDB;
using Volo.Abp.Modularity;

namespace MyCompany.MyProject
{
    [DependsOn(typeof(AbpMongoDbModule))]
    public class MyModule : AbpModule
    {
        //...
    }
}

创建 Mongo Db 上下文

ABP 引入了 Mongo Db 上下文 概念(类似于 Entity Framework Core 的 DbContext),以便更轻松地使用和配置集合。示例如下:

public class MyDbContext : AbpMongoDbContext
{
    public IMongoCollection<Question> Questions => Collection<Question>();

    public IMongoCollection<Category> Categories => Collection<Category>();

    protected override void CreateModel(IMongoModelBuilder modelBuilder)
    {
        base.CreateModel(modelBuilder);

        //为您的集合自定义配置。
    }
}
  • 它派生自 AbpMongoDbContext 类。
  • 为每个 MongoDB 集合添加一个公共的 IMongoCollection<TEntity> 属性。默认情况下,ABP 使用这些属性来创建默认仓库。
  • 重写 CreateModel 方法可以配置集合。

为集合配置映射

ABP 会自动为您的 DbContext 中所有的 IMongoCollection<TEntity> 属性将实体注册到 MongoDB 客户端库。对于上面的示例,QuestionCategory 实体会被自动注册。

对于每个注册的实体,它会调用 AutoMap() 并配置您实体的已知属性。例如,如果您的实体实现了 IHasExtraProperties 接口(默认情况下每个聚合根都已实现),它会自动配置 ExtraProperties

因此,大多数时候您不需要显式配置实体的注册。但是,如果需要,您可以在 DbContext 中通过重写 CreateModel 方法来实现。示例:

protected override void CreateModel(IMongoModelBuilder modelBuilder)
{
    base.CreateModel(modelBuilder);

    modelBuilder.Entity<Question>(b =>
    {
        b.CollectionName = "MyQuestions"; //设置集合名称
        b.BsonMap.UnmapProperty(x => x.MyProperty); //忽略 'MyProperty'
    });
}

此示例将数据库中映射的集合名称更改为 'MyQuestions',并忽略 Question 类中的一个属性。

如果您只需要配置集合名称,也可以在您的 DbContext 中为集合使用 [MongoCollection] 属性。示例:

[MongoCollection("MyQuestions")] //设置集合名称
public IMongoCollection<Question> Questions => Collection<Question>();

为集合配置索引和 CreateCollectionOptions

您可以通过重写 CreateModel 方法来为您的集合配置索引和 CreateCollectionOptions。示例:

protected override void CreateModel(IMongoModelBuilder modelBuilder)
{
    base.CreateModel(modelBuilder);

    modelBuilder.Entity<Question>(b =>
    {
        b.CreateCollectionOptions.Collation = new Collation(locale:"en_US", strength: CollationStrength.Secondary);
        b.ConfigureIndexes(indexes =>
            {
                indexes.CreateOne(
                    new CreateIndexModel<BsonDocument>(
                        Builders<BsonDocument>.IndexKeys.Ascending("MyProperty"),
                        new CreateIndexOptions { Unique = true }
                    )
                );
            }
        );
    });
}

此示例为集合设置了排序规则,并为 MyProperty 属性创建了一个唯一索引。

配置连接字符串选择

如果您的应用程序中有多个数据库,可以使用 [ConnectionStringName] 属性为您的 DbContext 配置连接字符串名称。示例:

[ConnectionStringName("MySecondConnString")]
public class MyDbContext : AbpMongoDbContext
{

}

如果不配置,则使用 Default 连接字符串。如果配置了特定的连接字符串名称,但在应用程序配置中未定义此连接字符串名称,则会回退到 Default 连接字符串。

将 DbContext 注册到依赖注入

在您的模块中使用 AddAbpDbContext 方法,将您的 DbContext 类注册到 依赖注入 系统。

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.MongoDB;
using Volo.Abp.Modularity;

namespace MyCompany.MyProject
{
    [DependsOn(typeof(AbpMongoDbModule))]
    public class MyModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.AddMongoDbContext<MyDbContext>();

            //...
        }
    }
}

添加默认仓库

ABP 可以自动为您的 DbContext 中的实体创建默认的 通用仓库。只需在注册时使用 AddDefaultRepositories() 选项:

services.AddMongoDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories();
});

默认情况下,这将为每个 聚合根实体(派生自 AggregateRoot 的类)创建一个仓库。如果您也想为其他实体创建仓库,则将 includeAllEntities 设置为 true

services.AddMongoDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories(includeAllEntities: true);
});

然后,您可以在服务中注入和使用 IRepository<TEntity, TPrimaryKey>。假设您有一个主键为 GuidBook 实体:

public class Book : AggregateRoot<Guid>
{
    public string Name { get; set; }

    public BookType Type { get; set; }
}

(此处 BookType 是一个简单的 enum)并且您想要在 领域服务 中创建一个新的 Book 实体:

public class BookManager : DomainService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public BookManager(IRepository<Book, Guid> bookRepository) //注入默认仓库
    {
        _bookRepository = bookRepository;
    }

    public async Task<Book> CreateBook(string name, BookType type)
    {
        Check.NotNullOrWhiteSpace(name, nameof(name));

        var book = new Book
        {
            Id = GuidGenerator.Create(),
            Name = name,
            Type = type
        };

        await _bookRepository.InsertAsync(book); //使用标准仓库方法

        return book;
    }
}

此示例使用 InsertAsync 方法将新实体插入数据库。

添加自定义仓库

在大多数情况下,默认的通用仓库功能足够强大(因为它们实现了 IQueryable)。但是,您可能需要创建自定义仓库来添加自己的仓库方法。

假设您想按类型删除所有书籍。建议为您的自定义仓库定义一个接口:

public interface IBookRepository : IRepository<Book, Guid>
{
    Task DeleteBooksByType(
        BookType type,
        CancellationToken cancellationToken = default(CancellationToken)
    );
}

通常,您希望从 IRepository 派生以继承标准仓库方法。但这不是必须的。仓库接口定义在分层应用程序的领域层中。它们在数据/基础设施层(启动模板 中的 MongoDB 项目)中实现。

IBookRepository 接口的示例实现:

public class BookRepository :
    MongoDbRepository<BookStoreMongoDbContext, Book, Guid>,
    IBookRepository
{
    public BookRepository(IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public async Task DeleteBooksByType(
        BookType type,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var collection = await GetCollectionAsync(cancellationToken);
        await collection.DeleteManyAsync(
            Builders<Book>.Filter.Eq(b => b.Type, type),
            cancellationToken
        );
    }
}

现在,可以在需要时 注入 IBookRepository 并使用 DeleteBooksByType 方法。

覆盖默认通用仓库

即使您创建了自定义仓库,您仍然可以注入默认的通用仓库(对于此示例是 IRepository<Book, Guid>)。默认的仓库实现将不使用您创建的类。

如果您想用您的自定义仓库替换默认的仓库实现,请在 AddMongoDbContext 选项中执行:

context.Services.AddMongoDbContext<BookStoreMongoDbContext>(options =>
{
    options.AddDefaultRepositories();
    options.AddRepository<Book, BookRepository>(); //替换 IRepository<Book, Guid>
});

当您想要 覆盖基本仓库方法 以自定义它时,这一点尤其重要。例如,您可能希望覆盖 DeleteAsync 方法,以更高效的方式删除实体:

public async override Task DeleteAsync(
    Guid id,
    bool autoSave = false,
    CancellationToken cancellationToken = default)
{
    //TODO: 删除方法的自定义实现
}

访问 MongoDB API

在大多数情况下,您希望将 MongoDB API 隐藏在仓库后面(这是仓库的主要目的)。但是,如果您想通过仓库访问 MongoDB API,可以使用 GetDatabaseAsync()GetCollectionAsync()GetAggregateAsync() 扩展方法。示例:

public class BookService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public BookService(IRepository<Book, Guid> bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task FooAsync()
    {
        IMongoDatabase database = await _bookRepository.GetDatabaseAsync();
        IMongoCollection<Book> books = await _bookRepository.GetCollectionAsync();
        IAggregateFluent<Book> bookAggregate = await _bookRepository.GetAggregateAsync();
    }
}

重要:您必须从要访问 MongoDB API 的项目中引用 Volo.Abp.MongoDB 包。这破坏了封装性,但这是您在这种情况下所期望的。

事务

MongoDB 从 4.0 版本开始支持多文档事务,ABP 也支持它。但是,启动模板 默认禁用 事务。如果您的 MongoDB 服务器 支持事务,您可以在 YourProjectMongoDbModule 类中启用它们:

删除以下代码以启用事务:

- context.Services.AddAlwaysDisableUnitOfWorkTransaction();
- Configure<AbpUnitOfWorkDefaultOptions>(options =>
- {
-     options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
- });

在 Docker 中设置支持事务的 MongoDB 副本集

使用以下 docker-compose.yml 创建一个支持事务的本地 MongoDB 副本集。连接字符串将为 mongodb://localhost:27017/YourProjectName?replicaSet=rs0

version: "3.8"

services:
  mongo:
    image: mongo:8.0
    command: ["--replSet", "rs0", "--bind_ip_all", "--port", "27017"]
    ports:
      - 27017:27017
    healthcheck:
      test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'127.0.0.1:27017'}]}) }" | mongosh --port 27017 --quiet
      interval: 5s
      timeout: 30s
      start_period: 0s
      start_interval: 1s
      retries: 30

高级主题

控制多租户

如果您的解决方案是 多租户 的,租户可能有 独立的数据库,您的解决方案中有 多个 DbContext 类,并且您的某些 DbContext 类应该 只能从宿主端使用,建议在您的 DbContext 类上添加 [IgnoreMultiTenancy] 属性。在这种情况下,ABP 保证相关的 DbContext 始终使用宿主 连接字符串,即使您在租户上下文中。

示例:

[IgnoreMultiTenancy]
public class MyDbContext : AbpMongoDbContext
{
    ...
}

如果您的 DbContext 中的任何实体可以保存在租户数据库中,请不要使用 [IgnoreMultiTenancy] 属性。

当您使用仓库时,ABP 已经对未实现 IMultiTenant 接口的实体使用宿主数据库。因此,在大多数情况下,如果您使用仓库来处理数据库,则不需要 [IgnoreMultiTenancy] 属性。

设置默认仓库类

默认的通用仓库默认由 MongoDbRepository 类实现。您可以创建自己的实现并将其用于默认仓库实现。

首先,像这样定义您的仓库类:

public class MyRepositoryBase<TEntity>
    : MongoDbRepository<BookStoreMongoDbContext, TEntity>
    where TEntity : class, IEntity
{
    public MyRepositoryBase(IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
}

public class MyRepositoryBase<TEntity, TKey>
    : MongoDbRepository<BookStoreMongoDbContext, TEntity, TKey>
    where TEntity : class, IEntity<TKey>
{
    public MyRepositoryBase(IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
}

第一个用于 具有复合键的实体,第二个用于具有单主键的实体。

建议从 MongoDbRepository 类继承并在需要时覆盖方法。否则,您必须手动实现所有标准仓库方法。

现在,您可以使用 SetDefaultRepositoryClasses 选项:

context.Services.AddMongoDbContext<BookStoreMongoDbContext>(options =>
{
    options.SetDefaultRepositoryClasses(
        typeof(MyRepositoryBase<,>),
        typeof(MyRepositoryBase<>)
    );
    //...
});

为默认仓库设置基础 MongoDbContext 类或接口

如果您的 MongoDbContext 继承自另一个 MongoDbContext 或实现了一个接口,您可以将该基类或接口用作默认仓库的 MongoDbContext。示例:

public interface IBookStoreMongoDbContext : IAbpMongoDbContext
{
    Collection<Book> Books { get; }
}

IBookStoreMongoDbContextBookStoreMongoDbContext 类实现。然后,您可以使用 AddDefaultRepositories 的泛型重载:

context.Services.AddMongoDbContext<BookStoreMongoDbContext>(options =>
{
    options.AddDefaultRepositories<IBookStoreMongoDbContext>();
    //...
});

现在,您的自定义 BookRepository 也可以使用 IBookStoreMongoDbContext 接口:

public class BookRepository
    : MongoDbRepository<IBookStoreMongoDbContext, Book, Guid>,
      IBookRepository
{
    //...
}

为 MongoDbContext 使用接口的一个优点是它变得可以被另一个实现替换。

替换其他 DbContext

一旦您为 MongoDbContext 正确定义并使用了接口,那么任何其他实现都可以使用以下方式替换它:

ReplaceDbContext 属性

[ReplaceDbContext(typeof(IBookStoreMongoDbContext))]
public class OtherMongoDbContext : AbpMongoDbContext, IBookStoreMongoDbContext
{
    //...
}

ReplaceDbContext 选项

context.Services.AddMongoDbContext<OtherMongoDbContext>(options =>
{
    //...
    options.ReplaceDbContext<IBookStoreMongoDbContext>();
});

在此示例中,OtherMongoDbContext 实现了 IBookStoreMongoDbContext。此功能允许您在开发时拥有多个 MongoDbContext(每个模块一个),但在运行时拥有单个 MongoDbContext(实现所有 MongoDbContext 的所有接口)。

多租户替换

还可以基于 多租户 方来替换 DbContext。ReplaceDbContext 属性和 ReplaceDbContext 方法可以获取一个 MultiTenancySides 选项,默认值为 MultiTenancySides.Both

示例: 仅针对租户替换 DbContext,使用 ReplaceDbContext 属性

[ReplaceDbContext(typeof(IBookStoreDbContext), MultiTenancySides.Tenant)]

示例: 仅针对宿主方替换 DbContext,使用 ReplaceDbContext 方法

options.ReplaceDbContext<IBookStoreDbContext>(MultiTenancySides.Host);

自定义批量操作

如果您有更好的逻辑或使用外部库进行批量操作,可以通过实现 IMongoDbBulkOperationProvider 来覆盖该逻辑。

  • 您可以使用以下示例模板:
public class MyCustomMongoDbBulkOperationProvider
    : IMongoDbBulkOperationProvider, ITransientDependency
{
    public async Task DeleteManyAsync<TEntity>(
        IMongoDbRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        IClientSessionHandle sessionHandle,
        bool autoSave,
        CancellationToken cancellationToken)
        where TEntity : class, IEntity
    {
        // 在此处编写您的逻辑。
    }

    public async Task InsertManyAsync<TEntity>(
        IMongoDbRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        IClientSessionHandle sessionHandle,
        bool autoSave,
        CancellationToken cancellationToken)
        where TEntity : class, IEntity
    {
        // 在此处编写您的逻辑。
    }

    public async Task UpdateManyAsync<TEntity>(
        IMongoDbRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        IClientSessionHandle sessionHandle,
        bool autoSave,
        CancellationToken cancellationToken)
        where TEntity : class, IEntity
    {
        // 在此处编写您的逻辑。
    }
}

另请参阅

在本文档中