aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs')
-rw-r--r--lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs507
1 files changed, 507 insertions, 0 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
new file mode 100644
index 0000000..8cf4e2e
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs
@@ -0,0 +1,507 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: DbStore.cs
+*
+* DbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory.Caching;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace VNLib.Plugins.Extensions.Data
+{
+ /// <summary>
+ /// Implements basic data-store functionality with abstract query builders
+ /// </summary>
+ /// <typeparam name="T">A <see cref="DbModelBase"/> implemented type</typeparam>
+ public abstract class DbStore<T> : IDataStore<T>, IPaginatedDataStore<T> where T: class, IDbModel
+ {
+ /// <summary>
+ /// Gets a unique ID for a new record being added to the store
+ /// </summary>
+ public abstract string RecordIdBuilder { get; }
+ /// <summary>
+ /// Gets a new <see cref="TransactionalDbContext"/> ready for use
+ /// </summary>
+ /// <returns></returns>
+ public abstract TransactionalDbContext NewContext();
+
+ /// <summary>
+ /// An object rental for entity collections
+ /// </summary>
+ public ObjectRental<List<T>> ListRental { get; } = ObjectRental.Create<List<T>>(null, static ret => ret.Clear());
+
+ #region Add Or Update
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> AddOrUpdateAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ IQueryable<T> query;
+ if (string.IsNullOrWhiteSpace(record.Id))
+ {
+ //Get the application
+ query = AddOrUpdateQueryBuilder(ctx, record);
+ }
+ else
+ {
+ //Get the application
+ query = (from et in ctx.Set<T>()
+ where et.Id == record.Id
+ select et);
+ }
+ //Using single
+ T? entry = await query.SingleOrDefaultAsync();
+ //Check if creted
+ if (entry == null)
+ {
+ //Create a new template id
+ record.Id = RecordIdBuilder;
+ //Set the created/lm times
+ record.Created = record.LastModified = DateTime.UtcNow;
+ //Add the new template to the ctx
+ ctx.Add(record);
+ }
+ else
+ {
+ OnRecordUpdate(record, entry);
+ }
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //commit transaction if update was successful
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> UpdateAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the application
+ IQueryable<T> query = UpdateQueryBuilder(ctx, record);
+ //Using single to make sure only one app is in the db (should never be an issue)
+ T? oldEntry = await query.SingleOrDefaultAsync();
+ if (oldEntry == null)
+ {
+ return false;
+ }
+ //Update the template meta-data
+ OnRecordUpdate(record, oldEntry);
+ //Only publish update if changes happened
+ if (!ctx.ChangeTracker.HasChanges())
+ {
+ //commit transaction if no changes need to be made
+ await ctx.CommitTransactionAsync();
+ return true;
+ }
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //commit transaction if update was successful
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> CreateAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Create a new template id
+ record.Id = RecordIdBuilder;
+ //Update the created/last modified time of the record
+ record.Created = record.LastModified = DateTime.UtcNow;
+ //Add the new template
+ ctx.Add(record);
+ //save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Builds a query that attempts to get a single entry from the
+ /// store based on the specified record if it does not have a
+ /// valid <see cref="DbModelBase.Id"/> property
+ /// </summary>
+ /// <param name="context">The active context to query</param>
+ /// <param name="record">The record to search for</param>
+ /// <returns>A query that yields a single record if it exists in the store</returns>
+ protected virtual IQueryable<T> AddOrUpdateQueryBuilder(TransactionalDbContext context, T record)
+ {
+ //default to get single of the specific record
+ return GetSingleQueryBuilder(context, record);
+ }
+ /// <summary>
+ /// Builds a query that attempts to get a single entry from the
+ /// store to update based on the specified record
+ /// </summary>
+ /// <param name="context">The active context to query</param>
+ /// <param name="record">The record to search for</param>
+ /// <returns>A query that yields a single record to update if it exists in the store</returns>
+ protected virtual IQueryable<T> UpdateQueryBuilder(TransactionalDbContext context, T record)
+ {
+ //default to get single of the specific record
+ return GetSingleQueryBuilder(context, record);
+ }
+ /// <summary>
+ /// Updates the current record (if found) to the new record before
+ /// storing the updates.
+ /// </summary>
+ /// <param name="newRecord">The new record to capture data from</param>
+ /// <param name="currentRecord">The current record to be updated</param>
+ protected abstract void OnRecordUpdate(T newRecord, T currentRecord);
+ #endregion
+
+ #region Delete
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> DeleteAsync(string key)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the template by its id
+ IQueryable<T> query = (from temp in ctx.Set<T>()
+ where temp.Id == key
+ select temp);
+ T? record = await query.SingleOrDefaultAsync();
+ if (record == null)
+ {
+ return false;
+ }
+ //Add the new application
+ ctx.Remove(record);
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> DeleteAsync(T record)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get a query for a a single item
+ IQueryable<T> query = GetSingleQueryBuilder(ctx, record);
+ //Get the entry
+ T? entry = await query.SingleOrDefaultAsync();
+ if (entry == null)
+ {
+ return false;
+ }
+ //Add the new application
+ ctx.Remove(entry);
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> DeleteAsync(params string[] specifiers)
+ {
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the template by its id
+ IQueryable<T> query = DeleteQueryBuilder(ctx, specifiers);
+ T? entry = await query.SingleOrDefaultAsync();
+ if (entry == null)
+ {
+ return false;
+ }
+ //Add the new application
+ ctx.Remove(entry);
+ //Save changes
+ ERRNO result = await ctx.SaveChangesAsync();
+ if (result)
+ {
+ //Commit transaction
+ await ctx.CommitTransactionAsync();
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Builds a query that results in a single entry to delete from the
+ /// constraint arguments
+ /// </summary>
+ /// <param name="context">The active context</param>
+ /// <param name="constraints">A variable length parameter array of query constraints</param>
+ /// <returns>A query that yields a single record (or no record) to delete</returns>
+ protected virtual IQueryable<T> DeleteQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ //default use the get-single method, as the implementation is usually identical
+ return GetSingleQueryBuilder(context, constraints);
+ }
+ #endregion
+
+ #region Get Collection
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit)
+ {
+ int previous = collection.Count;
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ await GetCollectionQueryBuilder(ctx, specifier).Take(limit).Select(static e => e).ForEachAsync(collection.Add);
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of elements add to the collection
+ return collection.Count - previous;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args)
+ {
+ int previous = collection.Count;
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ await GetCollectionQueryBuilder(ctx, args).Take(limit).Select(static e => e).ForEachAsync(collection.Add);
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of elements add to the collection
+ return collection.Count - previous;
+ }
+
+ /// <summary>
+ /// Builds a query to get a count of records constrained by the specifier
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="specifier">The specifier constrain</param>
+ /// <returns>A query that can be counted</returns>
+ protected virtual IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, string specifier)
+ {
+ return GetCollectionQueryBuilder(context, new string[] { specifier });
+ }
+
+ /// <summary>
+ /// Builds a query to get a collection of records based on an variable length array of parameters
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="constraints">An arguments array to constrain the results of the query</param>
+ /// <returns>A query that returns a collection of records from the store</returns>
+ protected abstract IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints);
+
+ #endregion
+
+ #region Get Count
+ ///<inheritdoc/>
+ public virtual async Task<long> GetCountAsync()
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Async get the number of records of the given entity type
+ long count = await ctx.Set<T>().LongCountAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return count;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<long> GetCountAsync(string specifier)
+ {
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Async get the number of records of the given entity type
+ long count = await GetCountQueryBuilder(ctx, specifier).LongCountAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return count;
+ }
+
+ /// <summary>
+ /// Builds a query to get a count of records constrained by the specifier
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="specifier">The specifier constrain</param>
+ /// <returns>A query that can be counted</returns>
+ protected virtual IQueryable<T> GetCountQueryBuilder(TransactionalDbContext context, string specifier)
+ {
+ //Default use the get collection and just call the count method
+ return GetCollectionQueryBuilder(context, specifier);
+ }
+ #endregion
+
+ #region Get Single
+ ///<inheritdoc/>
+ public virtual async Task<T?> GetSingleAsync(string key)
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ T? record = await (from entry in ctx.Set<T>()
+ where entry.Id == key
+ select entry)
+ .SingleOrDefaultAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return record;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<T?> GetSingleAsync(T record)
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return record;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<T?> GetSingleAsync(params string[] specifiers)
+ {
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ T? record = await GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync();
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ return record;
+ }
+ /// <summary>
+ /// Builds a query to get a single record from the variable length parameter arguments
+ /// </summary>
+ /// <param name="context">The context to execute query against</param>
+ /// <param name="constraints">Arguments to constrain the results of the query to a single record</param>
+ /// <returns>A query that yields a single record</returns>
+ protected abstract IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints);
+ /// <summary>
+ /// <para>
+ /// Builds a query to get a single record from the specified record.
+ /// </para>
+ /// <para>
+ /// Unless overridden, performs an ID based query for a single entry
+ /// </para>
+ /// </summary>
+ /// <param name="context">The context to execute query against</param>
+ /// <param name="record">A record to referrence the lookup</param>
+ /// <returns>A query that yields a single record</returns>
+ protected virtual IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, T record)
+ {
+ return from entry in context.Set<T>()
+ where entry.Id == record.Id
+ select entry;
+ }
+ #endregion
+
+ #region Get Page
+ ///<inheritdoc/>
+ public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit)
+ {
+ //Store preivous count
+ int previous = collection.Count;
+ //Open db connection
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get a page offset and a limit for the
+ await ctx.Set<T>()
+ .Skip(page * limit)
+ .Take(limit)
+ .Select(static p => p)
+ .ForEachAsync(collection.Add);
+
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of records added
+ return collection.Count - previous;
+ }
+ ///<inheritdoc/>
+ public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints)
+ {
+ //Store preivous count
+ int previous = collection.Count;
+ //Open new db context
+ await using TransactionalDbContext ctx = NewContext();
+ //Open transaction
+ await ctx.OpenTransactionAsync();
+ //Get the single template by its id
+ await GetPageQueryBuilder(ctx, constraints)
+ .Skip(page * limit)
+ .Take(limit)
+ .Select(static e => e)
+ .ForEachAsync(collection.Add);
+
+ //close db and transaction
+ await ctx.CommitTransactionAsync();
+ //Return the number of records added
+ return collection.Count - previous;
+ }
+ /// <summary>
+ /// Builds a query to get a collection of records based on an variable length array of parameters
+ /// </summary>
+ /// <param name="context">The active context to run the query on</param>
+ /// <param name="constraints">An arguments array to constrain the results of the query</param>
+ /// <returns>A query that returns a paginated collection of records from the store</returns>
+ protected virtual IQueryable<T> GetPageQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ //Default to getting the entire collection and just selecting a single page
+ return GetCollectionQueryBuilder(context, constraints);
+ }
+ #endregion
+ }
+}