CYQ.Data 支援 PostgreSQL 資料庫
前言:
很久之前,就有同學問我CYQ.Data能不能支援下PostgreSQL,之後小做了下調查,發現這個資料庫用的人少,加上各種因素,就一直沒動手。
前兩天,不小心看了一下Github上的訊息:
看到這個問題又被重新提了出來了,於是,鬧吧!
下面分享一下支援該資料庫要處理的過程,讓大夥明白CYQ.Data要支援一種新的資料庫,需要花多少功夫。
1、找到資料庫的驅動程式:Npgsql.dll
網上查找了點相關知識,發現.NET 裡操作PostgreSQL有兩種提供的dll,一種是正規的收費的,另一種是開源的Npgsql.dll,因此這裡選擇了開源的。
在Nuget上可以搜尋Npgsql,不過上面的版本要求依賴的版本很高,於是我找了最早的版本開始支援,畢竟CYQ.Data 是從支援最低2.0及以上的。
這裡是找到的下載低版本支援的網址:http://pgfoundry.org/frs/?group_id=1000140&release_id=1889
同時,下載的兩個2.0和4.0兩個版本,也一併上傳到:https://github.com/cyq1162/cyqdata/tree/master/文件
2、建立PostgreDal.cs,實現動態載入DLL
新增動態載入的程式碼:
using System; using System.Collections.Generic; using System.Text; using System.Reflection; using System.Data.Common;using CYQ.Data.Cache; using System.IO; namespace CYQ.Data { internal class PostgreDal : DbBase { public PostgreDal(ConnObject co) : base(co) { } internal static Assembly GetAssembly() { object ass = CacheManage.LocalInstance.Get("Postgre_Assembly"); if (ass == null) { try { string name = string.Empty; if (File.Exists(AppConst.RunFolderPath + "Npgsql.dll")) { name = "Npgsql"; } else { name = "Can't find the Npgsql.dll"; Error.Throw(name); } ass = Assembly.Load(name); CacheManage.LocalInstance.Set("Postgre_Assembly", ass, 10080); } catch (Exception err) { string errMsg = err.Message; Error.Throw(errMsg); } } return ass as Assembly; } protected override DbProviderFactory GetFactory(string providerName) { object factory = CacheManage.LocalInstance.Get("Postgre_Factory"); if (factory == null) { Assembly ass = GetAssembly(); factory = ass.GetType("Npgsql.NpgsqlFactory").GetField("Instance").GetValue(null); // factory = ass.CreateInstance("Npgsql.NpgsqlFactory.Instance"); if (factory == null) { throw new System.Exception("Can't Create NpgsqlFactory in Npgsql.dll"); } else { CacheManage.LocalInstance.Set("Postgre_Factory", factory, 10080); } } return factory as DbProviderFactory; } protected override bool IsExistsDbName(string dbName) { try { IsAllowRecordSql = false; bool result = ExeScalar("select 1 from pg_catalog.pg_database where datname='" + dbName + "'", false) != null; IsAllowRecordSql = true; return result; } catch { return true; } } public override char Pre { get { return ':'; } } public override void AddReturnPara() { } } }
幾點說明:
1、GetFactory方法,其它dll框架提供的都是直接例項化,而Npgsql.dll提供卻是單例屬性,所以程式碼有點變化。 2、Npgsql操作引數化的符號是“:”號。
3、DalCreate.cs追加PostgreSql型別及資料庫連結解析
這裡重點發現postgresql和mssql兩者的資料庫連結格式都一致:
server=...;uid=xxx;pwd=xxx;database=xxx;
因此從單純的語句上,根本無法判斷從屬於哪種資料庫。
經過小小的思考,解決方案出來了:
else { //postgre和mssql的連結語句一樣,這裡用database=和uid=順序來決定;database寫在後面的,為postgre int dbIndex = connString.IndexOf("database=", StringComparison.OrdinalIgnoreCase); int uid = connString.IndexOf("uid=", StringComparison.OrdinalIgnoreCase); if (uid > 0 && uid < dbIndex && File.Exists(AppConfig.RunPath + "Npgsql.dll")) { return PostgreClient; } return SqlClient; }
簡的說:只有滿足引用了npgsql.dll以及database寫在uid之後兩種條件下,判斷為postgresql,其它的都回歸到mssql。
4、處理表結構語句:獲取資料庫表以及表的結構語句:
這一塊花的時間比較多,網上也費了點時間查了不少資料,最後自己寫了語句:
獲取資料庫所有表:
internal static string GetPostgreTables(string dbName) { return string.Format("select table_name as TableName,cast(obj_description(relfilenode,'pg_class') as varchar) as Description from information_schema.tables t left join pg_class p on t.table_name=p.relname where table_schema='public' and table_catalog='{0}'", dbName); }
獲取某表的結構:
internal static string GetPostgreColumns() { return @"select a.attname AS ColumnName, case t.typname when 'int4' then 'int' when 'int8' then 'bigint' else t.typname end AS SqlType, coalesce(character_maximum_length,numeric_precision,-1) as MaxSize,numeric_scale as Scale, case a.attnotnull when 'true' then 0 else 1 end AS IsNullable, case when position('nextval' in column_default)>0 then 1 else 0 end as IsAutoIncrement, case when o.conname is null then 0 else 1 end as IsPrimaryKey, d.description AS Description, i.column_default as DefaultValue from pg_class c left join pg_attribute a on c.oid=a.attrelid left join pg_description d on a.attrelid=d.objoid AND a.attnum = d.objsubid left join pg_type t on a.atttypid = t.oid left join information_schema.columns i on i.table_schema='public' and i.table_name=c.relname and i.column_name=a.attname left join pg_constraint o on a.attnum = o.conkey[1] and o.contype='p' where c.relname =:TableName and a.attnum > 0 and a.atttypid>0 ORDER BY a.attnum"; }
5、處理關鍵字元號
由於PostgreSQL的大小寫敏感,而且關鍵字加需要用雙引號包含(這點和SQLite一致):
這裡在原有的基礎上加上case即可。
6、處理差異化的SQL語句:SqlCreate.cs
A、獲取插入後的自增值,這裡可以借用一下自增列產生的預設值:
這裡用預設值,替換一下nextval序列為currval序列即可。
else if (_action.dalHelper.dalType == DalType.PostgreSQL) { string key = Convert.ToString(primaryCell.Struct.DefaultValue); if (!string.IsNullOrEmpty(key)) { key = key.Replace("nextval", "currval"); sql = sql + "; select " + key + " as OutPutValue"; } }
B、需要引用關鍵字的地方:
略。。。。
7、處理分頁語句:SqlCreateForPager.cs
這裡PostgreSQL和分頁和sqlite及mysql是一致的,因此只要在相關的地方補上case即可:
public static string GetSql(DalType dalType, string version, int pageIndex, int pageSize, object objWhere, string tableName, int rowCount, string columns, string primaryKey, bool primaryKeyIsIdentity) { if (string.IsNullOrEmpty(columns)) { columns = "*"; } pageIndex = pageIndex == 0 ? 1 : pageIndex; string where = SqlFormat.GetIFieldSql(objWhere); if (string.IsNullOrEmpty(where)) { where = "1=1"; } if (pageSize == 0) { return string.Format(top1Pager, columns, tableName, where); } if (rowCount > 0)//分頁查詢。 { where = SqlCreate.AddOrderBy(where, primaryKey); } int topN = pageIndex * pageSize;//Top N 最大數 int max = (pageIndex - 1) * pageSize; int rowStart = (pageIndex - 1) * pageSize + 1; int rowEnd = rowStart + pageSize - 1; string orderBy = string.Empty; if (pageIndex == 1 && dalType != DalType.Oracle)//第一頁(oracle時 rownum 在排序條件為非數字時,和row_number()的不一樣,會導致結果差異,所以分頁統一用row_number()。) { switch (dalType) { case DalType.Access: case DalType.MsSql: case DalType.Sybase: return string.Format(top1Pager, "top " + pageSize + " " + columns, tableName, where); //case DalType.Oracle: // return string.Format(top1Pager, columns, tableName, "rownum<=" + pageSize + " and " + where); case DalType.SQLite: case DalType.MySql: case DalType.PostgreSQL: return string.Format(top1Pager, columns, tableName, where + " limit " + pageSize); } } else { switch (dalType) { case DalType.Access: case DalType.MsSql: case DalType.Sybase: int leftNum = rowCount % pageSize; int pageCount = leftNum == 0 ? rowCount / pageSize : rowCount / pageSize + 1;//頁數 if (pageIndex == pageCount && dalType != DalType.Sybase) // 最後一頁Sybase 不支援雙Top order by { return string.Format(top2Pager, pageSize+" "+columns, "top " + (leftNum == 0 ? pageSize : leftNum) + " * ", tableName, ReverseOrderBy(where, primaryKey), GetOrderBy(where, false, primaryKey));//反序 } if ((pageCount > 1000 || rowCount > 100000) && pageIndex > pageCount / 2) // 頁數過後半段,反轉查詢 { orderBy = GetOrderBy(where, false, primaryKey); where = ReverseOrderBy(where, primaryKey);//事先反轉一次。 topN = rowCount - max;//取後面的 int rowStartTemp = rowCount - rowEnd; rowEnd = rowCount - rowStart; rowStart = rowStartTemp; } break; } } switch (dalType) { case DalType.MsSql: case DalType.Oracle: if (version.StartsWith("08")) { goto temtable; // goto top3;//sql 2000 } int index = tableName.LastIndexOf(')'); if (index > 0) { tableName = tableName.Substring(0, index + 1); } string v = dalType == DalType.Oracle ? "" : " v"; string onlyWhere = "where " + SqlCreate.RemoveOrderBy(where); onlyWhere = SqlFormat.RemoveWhereOneEqualsOne(onlyWhere); return string.Format(rowNumberPager, GetOrderBy(where, false, primaryKey), (columns == "*" ? "t.*" : columns), tableName, onlyWhere, v, rowStart, rowEnd); case DalType.Sybase: temtable: if (primaryKeyIsIdentity) { bool isOk = columns == "*"; if (!isOk) { string kv = SqlFormat.NotKeyword(primaryKey); string[] items = columns.Split(','); foreach (string item in items) { if (string.Compare(SqlFormat.NotKeyword(item), kv, StringComparison.OrdinalIgnoreCase) == 0) { isOk = true; break; } } } else { columns = "t.*"; index = tableName.LastIndexOf(')'); if (index > 0) { tableName = tableName.Substring(0, index + 1); } tableName += " t "; } if (isOk) { return string.Format(tempTablePagerWithIdentity, DateTime.Now.Millisecond, topN, primaryKey, tableName, where, pageSize, columns, rowStart, rowEnd, orderBy); } } return string.Format(tempTablePager, DateTime.Now.Millisecond, pageIndex * pageSize + " " + columns, tableName, where, pageSize, rowStart, rowEnd, orderBy); case DalType.Access: top3: if (!string.IsNullOrEmpty(orderBy)) // 反轉查詢 { return string.Format(top4Pager,columns, (rowCount - max > pageSize ? pageSize : rowCount - max), topN, tableName, where, GetOrderBy(where, true, primaryKey), GetOrderBy(where, false, primaryKey), orderBy); } return string.Format(top3Pager, (rowCount - max > pageSize ? pageSize : rowCount - max),columns, topN, tableName, where, GetOrderBy(where, true, primaryKey), GetOrderBy(where, false, primaryKey)); case DalType.SQLite: case DalType.MySql: case DalType.PostgreSQL: if (max > 500000 && primaryKeyIsIdentity && Convert.ToString(objWhere) == "" && !tableName.Contains(" "))//單表大數量時的優化成主鍵訪問。 { where = string.Format("{0}>=(select {0} from {1} limit {2}, 1) limit {3}", primaryKey, tableName, max, pageSize); return string.Format(top1Pager, columns, tableName, where); } return string.Format(top1Pager, columns, tableName, where + " limit " + pageSize + " offset " + max); } return (string)Error.Throw("Pager::No Be Support:" + dalType.ToString()); }
總結:
一個數據庫的基本支援、寫到這裡就完成了增刪改查及分頁。
當然,對於CYQ.Data而言,還差一些未處理:
1、多種資料庫轉換互通處理:DataType.cs。
2、對錶的建立修改操作:SqlCreateForSchema.cs。
3、支援多資料庫相容性寫法:SqlCompatible.cs。
4、其它細節。