mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
Swap to dapper and system.text.json for database backend
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user