์ต๊ทผ์ ์ฌ์ด๋ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๊ฒช์๋ ๋ฌธ์ ์ฌํญ๊ณผ ํด๊ฒฐ(?)๋ฐฉ๋ฒ์ ๋ํด ๊ณต์ ๋๋ฆฌ๊ณ ์ ํฉ๋๋ค.
PostgresSql์ ์ฌ์ฉํ์ฌ EF Core๋ฅผ ์ฌ์ฉ์ค์ Transaction๊ณผ ๊ด๋ จ๋ ๋ฌธ์ ์
๋๋ค.
ํด๋ฆฐ์ํคํ
์ณ ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
[Authorize]
public record CreateRecipeByBarCommand : IRequest<BaseResponse<long>>, ITransactional
{
public string? RecipeName { get; init; }
public string? Description { get; init; }
public bool IsPublic { get; init; }
public bool IsChangeableByOthers { get; init; }
public ICollection<CreateRecipeAlcohol> Alcohols { get; init; } = new List<CreateRecipeAlcohol>();
public ICollection<CreateRecipeIngredient> Ingredients { get; init; } = new List<CreateRecipeIngredient>();
}
public class CreateRecipeByBarCommandHandler : IRequestHandler<CreateRecipeByBarCommand, BaseResponse<long>>
{
private readonly IApplicationDbContext _context;
private readonly IMediator _sender;
public CreateRecipeByBarCommandHandler(IApplicationDbContext context, IMediator sender)
{
_context = context;
_sender = sender;
}
public async Task<BaseResponse<long>> Handle(CreateRecipeByBarCommand request, CancellationToken cancellationToken)
{
var recipe = new Recipe
{
Name = request.RecipeName,
Description = request.Description,
IsPublic = request.IsPublic,
IsChangeableByOthers = request.IsChangeableByOthers
};
recipe.AddDomainEvent(new RecipeCreatedEvent(recipe));
_context.Recipes.Add(recipe);
await _context.SaveChangesAsync(cancellationToken);
var commandRecipeAlcohols = new CreateRecipeAlcoholsCommand
{
RecipeId = recipe.Id,
Alcohols = request.Alcohols
};
await _sender.Send(commandRecipeAlcohols, cancellationToken);
var commandRecipeIngredients = new CreateRecipeIngredientsCommand
{
RecipeId = recipe.Id,
Ingredients = request.Ingredients
};
await _sender.Send(commandRecipeIngredients, cancellationToken);
return BaseResponse<long>.Success(recipe.Id);
}
}
๋ค์๊ณผ ๊ฐ์ด Recipe๋ฅผ ์์ฑํ๋ ๋ฉ์๋๊ฐ ์์ต๋๋ค. ์ฌ๊ธฐ์ ITransactional์ด๋ผ๋ ๋ง์ปค์ธํฐํ์ด์ค๋ฅผ ์์๋ฐ์์ ํธ๋์ญ์ ์ ํ์ฑํ ์์ผฐ์ต๋๋ค.
public record CreateRecipeAlcoholsCommand : IRequest
{
public ICollection<CreateRecipeAlcohol> Alcohols { get; init; } = new List<CreateRecipeAlcohol>();
public long RecipeId { get; init; }
}
public record CreateRecipeAlcohol
{
public long AlcoholId { get; init; }
public decimal AlcoholCost { get; init; }
// [9] ์ก์ฒด ๋จ์ ํ์
์ฝ๋
public int SystemUnitTypeCode { get; init; }
}
public class CreateRecipeAlcoholsCommandHandler : IRequestHandler<CreateRecipeAlcoholsCommand>
{
private readonly IApplicationDbContext _context;
public CreateRecipeAlcoholsCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(CreateRecipeAlcoholsCommand request, CancellationToken cancellationToken)
{
{
var entities = request.Alcohols.Select(alcohol =>
{
var entity = new RecipeAlcohol
{
RecipeId = request.RecipeId,
AlcoholId = alcohol.AlcoholId,
AlcoholCost = alcohol.AlcoholCost.ToMilliLiterDecimal(alcohol.SystemUnitTypeCode),
SystemUnitTypeCode = alcohol.SystemUnitTypeCode
};
entity.AddDomainEvent(new RecipeAlcoholCreatedEvent(entity));
//_context.RecipeAlcohols.Add(entity);
return entity;
}).ToList();
_context.RecipeAlcohols.AddRange(entities);
await _context.SaveChangesAsync(cancellationToken);
}
}
}
๊ทธ๋ฆฌ๊ณ ์์ ๊ฐ์ด AlcoholRecipe ๋ฆฌ์คํธ๋ฅผ ์์ฑํด์ฃผ๋ ๋ฉ์๋๋ ํธ์ถํ์ต๋๋ค (IngredientRecipe์ ๋ชจ์ต์ ๋์ผํ์ฌ, ์๋ตํฉ๋๋ค.)
๋ค๋ง,
์โฆ ์ฌ๊ธฐ์ ๋ฉ๋ถ์ด ์ค๋๊ตฐ์.
์ผ๋จ ๊ตฌ๊ธ์ ์ข ์ฐพ์๋ณด๋ AddRange๋ฅผ ์ ์ฅํ ๊ฒฝ์ฐ์๋ ๋จ๊ฑด์ ์ฟผ๋ฆฌ๋ฅผ ์ฌ๋ฌ๋ฒ ์ ์กํ๋ค. ๋ผ๊ณ ํ์ธ์ ํ์ต๋๋ค.
ํ , ๊ทธ๋ ๋ค๋ฉด ์ฌ๊ธฐ์ ๋ฐฉ๋ฒ์ ๋ฐ๊ฟ์
Add๋ ์ด๋จ๊น ํ๋ ์คํ์ ์ ์ผ๋ก ๋จ๊ฑด์ผ๋ก ์ฌ๋ฌ๋ฒ ์ถ๊ฐํด๋ณธ๊ฒฐ๊ณผ, ์ด๋๋โฆ ์๋๋ค์ฌโฆ? ๋ค๋ง ๋ ์ฐพ์๋ณด๋ Add๋ ๋ฒค์น๋งํฌ์์๋ ํ์คํ ์๊ฐ์ฐจ์ด๊ฐ ๋ง์ด๋ฒ์ด์ง๋ค์ฌโฆ
๊ทธ๋์ ์ ๊ฐ ์ ํํ๋ฐฉ๋ฒ์
ef core์ extension nuget์ ์ฌ์ฉํ์ต๋๋ค.
public async Task BulkInsertAuditableEntitiesAsync<T>(IList<T> entities, BulkConfig? bulkConfig = null,
Action<decimal>? progress = null, Type? type = null, CancellationToken cancellationToken = default)
where T : BaseAuditableEntity
{
entities.UpdateEntities(_userService.Id);
await this.BulkInsertAsync(entities, bulkConfig, progress, type, cancellationToken);
}
public async Task BulkInsertEntitiesAsync<T>(IList<T> entities, BulkConfig? bulkConfig = null,
Action<decimal>? progress = null, Type? type = null, CancellationToken cancellationToken = default)
where T : class
{
await this.BulkInsertAsync(entities, bulkConfig, progress, type, cancellationToken);
}
์์ ๊ฐ์ด ์งํํ์์ผ๋ฉฐ,

