diff options
author | crupest <crupest@outlook.com> | 2024-11-11 01:12:29 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2024-12-19 21:42:01 +0800 |
commit | f9aa02ec1a4c24e80a206857d4f68198bb027bb4 (patch) | |
tree | 5994f0a62733b13f9f330e3515260ae20dc4a0bd /dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs | |
parent | 7b4d49e4bbdff6ddf1f8f7e937130e700024d5e9 (diff) | |
download | crupest-f9aa02ec1a4c24e80a206857d4f68198bb027bb4.tar.gz crupest-f9aa02ec1a4c24e80a206857d4f68198bb027bb4.tar.bz2 crupest-f9aa02ec1a4c24e80a206857d4f68198bb027bb4.zip |
HALF WORK: 2024.12.19
Re-organize file structure.
Diffstat (limited to 'dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs')
-rw-r--r-- | dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs new file mode 100644 index 0000000..4a7ea95 --- /dev/null +++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs @@ -0,0 +1,628 @@ +using System.Data; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Dapper; + +namespace CrupestApi.Commons.Crud; + +/// <summary> +/// Contains all you need to manipulate a table. +/// </summary> +public class TableInfo +{ + private readonly IColumnTypeProvider _columnTypeProvider; + private readonly Lazy<List<string>> _lazyColumnNameList; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger<TableInfo> _logger; + + public TableInfo(Type entityType, IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) + : this(entityType.Name, entityType, columnTypeProvider, loggerFactory) + { + } + + public TableInfo(string tableName, Type entityType, IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger<TableInfo>(); + + _logger.LogInformation("Create TableInfo for entity type '{}'.", entityType.Name); + + _columnTypeProvider = columnTypeProvider; + + TableName = tableName; + EntityType = entityType; + + + var properties = entityType.GetProperties(); + _logger.LogInformation("Find following properties: {}", string.Join(", ", properties.Select(p => p.Name))); + + var columnInfos = new List<ColumnInfo>(); + + bool hasId = false; + ColumnInfo? primaryKeyColumn = null; + ColumnInfo? keyColumn = null; + + List<PropertyInfo> nonColumnProperties = new(); + + foreach (var property in properties) + { + _logger.LogInformation("Check property '{}'.", property.Name); + if (CheckPropertyIsColumn(property)) + { + _logger.LogInformation("{} is a column, create ColumnInfo for it.", property.Name); + var columnInfo = new ColumnInfo(this, property, _columnTypeProvider, _loggerFactory); + columnInfos.Add(columnInfo); + if (columnInfo.IsPrimaryKey) + { + _logger.LogInformation("Column {} is a primary key.", property.Name); + primaryKeyColumn = columnInfo; + } + if (columnInfo.ColumnName.Equals("id", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Column {} has name id.", property.Name); + hasId = true; + } + if (columnInfo.IsSpecifiedAsKey) + { + if (keyColumn is not null) + { + throw new Exception("Already exists a key column."); + } + _logger.LogInformation("Column {} is specified as key.", property.Name); + keyColumn = columnInfo; + } + } + else + { + _logger.LogInformation("{} is not a column.", property.Name); + nonColumnProperties.Add(property); + } + } + + if (primaryKeyColumn is null) + { + if (hasId) throw new Exception("A column named id already exists but is not primary key."); + _logger.LogInformation("No primary key column found, create one automatically."); + primaryKeyColumn = CreateAutoIdColumn(); + columnInfos.Add(primaryKeyColumn); + } + + if (keyColumn is null) + { + _logger.LogInformation("No key column is specified, will use primary key."); + keyColumn = primaryKeyColumn; + } + + Columns = columnInfos; + PrimaryKeyColumn = primaryKeyColumn; + KeyColumn = keyColumn; + NonColumnProperties = nonColumnProperties; + + _logger.LogInformation("Check table validity."); + CheckValidity(); + + _logger.LogInformation("TableInfo succeeded to create."); + + _lazyColumnNameList = new Lazy<List<string>>(() => Columns.Select(c => c.ColumnName).ToList()); + } + + private ColumnInfo CreateAutoIdColumn() + { + return new ColumnInfo(this, + new ColumnAttribute + { + ColumnName = "Id", + NotNull = true, + IsPrimaryKey = true, + }, + typeof(long), _columnTypeProvider, _loggerFactory); + } + + public Type EntityType { get; } + public string TableName { get; } + public IReadOnlyList<ColumnInfo> Columns { get; } + public IReadOnlyList<ColumnInfo> PropertyColumns => Columns.Where(c => c.PropertyInfo is not null).ToList(); + public ColumnInfo PrimaryKeyColumn { get; } + /// <summary> + /// Maybe not the primary key. But acts as primary key. + /// </summary> + /// <seealso cref="ColumnMetadataKeys.ActAsKey"/> + public ColumnInfo KeyColumn { get; } + public IReadOnlyList<PropertyInfo> ColumnProperties => PropertyColumns.Select(c => c.PropertyInfo!).ToList(); + public IReadOnlyList<PropertyInfo> NonColumnProperties { get; } + public IReadOnlyList<string> ColumnNameList => _lazyColumnNameList.Value; + + protected bool CheckPropertyIsColumn(PropertyInfo property) + { + var columnAttribute = property.GetCustomAttribute<ColumnAttribute>(); + if (columnAttribute is null) return false; + return true; + } + + public ColumnInfo GetColumn(string columnName) + { + foreach (var column in Columns) + { + if (column.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)) + { + return column; + } + } + throw new KeyNotFoundException("No such column with given name."); + } + + public void CheckGeneratedColumnHasGenerator() + { + foreach (var column in Columns) + { + if (column.IsOnlyGenerated && column.DefaultValueGeneratorMethod is null) + { + throw new Exception($"Column '{column.ColumnName}' is generated but has no generator."); + } + } + } + + public void CheckValidity() + { + // Check if there is only one primary key. + bool hasPrimaryKey = false; + bool hasKey = false; + foreach (var column in Columns) + { + if (column.IsPrimaryKey) + { + if (hasPrimaryKey) throw new Exception("More than one columns are primary key."); + hasPrimaryKey = true; + } + + if (column.IsSpecifiedAsKey) + { + if (hasKey) throw new Exception("More than one columns are specified as key column."); + } + } + + if (!hasPrimaryKey) throw new Exception("No column is primary key."); + + // Check two columns have the same sql name. + HashSet<string> sqlNameSet = new HashSet<string>(); + + foreach (var column in Columns) + { + if (sqlNameSet.Contains(column.ColumnName)) + throw new Exception($"Two columns have the same sql name '{column.ColumnName}'."); + sqlNameSet.Add(column.ColumnName); + } + + CheckGeneratedColumnHasGenerator(); + } + + public string GenerateCreateIndexSql(string? dbProviderId = null) + { + var sb = new StringBuilder(); + + foreach (var column in Columns) + { + if (column.Index == ColumnIndexType.None) continue; + + sb.Append($"CREATE {(column.Index == ColumnIndexType.Unique ? "UNIQUE" : "")} INDEX {TableName}_{column.ColumnName}_index ON {TableName} ({column.ColumnName});\n"); + } + + return sb.ToString(); + } + + public string GenerateCreateTableSql(bool createIndex = true, string? dbProviderId = null) + { + var tableName = TableName; + var columnSql = string.Join(",\n", Columns.Select(c => c.GenerateCreateTableColumnString(dbProviderId))); + + var sql = $@" +CREATE TABLE {tableName}( + {columnSql} +); + "; + + if (createIndex) + { + sql += GenerateCreateIndexSql(dbProviderId); + } + + return sql; + } + + public void CheckColumnName(string columnName) + { + if (!ColumnNameList.Contains(columnName)) + { + throw new ArgumentException($"Column {columnName} is not in the table."); + } + } + + public void CheckRelatedColumns(IClause? clause) + { + if (clause is not null) + { + var relatedColumns = clause.GetRelatedColumns(); + foreach (var column in relatedColumns) + { + CheckColumnName(column); + } + } + } + + /// <summary> + /// If you call this manually, it's your duty to call hooks. + /// </summary> + /// <seealso cref="SelectDynamic"/> + public (string sql, ParamList parameters) GenerateSelectSql(string? selectWhat, IWhereClause? whereClause, IOrderByClause? orderByClause = null, int? skip = null, int? limit = null, string? dbProviderId = null) + { + CheckRelatedColumns(whereClause); + CheckRelatedColumns(orderByClause); + + var parameters = new ParamList(); + + StringBuilder result = new StringBuilder() + .Append($"SELECT {selectWhat ?? "*"} FROM ") + .Append(TableName); + + if (whereClause is not null) + { + result.Append(" WHERE "); + var (whereSql, whereParameters) = whereClause.GenerateSql(dbProviderId); + parameters.AddRange(whereParameters); + result.Append(whereSql); + } + + if (orderByClause is not null) + { + result.Append(' '); + var orderBySql = orderByClause.GenerateSql(dbProviderId); + result.Append(orderBySql); + } + + if (limit is not null) + { + result.Append(" LIMIT @Limit"); + parameters.Add("Limit", limit.Value); + } + + if (skip is not null) + { + result.Append(" OFFSET @Skip"); + parameters.Add("Skip", skip.Value); + } + + result.Append(';'); + + return (result.ToString(), parameters); + } + + /// <summary> + /// If you call this manually, it's your duty to call hooks. + /// </summary> + /// <seealso cref="Insert"/> + public (string sql, ParamList parameters) GenerateInsertSql(IInsertClause insertClause, string? dbProviderId = null) + { + CheckRelatedColumns(insertClause); + + var parameters = new ParamList(); + + var result = new StringBuilder() + .Append("INSERT INTO ") + .Append(TableName) + .Append(" (") + .Append(insertClause.GenerateColumnListSql(dbProviderId)) + .Append(") VALUES ("); + + var (valueSql, valueParameters) = insertClause.GenerateValueListSql(dbProviderId); + result.Append(valueSql).Append(");"); + + parameters.AddRange(valueParameters); + + return (result.ToString(), parameters); + } + + /// <summary> + /// If you call this manually, it's your duty to call hooks. + /// </summary> + /// <seealso cref="Update"/> + public (string sql, ParamList parameters) GenerateUpdateSql(IWhereClause? whereClause, IUpdateClause updateClause) + { + CheckRelatedColumns(whereClause); + CheckRelatedColumns(updateClause); + + var parameters = new ParamList(); + + StringBuilder sb = new StringBuilder("UPDATE "); + sb.Append(TableName); + sb.Append(" SET "); + var (updateSql, updateParameters) = updateClause.GenerateSql(); + sb.Append(updateSql); + parameters.AddRange(updateParameters); + if (whereClause is not null) + { + sb.Append(" WHERE "); + var (whereSql, whereParameters) = whereClause.GenerateSql(); + sb.Append(whereSql); + parameters.AddRange(whereParameters); + } + sb.Append(';'); + + return (sb.ToString(), parameters); + } + + /// <summary> + /// If you call this manually, it's your duty to call hooks. + /// </summary> + /// <seealso cref="Delete"/> + public (string sql, ParamList parameters) GenerateDeleteSql(IWhereClause? whereClause) + { + CheckRelatedColumns(whereClause); + + var parameters = new ParamList(); + + StringBuilder sb = new StringBuilder("DELETE FROM "); + sb.Append(TableName); + if (whereClause is not null) + { + sb.Append(" WHERE "); + var (whereSql, whereParameters) = whereClause.GenerateSql(); + parameters.AddRange(whereParameters); + sb.Append(whereSql); + } + sb.Append(';'); + + return (sb.ToString(), parameters); + } + + private DynamicParameters ConvertParameters(ParamList parameters) + { + var result = new DynamicParameters(); + foreach (var param in parameters) + { + if (param.Value is null || param.Value is DbNullValue) + { + result.Add(param.Name, null); + continue; + } + + var columnName = param.ColumnName; + IColumnTypeInfo typeInfo; + if (columnName is not null) + { + typeInfo = GetColumn(columnName).ColumnType; + } + else + { + typeInfo = _columnTypeProvider.Get(param.Value.GetType()); + } + + result.Add(param.Name, typeInfo.ConvertToDatabase(param.Value), typeInfo.DbType); + } + return result; + } + + /// <summary> + /// ConvertParameters. Select. Call hooks. + /// </summary> + public virtual List<dynamic> SelectDynamic(IDbConnection dbConnection, string? what = null, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + var (sql, parameters) = GenerateSelectSql(what, where, orderBy, skip, limit); + var queryResult = dbConnection.Query<dynamic>(sql, ConvertParameters(parameters)); + return queryResult.ToList(); + } + + public virtual int SelectCount(IDbConnection dbConnection, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + var (sql, parameters) = GenerateSelectSql("COUNT(*)", where, orderBy, skip, limit); + var result = dbConnection.QuerySingle<int>(sql, ConvertParameters(parameters)); + return result; + } + + public virtual TResult MapDynamicTo<TResult>(dynamic d) + { + var dict = (IDictionary<string, object?>)d; + + var result = Activator.CreateInstance<TResult>(); + Type resultType = typeof(TResult); + + foreach (var column in Columns) + { + var resultProperty = resultType.GetProperty(column.ColumnName); + if (dict.ContainsKey(column.ColumnName) && resultProperty is not null) + { + if (dict[column.ColumnName] is null) + { + resultProperty.SetValue(result, null); + continue; + } + object? value = Convert.ChangeType(dict[column.ColumnName], column.ColumnType.DatabaseClrType); + value = column.ColumnType.ConvertFromDatabase(value); + resultProperty.SetValue(result, value); + } + } + + return result; + } + + /// <summary> + /// Select and call hooks. + /// </summary> + public virtual List<TResult> Select<TResult>(IDbConnection dbConnection, string? what = null, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + List<dynamic> queryResult = SelectDynamic(dbConnection, what, where, orderBy, skip, limit).ToList(); + + return queryResult.Select(MapDynamicTo<TResult>).ToList(); + } + + public IInsertClause ConvertEntityToInsertClause(object entity) + { + Debug.Assert(EntityType.IsInstanceOfType(entity)); + var result = new InsertClause(); + foreach (var column in PropertyColumns) + { + var value = column.PropertyInfo!.GetValue(entity); + result.Add(column.ColumnName, value); + } + return result; + } + + /// <summary> + /// Insert a entity and call hooks. + /// </summary> + /// <returns>The key of insert entity.</returns> + public int Insert(IDbConnection dbConnection, IInsertClause insert, out object key) + { + object? finalKey = null; + + var realInsert = InsertClause.Create(); + + foreach (var column in Columns) + { + InsertItem? item = insert.Items.SingleOrDefault(i => i.ColumnName == column.ColumnName); + + var value = item?.Value; + + if (column.IsOnlyGenerated && value is not null) + { + throw new Exception($"The column '{column.ColumnName}' is auto generated. You can't specify it explicitly."); + } + + if (value is null) + { + value = column.GenerateDefaultValue(); + } + + if (value is null && column.IsAutoIncrement) + { + continue; + } + + if (value is null) + { + value = DbNullValue.Instance; + } + + column.InvokeValidator(value); + + InsertItem realInsertItem; + + if (value is DbNullValue) + { + if (column.IsNotNull) + { + throw new Exception($"Column '{column.ColumnName}' is not nullable. Please specify a non-null value."); + } + + realInsertItem = new InsertItem(column.ColumnName, null); + } + else + { + realInsertItem = new InsertItem(column.ColumnName, value); + } + + realInsert.Add(realInsertItem); + + if (realInsertItem.ColumnName == KeyColumn.ColumnName) + { + finalKey = realInsertItem.Value; + } + } + + if (finalKey is null) throw new Exception("No key???"); + key = finalKey; + + var (sql, parameters) = GenerateInsertSql(realInsert); + + var affectedRowCount = dbConnection.Execute(sql, ConvertParameters(parameters)); + + if (affectedRowCount != 1) + throw new Exception("Failed to insert."); + + return affectedRowCount; + } + + /// <summary> + /// Upgrade a entity and call hooks. + /// </summary> + /// <returns>The key of insert entity.</returns> + public virtual int Update(IDbConnection dbConnection, IWhereClause? where, IUpdateClause update, out object? newKey) + { + newKey = null; + + var realUpdate = UpdateClause.Create(); + + foreach (var column in Columns) + { + UpdateItem? item = update.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); + object? value = item?.Value; + + if (value is not null) + { + if (column.IsNoUpdate) + { + throw new Exception($"The column '{column.ColumnName}' can't be update."); + } + + column.InvokeValidator(value); + + realUpdate.Add(column.ColumnName, value); + + if (column.ColumnName == KeyColumn.ColumnName) + { + newKey = value; + } + } + } + + var (sql, parameters) = GenerateUpdateSql(where, realUpdate); + return dbConnection.Execute(sql, ConvertParameters(parameters)); + } + + public virtual int Delete(IDbConnection dbConnection, IWhereClause? where) + { + var (sql, parameters) = GenerateDeleteSql(where); + return dbConnection.Execute(sql, ConvertParameters(parameters)); + } +} + +public interface ITableInfoFactory +{ + TableInfo Get(Type type); +} + +public class TableInfoFactory : ITableInfoFactory +{ + private readonly Dictionary<Type, TableInfo> _cache = new Dictionary<Type, TableInfo>(); + private readonly IColumnTypeProvider _columnTypeProvider; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger<TableInfoFactory> _logger; + + public TableInfoFactory(IColumnTypeProvider columnTypeProvider, ILoggerFactory loggerFactory) + { + _columnTypeProvider = columnTypeProvider; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger<TableInfoFactory>(); + } + + // This is thread-safe. + public TableInfo Get(Type type) + { + lock (_cache) + { + if (_cache.TryGetValue(type, out var tableInfo)) + { + _logger.LogDebug("Table info of type '{}' is cached, return it.", type.Name); + return tableInfo; + } + else + { + _logger.LogDebug("Table info for type '{}' is not in cache, create it.", type.Name); + tableInfo = new TableInfo(type, _columnTypeProvider, _loggerFactory); + _logger.LogDebug("Table info for type '{}' is created, add it to cache.", type.Name); + _cache.Add(type, tableInfo); + return tableInfo; + } + } + } +} |