How to open database connection in a BackgroundJob in ABP application

不想你离开。 提交于 2019-12-02 18:52:14

问题


Issue

For testing, I create a new job, it just use IRepository to read data from database. The code as below:

public class TestJob : BackgroundJob<string>, ITransientDependency
{
    private readonly IRepository<Product, long> _productRepository;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    public TestJob(IRepository<Product, long> productRepository,
        IUnitOfWorkManager unitOfWorkManager)
    {
        _productRepository = productRepository;
        _unitOfWorkManager = unitOfWorkManager;
    }

    public override void Execute(string args)
    {
        var task = _productRepository.GetAll().ToListAsync();
        var items = task.Result;
        Debug.WriteLine("test db connection");
    }
}

Then I create a new application service to trigger the job. The code snippet as below:

public async Task UowInJobTest()
{
   await  _backgroundJobManager.EnqueueAsync<TestJob, string>("aaaa");
}

When I test the job, It will throw following exception when execute var task = _productRepository.GetAll().ToListAsync();

Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.Object name: 'AbpExampleDbContext'.

Solution

S1: Add UnitOfWork attribute on execute method. It can address the issue. But it is not better for my actual scenario. In my actual scenario, the job is a long time task, and has much DB operatons, if enable UnitOfWork for Execute method, it will lock db resource for a long time. So this is not a solution for my scenario.

[UnitOfWork]
public override void Execute(string args)
{
    var task = _productRepository.GetAll().ToListAsync();
    var items = task.Result;
    Debug.WriteLine("test db connection");
}

S2: Execute DB operation in UnitOfWork explicitly. Also, this can address the issue, but I don’t think this is a best practice. In my example,just read data from database, no transaction is required. Even-though the issue is addressed, but I don’t think it’s a correct way.

public override void Execute(string args)
{
    using (var unitOfWork = _unitOfWorkManager.Begin())
    {
        var task = _productRepository.GetAll().ToListAsync();
        var items = task.Result;
        unitOfWork.Complete();
    }
    Debug.WriteLine("test db connection");
}

Question

My question is what’s the correct and best way to execute a DB operation in BackgroundJob?

There is addtional another question, I create a new application service, and disable UnitOfWrok, but it works fine. Please see the code as below. Why It works fine in application service, but doesn’t work in BackgroundJob?

[UnitOfWork(IsDisabled =true)]
public async Task<GetAllProductsOutput> GetAllProducts()
{
    var result = await _productRepository.GetAllListAsync();
    var itemDtos = ObjectMapper.Map<List<ProductDto>>(result);
    return new GetAllProductsOutput()
    {
        Items = itemDtos
    };
}

回答1:


The documentation on Background Jobs And Workers uses [UnitOfWork] attribute.

S1: Add UnitOfWork attribute on execute method. It can address the issue. But it is not better for my actual scenario. In my actual scenario, the job is a long time task, and has much DB operatons, if enable UnitOfWork for Execute method, it will lock db resource for a long time. So this is not a solution for my scenario.

Background jobs are run synchronously on a background thread, so this concern is unfounded.

S2: Execute DB operation in UnitOfWork explicitly. Also, this can address the issue, but I don’t think this is a best practice. In my example,just read data from database, no transaction is required. Even-though the issue is addressed, but I don’t think it’s a correct way.

You can use a Non-Transactional Unit Of Work:

[UnitOfWork(isTransactional: false)]
public override void Execute(string args)
{
    var task = _productRepository.GetAll().ToListAsync();
    var items = task.Result;
}

You can use IUnitOfWorkManager:

public override void Execute(string args)
{
    using (var unitOfWork = _unitOfWorkManager.Begin(TransactionScopeOption.Suppress))
    {
        var task = _productRepository.GetAll().ToListAsync();
        var items = task.Result;
        unitOfWork.Complete();
    }
}

You can also use AsyncHelper:

[UnitOfWork(isTransactional: false)]
public override void Execute(string args)
{
    var items = AsyncHelper.RunSync(() => _productRepository.GetAll().ToListAsync());
}

Conventional Unit Of Work Methods

I create a new application service, and disable UnitOfWork, but it works fine.
Why it works fine in application service, but doesn’t work in BackgroundJob?

[UnitOfWork(IsDisabled = true)]
public async Task<GetAllProductsOutput> GetAllProducts()
{
    var result = await _productRepository.GetAllListAsync();
    var itemDtos = ObjectMapper.Map<List<ProductDto>>(result);
    return new GetAllProductsOutput
    {
        Items = itemDtos
    };
}

You are using different methods: GetAllListAsync() vs GetAll().ToListAsync()

Repository methods are Conventional Unit Of Work Methods, but ToListAsync() isn't one.

From the documentation on About IQueryable<T>:

When you call GetAll() outside of a repository method, there must be an open database connection. This is because of the deferred execution of IQueryable<T>. It does not perform a database query unless you call the ToList() method or use the IQueryable<T> in a foreach loop (or somehow access the queried items). So when you call the ToList() method, the database connection must be alive.



来源:https://stackoverflow.com/questions/50149302/how-to-open-database-connection-in-a-backgroundjob-in-abp-application

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!