๋ค ํ ์คํธ๊ฐ ํต๊ณผ๋์์ต๋๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ์๋์ ๊ฐ์ด ์ ์ถ๊ฐ ๊ฐ๋ฅํ ๊ฒ ๊ฐ์ต๋๋ค.
- AddRange ๋ SaveChangesAsync๋ฅผ ํธ์ถํ๋ ์๊ฐ transaction์ ๊ฑด๋ค.
- Add๋ก ๋จ๊ฑด์ฉ ์ถ๊ฐํ๊ฒฝ์ฐ์๋ transaction์ ๊ฑธ์ง์๊ณ , ๋จ๊ฑด์ฉ ์ ์ฅํ๋ค.
๋ค๋ง, ๋ช๋ช๋ถ๋ถ๋ค์ ์ข ๋ ๊ณ ๋ฏผ์ด ํ์ํ ๊ฒ ๊ฐ์ง๋ง, EF Core์์์ ๋๋ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ๊ฑฐ๋, ์๋จ์์ Transaction์ด ๊ฑธ๋ ค์๋ค๋ฉด, BulkInsert Extension์ ๊ณ ๋ คํด๋ณผ๋ง ํ๊ฒ๊ฐ์ต๋๋ค.
์ถ๊ฐ๋ก, ์ ๊ฐ ํ๋ฆฐ๋ถ๋ถ์ด ์๊ฑฐ๋, ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ ์๊ฒ ์๋ ค์ฃผ์ธ์. ๊ฐ์ฌํฉ๋๋ค.