You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
19 KiB
480 lines
19 KiB
#if DEBUG
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Data.Common;
|
|
using System.Text;
|
|
|
|
namespace Apewer.Source
|
|
{
|
|
|
|
/// <summary>数据库客户端基类。</summary>
|
|
|
|
|
|
public abstract class DbClient : IDbClient
|
|
{
|
|
|
|
/// <summary></summary>
|
|
public virtual Logger Logger { get; set; }
|
|
|
|
/// <summary></summary>
|
|
public DbClient(Timeout timeout) { _timeout = timeout ?? Timeout.Default; }
|
|
|
|
#region connection
|
|
|
|
Timeout _timeout = null;
|
|
IDbConnection _conn = null;
|
|
string _str = null;
|
|
|
|
/// <summary></summary>
|
|
public Timeout Timeout { get => _timeout; set => _timeout = value ?? Timeout.Default; }
|
|
|
|
/// <summary>获取当前的 SqlConnection 对象。</summary>
|
|
public IDbConnection Connection { get => _conn; }
|
|
|
|
/// <summary></summary>
|
|
public bool Online { get => _conn == null ? false : (_conn.State == ConnectionState.Open); }
|
|
|
|
/// <summary>连接字符串。</summary>
|
|
public string ConnectionString { get => _str; }
|
|
|
|
/// <summary>连接数据库,若未连接则尝试连接,获取连接成功的状态。</summary>
|
|
public string Connect()
|
|
{
|
|
if (_conn == null)
|
|
{
|
|
_str = NewConnectionString();
|
|
_conn = NewConnection();
|
|
_conn.ConnectionString = _str;
|
|
}
|
|
else
|
|
{
|
|
if (_conn.State == ConnectionState.Open) return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
_conn.Open();
|
|
if (_conn.State == ConnectionState.Open) return null;
|
|
var message = $"连接后状态为 {_conn.State},无法验证打开状态。";
|
|
Logger.Error(this, "Connect", message, _str);
|
|
return "连接失败," + message;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error(this, "Connect", ex.GetType().Name, ex.Message, _str);
|
|
Close();
|
|
return ex.Message;
|
|
}
|
|
}
|
|
|
|
/// <summary>关闭连接,并释放对象所占用的系统资源。</summary>
|
|
public void Close()
|
|
{
|
|
if (_conn != null)
|
|
{
|
|
if (_transaction != null)
|
|
{
|
|
if (_autocommit) Commit();
|
|
else Rollback();
|
|
}
|
|
_conn.Close();
|
|
_conn.Dispose();
|
|
_conn = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>关闭连接,释放对象所占用的系统资源,并清除连接信息。</summary>
|
|
public void Dispose() { Close(); }
|
|
|
|
#endregion
|
|
|
|
#region transaction
|
|
|
|
private IDbTransaction _transaction = null;
|
|
private bool _autocommit = false;
|
|
|
|
/// <summary>
|
|
/// <para>启动事务,可指定事务锁定行为。</para>
|
|
/// <para>Chaos<br />无法覆盖隔离级别更高的事务中的挂起的更改。</para>
|
|
/// <para>ReadCommitted<br />在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。</para>
|
|
/// <para>ReadUncommitted<br />可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。</para>
|
|
/// <para>RepeatableRead<br />在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。 防止不可重复的读取,但是仍可以有幻像行。</para>
|
|
/// <para>Serializable<br />在 System.Data.DataSet 上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。</para>
|
|
/// <para>Snapshot<br />通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相同数据版本来减少阻止。 表示您无法从一个事务中看到在其他事务中进行的更改,即便重新查询也是如此。</para>
|
|
/// <para>Unspecified = -1<br />正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。</para>
|
|
/// </summary>
|
|
/// <param name="commit">在连接的生命周期结束时未结束事务,指定 TRUE 将自动提交,指定 FALSE 将自动回滚。</param>
|
|
/// <param name="isolation">指定事务锁定行为,不指定时将使用默认值。</param>
|
|
public string Begin(bool commit = false, Class<IsolationLevel> isolation = null)
|
|
{
|
|
if (_transaction != null) return "已存在未完成的事务,无法再次启动。";
|
|
var connect = Connect();
|
|
if (connect.NotEmpty()) return $"无法启动事务:连接失败。(${connect})";
|
|
try
|
|
{
|
|
_transaction = isolation ? _conn.BeginTransaction(isolation.Value) : _conn.BeginTransaction();
|
|
_autocommit = commit;
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error(this, "Begin", ex.Message());
|
|
return ex.Message();
|
|
}
|
|
}
|
|
|
|
/// <summary>提交事务。</summary>
|
|
public string Commit()
|
|
{
|
|
if (_transaction == null) return "事务不存在。";
|
|
try
|
|
{
|
|
_transaction.Commit();
|
|
RuntimeUtility.Dispose(_transaction);
|
|
_transaction = null;
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RuntimeUtility.Dispose(_transaction);
|
|
_transaction = null;
|
|
Logger.Error(this, "Commit", ex.Message());
|
|
return ex.Message();
|
|
}
|
|
}
|
|
|
|
/// <summary>从挂起状态回滚事务。</summary>
|
|
public string Rollback()
|
|
{
|
|
if (_transaction == null) return "事务不存在。";
|
|
try
|
|
{
|
|
_transaction.Rollback();
|
|
RuntimeUtility.Dispose(_transaction);
|
|
_transaction = null;
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RuntimeUtility.Dispose(_transaction);
|
|
_transaction = null;
|
|
Logger.Error(this, "Rollback", ex.Message());
|
|
return ex.Message();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ado
|
|
|
|
/// <summary>查询。</summary>
|
|
public IQuery Query(string sql) => Query(sql, null);
|
|
|
|
/// <summary>查询。</summary>
|
|
public IQuery Query(string sql, IEnumerable<IDataParameter> parameters)
|
|
{
|
|
if (TextUtility.IsEmpty(sql)) return new Query(false, "语句无效。");
|
|
|
|
var connected = Connect();
|
|
if (connected.NotEmpty()) return new Query(false, connected);
|
|
|
|
try
|
|
{
|
|
using (var command = NewCommand())
|
|
{
|
|
command.Connection = _conn;
|
|
if (_timeout != null) command.CommandTimeout = Timeout.Query;
|
|
command.CommandText = sql;
|
|
if (parameters != null)
|
|
{
|
|
foreach (var parameter in parameters)
|
|
{
|
|
if (parameter != null) command.Parameters.Add(parameter);
|
|
}
|
|
}
|
|
|
|
var ex = null as Exception;
|
|
var da = null as IDataAdapter;
|
|
try { da = NewDataAdapter(command); }
|
|
catch (Exception adapterEx) { ex = adapterEx; }
|
|
if (ex == null)
|
|
{
|
|
using (var ds = new DataSet())
|
|
{
|
|
da.Fill(ds);
|
|
if (ds.Tables.Count > 0)
|
|
{
|
|
var tables = new DataTable[ds.Tables.Count];
|
|
ds.Tables.CopyTo(tables, 0);
|
|
return new Query(tables, true);
|
|
}
|
|
else
|
|
{
|
|
Logger.Error(this, "Query", "查询结果不包含任何数据表。", sql);
|
|
return new Query(false, "查询结果不包含任何数据表。");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.Error(this, "Query", ex.GetType().FullName, ex.Message, sql);
|
|
return new Query(ex);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Logger.Error(this, "Query", exception.GetType().FullName, exception.Message, sql);
|
|
return new Query(exception);
|
|
}
|
|
}
|
|
|
|
/// <summary>执行。</summary>
|
|
public IExecute Execute(string sql) => Execute(sql, null);
|
|
|
|
/// <summary>执行单条 Transact-SQL 语句,并加入参数。</summary>
|
|
public IExecute Execute(string sql, IEnumerable<IDataParameter> parameters)
|
|
{
|
|
if (TextUtility.IsEmpty(sql)) return new Execute(false, "语句无效。");
|
|
|
|
var connected = Connect();
|
|
if (connected.NotEmpty()) return new Execute(false, connected);
|
|
|
|
var inTransaction = _transaction != null;
|
|
if (!inTransaction) Begin();
|
|
try
|
|
{
|
|
|
|
using (var command = NewCommand())
|
|
{
|
|
command.Connection = _conn;
|
|
command.Transaction = (DbTransaction)_transaction;
|
|
if (Timeout != null) command.CommandTimeout = Timeout.Execute;
|
|
command.CommandText = sql;
|
|
if (parameters != null)
|
|
{
|
|
foreach (var parameter in parameters)
|
|
{
|
|
if (parameter != null) command.Parameters.Add(parameter);
|
|
}
|
|
}
|
|
var rows = command.ExecuteNonQuery();
|
|
if (!inTransaction) Commit(); // todo 此处应该检查事务提交产生的错误。
|
|
return new Execute(true, rows);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Logger.Error(this, "Execute", exception, sql);
|
|
if (!inTransaction) Rollback();
|
|
return new Execute(exception);
|
|
}
|
|
}
|
|
|
|
/// <summary>输出查询结果的首列数据。</summary>
|
|
protected string[] TextColumn(string sql, string[] excluded = null)
|
|
{
|
|
if (Connect().NotEmpty()) return new string[0];
|
|
using (var query = Query(sql))
|
|
{
|
|
var rows = query.Rows;
|
|
var list = new List<string>(rows);
|
|
for (int r = 0; r < query.Rows; r++)
|
|
{
|
|
var cell = query.Text(r, 0);
|
|
if (TextUtility.IsEmpty(cell)) continue;
|
|
if (excluded != null && excluded.Contains(cell)) continue;
|
|
list.Add(cell);
|
|
}
|
|
return list.ToArray();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region orm
|
|
|
|
/// <summary>使用指定语句查询,获取查询结果。</summary>
|
|
/// <param name="sql">要执行的 SQL 语句。</param>
|
|
/// <param name="parameters">为 SQL 语句提供的参数。</param>
|
|
public Result<T[]> Query<T>(string sql, IEnumerable<IDataParameter> parameters = null) where T : class, new() => SourceUtility.As<object, T>(Query(typeof(T), sql, parameters));
|
|
|
|
/// <summary>使用指定语句查询,获取查询结果。</summary>
|
|
/// <param name="model">目标记录的类型。</param>
|
|
/// <param name="sql">要执行的 SQL 语句。</param>
|
|
/// <param name="parameters">为 SQL 语句提供的参数。</param>
|
|
public Result<object[]> Query(Type model, string sql, IEnumerable<IDataParameter> parameters = null)
|
|
{
|
|
if (_conn == null) return new Result<object[]>("连接无效。");
|
|
if (model == null) return new Result<object[]>("数据模型类型无效。");
|
|
if (string.IsNullOrEmpty(sql)) return new Result<object[]>("SQL 语句无效。");
|
|
|
|
using (var query = Query(sql, parameters))
|
|
{
|
|
var result = null as Result<object[]>;
|
|
if (query.Success)
|
|
{
|
|
try
|
|
{
|
|
var array = SourceUtility.Fill(query, model);
|
|
result = new Result<object[]>(array);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result = new Result<object[]>(ex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = new Result<object[]>(query.Message);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region static
|
|
|
|
/// <summary>对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。</summary>
|
|
public static string Escape(string text, int bytes = 0)
|
|
{
|
|
if (text.IsEmpty()) return "";
|
|
|
|
var t = text ?? "";
|
|
t = t.Replace("\\", "\\\\");
|
|
t = t.Replace("'", "\\'");
|
|
t = t.Replace("\n", "\\n");
|
|
t = t.Replace("\r", "\\r");
|
|
t = t.Replace("\b", "\\b");
|
|
t = t.Replace("\t", "\\t");
|
|
t = t.Replace("\f", "\\f");
|
|
|
|
if (bytes > 5)
|
|
{
|
|
if (t.Bytes(Encoding.UTF8).Length > bytes)
|
|
{
|
|
while (true)
|
|
{
|
|
t = t.Substring(0, t.Length - 1);
|
|
if (t.Bytes(Encoding.UTF8).Length <= (bytes - 4)) break;
|
|
}
|
|
t = t + " ...";
|
|
}
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
/// <summary>获取表名。</summary>
|
|
protected static string Table<T>() => Table(typeof(T));
|
|
|
|
/// <summary>获取表名。</summary>
|
|
protected static string Table(Type model)
|
|
{
|
|
if (model == null) throw new Exception($"无法从无效类型获取表名。");
|
|
|
|
var ts = TableStructure.Parse(model);
|
|
if (ts == null) throw new Exception($"无法从类型 {model.FullName} 获取表名。");
|
|
|
|
return ts.Name;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region derived
|
|
|
|
/// <summary>为 Ado 创建连接字符串。</summary>
|
|
protected abstract string NewConnectionString();
|
|
|
|
/// <summary>为 Ado 创建 IDbConnection 对象。</summary>
|
|
protected abstract IDbConnection NewConnection();
|
|
|
|
/// <summary>为 Ado 创建 IDbCommand 对象。</summary>
|
|
protected abstract IDbCommand NewCommand();
|
|
|
|
/// <summary>为 Ado 创建 IDataAdapter 对象。</summary>
|
|
protected abstract IDataAdapter NewDataAdapter(IDbCommand command);
|
|
|
|
// /// <summary>为 Ado 创建 IDataParameter 对象。</summary>
|
|
// protected abstract IDataParameter NewDataParameter();
|
|
|
|
#endregion
|
|
|
|
#region initialization
|
|
|
|
/// <summary>查询数据库中的所有表名。</summary>
|
|
public abstract string[] TableNames();
|
|
|
|
/// <summary>查询数据库实例中的所有数据库名。</summary>
|
|
public abstract string[] StoreNames();
|
|
|
|
/// <summary>查询表中的所有列名。</summary>
|
|
public abstract string[] ColumnNames(string tableName);
|
|
|
|
/// <summary>获取列信息。</summary>
|
|
public abstract ColumnInfo[] ColumnsInfo(string tableName);
|
|
|
|
/// <summary>初始化指定类型,以创建表或增加字段。</summary>
|
|
/// <returns>错误信息。当成功时候返回空字符串。</returns>
|
|
public string Initialize<T>() where T : class, new() => Initialize(typeof(T));
|
|
|
|
#endregion
|
|
|
|
#region IDbClientOrm
|
|
|
|
/// <summary>初始化指定类型,以创建表或增加字段。</summary>
|
|
/// <param name="model">要初始化的类型。</param>
|
|
/// <returns>错误信息。当成功时候返回空字符串。</returns>
|
|
public abstract string Initialize(Type model);
|
|
|
|
/// <summary>插入记录。</summary>
|
|
/// <param name="record">要插入的记录实体。</param>
|
|
/// <param name="table">插入到指定表。当不指定时,由 record 类型决定。</param>
|
|
/// <returns>错误信息。当成功时候返回空字符串。</returns>
|
|
public abstract string Insert(object record, string table = null);
|
|
|
|
/// <summary>更新记录。</summary>
|
|
/// <param name="record">要插入的记录实体。</param>
|
|
/// <param name="table">插入到指定表。当不指定时,由 record 类型决定。</param>
|
|
/// <returns>错误信息。当成功时候返回空字符串。</returns>
|
|
public abstract string Update(IRecord record, string table = null);
|
|
|
|
/// <summary>获取指定类型的主键,按 Flag 属性筛选。</summary>
|
|
/// <param name="model">要查询的类型。</param>
|
|
/// <param name="flag">要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。</param>
|
|
public abstract Result<string[]> Keys(Type model, long flag = 0);
|
|
|
|
/// <summary>获取指定类型的主键,按 Flag 属性筛选。</summary>
|
|
/// <param name="flag">要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。</param>
|
|
public abstract Result<string[]> Keys<T>(long flag = 0) where T : class, IRecord, new();
|
|
|
|
/// <summary>获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。</summary>
|
|
/// <param name="model">目标记录的类型。</param>
|
|
/// <param name="key">目标记录的主键。</param>
|
|
/// <param name="flag">要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。</param>
|
|
public abstract Result<object> Get(Type model, string key, long flag = 0);
|
|
|
|
/// <summary>获取具有指定 Key 的记录,并要求记录具有指定的 Flag 属性。</summary>
|
|
/// <param name="key">目标记录的主键。</param>
|
|
/// <param name="flag">要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。</param>
|
|
public abstract Result<T> Get<T>(string key, long flag = 0) where T : class, IRecord, new();
|
|
|
|
/// <summary>查询所有记录。</summary>
|
|
/// <param name="model">目标记录的类型。</param>
|
|
/// <param name="flag">要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。</param>
|
|
public abstract Result<object[]> Query(Type model, long flag = 0);
|
|
|
|
/// <summary>查询所有记录。</summary>
|
|
/// <param name="flag">要求目标记录具有的 Flag 属性,当指定 0 时忽略此要求。</param>
|
|
public abstract Result<T[]> Query<T>(long flag = 0) where T : class, IRecord, new();
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|