Swap to dapper and system.text.json for database backend

This commit is contained in:
ta264
2019-12-15 15:04:42 +00:00
parent 7b17c3e36c
commit d2065bfa1b
155 changed files with 2333 additions and 2340 deletions

View File

@@ -3,11 +3,10 @@ using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using Marr.Data;
using Marr.Data.QGen;
using NzbDrone.Common.Extensions;
using System.Reflection;
using System.Text;
using Dapper;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Datastore
@@ -31,43 +30,89 @@ namespace NzbDrone.Core.Datastore
bool HasItems();
void DeleteMany(IEnumerable<int> ids);
void SetFields(TModel model, params Expression<Func<TModel, object>>[] properties);
void SetFields(IList<TModel> models, params Expression<Func<TModel, object>>[] properties);
TModel Single();
PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec);
}
public class BasicRepository<TModel> : IBasicRepository<TModel> where TModel : ModelBase, new()
{
private readonly IDatabase _database;
private readonly IEventAggregator _eventAggregator;
private readonly PropertyInfo _keyProperty;
private readonly List<PropertyInfo> _properties;
private readonly string _updateSql;
private readonly string _insertSql;
protected IDataMapper DataMapper => _database.GetDataMapper();
protected readonly IDatabase _database;
protected readonly string _table;
protected string _selectTemplate;
protected string _deleteTemplate;
public BasicRepository(IDatabase database, IEventAggregator eventAggregator)
{
_database = database;
_eventAggregator = eventAggregator;
var type = typeof(TModel);
_table = TableMapping.Mapper.TableNameMapping(type);
_keyProperty = type.GetProperty(nameof(ModelBase.Id));
var excluded = TableMapping.Mapper.ExcludeProperties(type).Select(x => x.Name).ToList();
excluded.Add(_keyProperty.Name);
_properties = type.GetProperties().Where(x => !excluded.Contains(x.Name)).ToList();
_insertSql = GetInsertSql();
_updateSql = GetUpdateSql(_properties);
_selectTemplate = $"SELECT /**select**/ FROM {_table} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**orderby**/";
_deleteTemplate = $"DELETE FROM {_table} /**where**/";
}
protected QueryBuilder<TModel> Query => AddJoinQueries(DataMapper.Query<TModel>());
protected virtual SqlBuilder BuilderBase() => new SqlBuilder();
protected virtual SqlBuilder Builder() => BuilderBase().SelectAll();
protected void Delete(Expression<Func<TModel, bool>> filter)
protected virtual IEnumerable<TModel> GetResults(SqlBuilder.Template sql)
{
DataMapper.Delete(filter);
using (var conn = _database.OpenConnection())
{
return conn.Query<TModel>(sql.RawSql, sql.Parameters);
}
}
public IEnumerable<TModel> All()
protected List<TModel> Query(Expression<Func<TModel, bool>> where)
{
return AddJoinQueries(DataMapper.Query<TModel>()).ToList();
return Query(Builder().Where<TModel>(where));
}
protected List<TModel> Query(SqlBuilder builder)
{
return Query(builder, GetResults);
}
protected List<TModel> Query(SqlBuilder builder, Func<SqlBuilder.Template, IEnumerable<TModel>> queryFunc)
{
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
return queryFunc(sql).ToList();
}
public int Count()
{
return DataMapper.Query<TModel>().GetRowCount();
using (var conn = _database.OpenConnection())
{
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM {_table}");
}
}
public virtual IEnumerable<TModel> All()
{
return Query(Builder());
}
public TModel Get(int id)
{
var model = Query.Where(c => c.Id == id).SingleOrDefault();
var model = Query(x => x.Id == id).FirstOrDefault();
if (model == null)
{
@@ -79,13 +124,16 @@ namespace NzbDrone.Core.Datastore
public IEnumerable<TModel> Get(IEnumerable<int> ids)
{
var idList = ids.ToList();
var query = string.Format("Id IN ({0})", string.Join(",", idList));
var result = Query.Where(m => m.Id.In(idList)).ToList();
if (result.Count != idList.Count())
if (!ids.Any())
{
throw new ApplicationException($"Expected query to return {idList.Count} rows but returned {result.Count}");
return new List<TModel>();
}
var result = Query(x => ids.Contains(x.Id));
if (result.Count != ids.Count())
{
throw new ApplicationException($"Expected query to return {ids.Count()} rows but returned {result.Count}");
}
return result;
@@ -108,13 +156,70 @@ namespace NzbDrone.Core.Datastore
throw new InvalidOperationException("Can't insert model with existing ID " + model.Id);
}
DataMapper.Insert(model);
using (var conn = _database.OpenConnection())
{
model = Insert(conn, null, model);
}
ModelCreated(model);
return model;
}
private string GetInsertSql()
{
var sbColumnList = new StringBuilder(null);
for (var i = 0; i < _properties.Count; i++)
{
var property = _properties[i];
sbColumnList.AppendFormat("\"{0}\"", property.Name);
if (i < _properties.Count - 1)
sbColumnList.Append(", ");
}
var sbParameterList = new StringBuilder(null);
for (var i = 0; i < _properties.Count; i++)
{
var property = _properties[i];
sbParameterList.AppendFormat("@{0}", property.Name);
if (i < _properties.Count - 1)
sbParameterList.Append(", ");
}
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
{
var multi = connection.QueryMultiple(_insertSql, model, transaction);
var id = (int)multi.Read().First().id;
_keyProperty.SetValue(model, id);
return model;
}
public void InsertMany(IList<TModel> models)
{
if (models.Any(x => x.Id != 0))
{
throw new InvalidOperationException("Can't insert model with existing ID != 0");
}
using (var conn = _database.OpenConnection())
{
using (IDbTransaction tran = conn.BeginTransaction(IsolationLevel.ReadCommitted))
{
foreach (var model in models)
{
Insert(conn, tran, model);
}
tran.Commit();
}
}
}
public TModel Update(TModel model)
{
if (model.Id == 0)
@@ -122,52 +227,59 @@ namespace NzbDrone.Core.Datastore
throw new InvalidOperationException("Can't update model with ID 0");
}
DataMapper.Update(model, c => c.Id == model.Id);
using (var conn = _database.OpenConnection())
{
UpdateFields(conn, null, model, _properties);
}
ModelUpdated(model);
return model;
}
public void Delete(TModel model)
public void UpdateMany(IList<TModel> models)
{
Delete(model.Id);
}
public void InsertMany(IList<TModel> models)
{
using (var unitOfWork = new UnitOfWork(() => DataMapper))
if (models.Any(x => x.Id == 0))
{
unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted);
throw new InvalidOperationException("Can't update model with ID 0");
}
foreach (var model in models)
{
unitOfWork.DB.Insert(model);
}
unitOfWork.Commit();
using (var conn = _database.OpenConnection())
{
UpdateFields(conn, null, models, _properties);
}
}
public void UpdateMany(IList<TModel> models)
protected void Delete(Expression<Func<TModel, bool>> where)
{
using (var unitOfWork = new UnitOfWork(() => DataMapper))
Delete(Builder().Where<TModel>(where));
}
protected void Delete(SqlBuilder builder)
{
var sql = builder.AddTemplate(_deleteTemplate).LogQuery();
using (var conn = _database.OpenConnection())
{
unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted);
conn.Execute(sql.RawSql, sql.Parameters);
}
}
foreach (var model in models)
{
var localModel = model;
public void Delete(TModel model)
{
Delete(x => x.Id == model.Id);
}
if (model.Id == 0)
{
throw new InvalidOperationException("Can't update model with ID 0");
}
public void Delete(int id)
{
Delete(x => x.Id == id);
}
unitOfWork.DB.Update(model, c => c.Id == localModel.Id);
}
unitOfWork.Commit();
public void DeleteMany(IEnumerable<int> ids)
{
if (ids.Any())
{
Delete(x => ids.Contains(x.Id));
}
}
@@ -187,31 +299,13 @@ namespace NzbDrone.Core.Datastore
return model;
}
public void Delete(int id)
{
DataMapper.Delete<TModel>(c => c.Id == id);
}
public void DeleteMany(IEnumerable<int> ids)
{
using (var unitOfWork = new UnitOfWork(() => DataMapper))
{
unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted);
foreach (var id in ids)
{
var localId = id;
unitOfWork.DB.Delete<TModel>(c => c.Id == localId);
}
unitOfWork.Commit();
}
}
public void Purge(bool vacuum = false)
{
DataMapper.Delete<TModel>(c => c.Id > -1);
using (var conn = _database.OpenConnection())
{
conn.Execute($"DELETE FROM [{_table}]");
}
if (vacuum)
{
Vacuum();
@@ -232,43 +326,115 @@ namespace NzbDrone.Core.Datastore
{
if (model.Id == 0)
{
throw new InvalidOperationException("Attempted to updated model without ID");
throw new InvalidOperationException("Attempted to update model without ID");
}
DataMapper.Update<TModel>()
.Where(c => c.Id == model.Id)
.ColumnsIncluding(properties)
.Entity(model)
.Execute();
var propertiesToUpdate = properties.Select(x => x.GetMemberName()).ToList();
using (var conn = _database.OpenConnection())
{
UpdateFields(conn, null, model, propertiesToUpdate);
}
ModelUpdated(model);
}
public void SetFields(IList<TModel> models, params Expression<Func<TModel, object>>[] properties)
{
if (models.Any(x => x.Id == 0))
{
throw new InvalidOperationException("Attempted to update model without ID");
}
var propertiesToUpdate = properties.Select(x => x.GetMemberName()).ToList();
using (var conn = _database.OpenConnection())
{
UpdateFields(conn, null, models, propertiesToUpdate);
}
foreach(var model in models)
{
ModelUpdated(model);
}
}
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
{
var sb = new StringBuilder();
sb.AppendFormat("update {0} set ", _table);
for (var i = 0; i < propertiesToUpdate.Count; i++)
{
var property = propertiesToUpdate[i];
sb.AppendFormat("\"{0}\" = @{1}", property.Name, property.Name);
if (i < propertiesToUpdate.Count - 1)
sb.Append(", ");
}
sb.Append($" where \"{_keyProperty.Name}\" = @{_keyProperty.Name}");
return sb.ToString();
}
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, TModel model, List<PropertyInfo> propertiesToUpdate)
{
var sql = propertiesToUpdate == _properties ? _updateSql : GetUpdateSql(propertiesToUpdate);
connection.Execute(sql, model, transaction: transaction);
}
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate)
{
var sql = propertiesToUpdate == _properties ? _updateSql : GetUpdateSql(propertiesToUpdate);
connection.Execute(sql, models, transaction: transaction);
}
protected virtual SqlBuilder PagedBuilder() => BuilderBase();
protected virtual IEnumerable<TModel> PagedSelector(SqlBuilder.Template sql) => GetResults(sql);
public virtual PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec)
{
pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList();
pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount();
pagingSpec.Records = GetPagedRecords(PagedBuilder().SelectAll(), pagingSpec, PagedSelector);
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder().SelectCount(), pagingSpec);
return pagingSpec;
}
protected virtual SortBuilder<TModel> GetPagedQuery(QueryBuilder<TModel> query, PagingSpec<TModel> pagingSpec)
private void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
{
var filterExpressions = pagingSpec.FilterExpressions;
var sortQuery = query.Where(filterExpressions.FirstOrDefault());
var filters = pagingSpec.FilterExpressions;
if (filterExpressions.Count > 1)
foreach (var filter in filters)
{
// Start at the second item for the AndWhere clauses
for (var i = 1; i < filterExpressions.Count; i++)
{
sortQuery.AndWhere(filterExpressions[i]);
}
builder.Where<TModel>(filter);
}
}
return sortQuery.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
.Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize);
protected List<TModel> GetPagedRecords(SqlBuilder builder, PagingSpec<TModel> pagingSpec, Func<SqlBuilder.Template, IEnumerable<TModel>> queryFunc)
{
AddFilters(builder, pagingSpec);
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
var pagingOffset = (pagingSpec.Page - 1)*pagingSpec.PageSize;
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
return queryFunc(sql).ToList();
}
protected int GetPagedRecordCount(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
{
AddFilters(builder, pagingSpec);
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
using (var conn = _database.OpenConnection())
{
return conn.ExecuteScalar<int>(sql.RawSql, sql.Parameters);
}
}
protected void ModelCreated(TModel model)
@@ -294,11 +460,6 @@ namespace NzbDrone.Core.Datastore
}
}
protected virtual QueryBuilder<TActual> AddJoinQueries<TActual>(QueryBuilder<TActual> baseQuery)
{
return baseQuery;
}
protected virtual bool PublishModelEvents => false;
}
}