Hello team,
I have a table that is similar to Organization Units, where there is a code column to store the sequence of the record. So for each record inserted under a root, it will be 00001.00001, 00001.00002, 00001.00003, 00001.00004 etc... however, if I call my insert API multiple times in quick succession, the code wont be sequential... meaning it will 00001.00001, 00001.00001, 00001.00001, 00001.00002 is there a way to control the concurrency on a table?
Thank you
4 Answer(s)
-
0
Concurrency control is usually about reading or writing the same record (Optimistic Locking: #4146@6c148c17-0f2f-42c9-a386-dd70810dd348).
In your case, you are inserting a new record. Can you show related code on how you determine and set the next "code"?
Looks like you need to implement a singleton lock.
-
0
Hello aaron, thank you for your reply,
my entity is Sample, this is my application service to create a new Sample
public async Task<SampleDto> CreateNewSample(CreateNewSampleInput input) { var sample = ObjectMapper.Map<Sample>(input); var insertedSample = await _sampleManager.CreateNewSample(sample); return ObjectMapper.Map<SampleDto>(insertedSample); }
This is the sample manager (i left out some unrelated business logic) Sample's Line is similar to the Organization Unit's Code
public async Task<Sample> CreateNewSample(Sample sample) { .... sample.Line = await GetNextChildCodeAsync(sample.ParentSampleId); .... var insertedSample = await _sampleRepository.InsertAsync(sample); await CurrentUnitOfWork.SaveChangesAsync(); return insertedSample; }
This is taken from organization unit manager's codes
public virtual async Task<string> GetNextChildCodeAsync(long? parentId) { var lastChild = await GetLastChildOrNullAsync(parentId); if (lastChild == null) { var parentCode = parentId != null ? await GetCodeAsync(parentId.Value) : null; return Sample.AppendCode(parentCode, Sample.CreateCode(1)); } return Sample.CalculateNextCode(lastChild.Line); } public virtual async Task<Sample> GetLastChildOrNullAsync(long? parentId) { var children = await _sampleRepository.GetAllListAsync(ou => ou.ParentSampleId == parentId); return children.OrderBy(c => c.Line).LastOrDefault(); } public virtual async Task<string> GetCodeAsync(long id) { return (await _sampleRepository.GetAsync(id)).Line; }
this is my sample entity model, most of it taken from organization unit
public class Sample : FullAuditedEntity<long>, IMustHaveTenant { /// <summary> /// Maximum depth of an UO hierarchy. /// </summary> public const int MaxDepth = 16; /// <summary> /// Length of a code unit between dots. /// </summary> public const int CodeUnitLength = 6; /// <summary> /// Maximum length of the <see cref="Line"/> property. /// </summary> public const int MaxCodeLength = MaxDepth * (CodeUnitLength + 1) - 1; public Sample() { this.ChildSamples = new HashSet<Sample>(); } /// <summary> /// Initializes a new instance of the <see cref="Sample"/> class. /// </summary> /// <param name="tenantId">Tenant's Id or null for host.</param> /// <param name="sampleDefinitionId">Sample Definition Id.</param> /// <param name="parentId">Parent's Id or null if OU is a root.</param> public Sample(int tenantId, long? sampleDefinitionId, long? parentId = null) { TenantId = tenantId; SampleDefinitionId = sampleDefinitionId; ParentSampleId = parentId; } ..... [ForeignKey("ParentSampleId")] public Sample ParentSample { get; set; } public long? ParentSampleId { get; set; } ..... /// <summary> /// Hierarchical Lineage of this sample. /// Example: "00001.00042.00005". /// It's changeable if OU hierarch is changed. /// </summary> [StringLength(MaxCodeLength)] public string Line { get; set; } ..... public virtual ICollection<Sample> ChildSamples { get; set; } public int TenantId { get; set; } /// <summary> /// Gets user friendly code to be displayed. /// Example: if code = "00019.00055.00001" then returns "19.55.1". /// </summary> /// <param name="code">The code.</param> /// <param name="parentCode">The parent code.</param> public static string GetUserFriendlyCode(string code) { List<string> friendlyCodeList = new List<string>(); var codeList = code.Split("."); foreach (var codePart in codeList) { friendlyCodeList.Add(codePart.TrimStart('0')); } return String.Join(".", friendlyCodeList); } /// <summary> /// Creates code for given numbers. /// Example: if numbers are 4,2 then returns "00004.00002"; /// </summary> /// <param name="numbers">Numbers</param> public static string CreateCode(params int[] numbers) { if (numbers.IsNullOrEmpty()) { return null; } return numbers.Select(number => number.ToString(new string('0', CodeUnitLength))).JoinAsString("."); } /// <summary> /// Appends a child code to a parent code. /// Example: if parentCode = "00001", childCode = "00042" then returns "00001.00042". /// </summary> /// <param name="parentCode">Parent code. Can be null or empty if parent is a root.</param> /// <param name="childCode">Child code.</param> public static string AppendCode(string parentCode, string childCode) { if (childCode.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(childCode), "childCode can not be null or empty."); } if (parentCode.IsNullOrEmpty()) { return childCode; } return parentCode + "." + childCode; } /// <summary> /// Gets relative code to the parent. /// Example: if code = "00019.00055.00001" and parentCode = "00019" then returns "00055.00001". /// </summary> /// <param name="code">The code.</param> /// <param name="parentCode">The parent code.</param> public static string GetRelativeCode(string code, string parentCode) { if (code.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(code), "code can not be null or empty."); } if (parentCode.IsNullOrEmpty()) { return code; } if (code.Length == parentCode.Length) { return null; } return code.Substring(parentCode.Length + 1); } /// <summary> /// Calculates next code for given code. /// Example: if code = "00019.00055.00001" returns "00019.00055.00002". /// </summary> /// <param name="code">The code.</param> public static string CalculateNextCode(string code) { if (code.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(code), "code can not be null or empty."); } var parentCode = GetParentCode(code); var lastUnitCode = GetLastUnitCode(code); return AppendCode(parentCode, CreateCode(Convert.ToInt32(lastUnitCode) + 1)); } /// <summary> /// Gets the last unit code. /// Example: if code = "00019.00055.00001" returns "00001". /// </summary> /// <param name="code">The code.</param> public static string GetLastUnitCode(string code) { if (code.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(code), "code can not be null or empty."); } var splittedCode = code.Split('.'); return splittedCode[splittedCode.Length - 1]; } /// <summary> /// Gets parent code. /// Example: if code = "00019.00055.00001" returns "00019.00055". /// </summary> /// <param name="code">The code.</param> public static string GetParentCode(string code) { if (code.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(code), "code can not be null or empty."); } var splittedCode = code.Split('.'); if (splittedCode.Length == 1) { return null; } return splittedCode.Take(splittedCode.Length - 1).JoinAsString("."); } }
I notice that whenever the CreateNewSample api is called multiple times with the same parent id, the Line doesn't set correctly..
-
0
Yes, so it is currently possible for the next code to be calculated before an insert is completed.
A quick fix is to lock with a private static _syncObj:
private static readonly object _syncObj = new object(); public async Task<Sample> CreateNewSample(Sample sample) { // ... lock (_syncObj) { AsyncHelper.RunSync(async () => { sample.Line = await GetNextChildCodeAsync(sample.ParentSampleId); // ... var insertedSample = await _sampleRepository.InsertAsync(sample); await CurrentUnitOfWork.SaveChangesAsync(); } } }
Note that:
- You must run synchronous code in lock.
- Requests cannot no longer run in parallel.
A better alternative is to retrieve the next code from a singleton cache that has a lock, like in UserFriendsCache. Then, you can store the next code as a variable in the cache instead of reading from the database every time. You can try to implement that yourself.
-
0
Thank you for your help, I will try the first method, the second method seems hard, I'm not sure how to store the next code in cache, because the next code is not always in a sequence as it might be under a different parent