65.9K
CodeProject 正在变化。 阅读更多。
Home

C#.NET 中的数据字典

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (35投票s)

2016年9月23日

CPOL

17分钟阅读

viewsIcon

101207

downloadIcon

2373

为数据库表创建数据字典

引言

数据库是任何公司/项目的组成部分。它们规模从小到大不等。但大多数时候,如果你要求一份数据字典,那么“砰”——没有人有数据字典。这个工具将使你能够为你的表创建数据字典,并将它们导出到Excel文件。作为一项附加功能,该工具还将让你了解主键和外键与数据库其他表的关系。

背景

我在几个地方工作过,都发现了这个重要功能的缺失。他们有数据库,但没有数据字典。时不时地,人们意识到这个需求,然后只是在数据库上沉思,以鸟瞰数据库和表,通过必要的修改让业务得以进行,然后就忘了它。此外,我感觉需要知道哪些其他表将一个表的某个主键用作外键。也就是说,如果“EmployeeID”是某个表的主键,我需要知道哪些其他表将它用作外键。同样,我感觉需要知道将一个表的某个外键用作主键的表。也就是说,如果“ItemID”在一个表中是外键,那么我需要知道哪个表将此键作为主键。考虑到所有这些需求,我将它们都整合到了这个工具中。

接口

界面很简单。先选择数据库,然后选择表。之后,编辑描述,最后导出到Excel文件。界面如下所示

这是“aspnetdb”数据库和“aspnet_Membership”表的示例。只有描述示例备注。在所有列成功加载后,编辑功能的启用/禁用可以在下面的部分进行。

foreach (DataGridViewColumn Col in ColumnsGridView.Columns) // Only the three columns 
                                                            // are editable, 
                                                            // others are read-only.
    Col.ReadOnly = !(Col.Name.Equals("Description") || Col.Name.Equals("Example") || 
                                                       Col.Name.Equals("Notes"));        

下面的两个datagrid显示了主键和外键与其他表的关系;这些是只读网格。

在表组合框下方,有两个单选按钮用于指示用户的示例选择。如果用户选择从config文件加载示例,则这样做。另一方面,如果用户选择从数据库填充示例,则从数据库加载示例。请注意,在Excel输出中包含实时数据可能会导致机密性问题,尤其是在数据包含机密信息的情况下。典型的例子是HIV患者数据,这是高度机密的。此类表的示例数据应从配置文件中填充,或者如果需要反映真实示例,则必须注意隐藏任何类型的身份信息。

屏幕的末尾,显示了当前表记录数、每条记录平均占用的磁盘空间以及整个表占用的总磁盘空间。

成功修改后,可以通过点击按钮将字典导出到Excel文件。这还将导出键关系以及每条记录占用的磁盘空间的粗略估算。

Using the Code

第一部分:项目类简介

PrimaryKeyClass.cs

此类用于保存表的某个主键的信息。此类内部还有一个类,用于实际保存表中每个主键的信息。此类定义如下。PrimaryKeyName 是表中作为主键的列名。ForeignKeyTable 是此列用作外键的表名。NameInForeignKeyTable 是外键表中对应的列名(列名可能不同)。

class PKKeyCriteria
{
    public string PrimaryKeyName { get; set; }    // The primary key column name of a table.
    public string ForeignKeyTable { get; set; }   // The table where this primary key 
                                                  // is used as a foreign key.
    public string NameInForeignKeyTable { get; set; }   // The column name that the 
                                                  // foreign table uses for this primary key.
}

PrimaryKeyClass.cs 本身有一个列表对象(PKList),其中逐渐累积了所有主键。

public List<PKKeyCriteria> PKList = new List<PKKeyCriteria>();

用于累积主键列表的实际方法是该类的GetPrimaryKeysAndDependencies()方法。

public void GetPrimaryKeysAndDependencies()
{
    SqlDataAdapter DAdapter = new SqlDataAdapter();
    DataTable DTable = new DataTable();
    using (var Cmd = new SqlCommand("sp_fkeys", Conn))
    {
        Cmd.CommandType = CommandType.StoredProcedure;
        Cmd.Parameters.AddWithValue("@pktable_name", TableName);
        DAdapter.SelectCommand = Cmd;
        DAdapter.Fill(DTable);
    }
...............................
...............................

在此方法中,我们使用SP_FKEYS,这是SQL Server的一个内置存储过程。我们将表名作为参数传递给SP。SP为该表返回以下列。

我们只对PKCOLUMN_NAMEFKTABLE_NAMEFKCOLUMN_NAME感兴趣。因此,我们将它们列在PKList中。

foreach (DataRow DR in DTable.Rows)
{
    PrimaryKeyClass KeyItem = new PrimaryKeyClass();
    KeyItem.PrimaryKeyName = DR["PKCOLUMN_NAME"].ToString();
    KeyItem.ForeignKeyTable = DR["FKTABLE_NAME"].ToString();
    KeyItem.NameInForeignKeyTable = DR["FKCOLUMN_NAME"].ToString();
    PKList.Add(KeyItem);
}

然而,在非常罕见的情况下,可能无法从上述方法中获取主键。在这种情况下,我们使用数据适配器的FillSchema() 方法并检查PrimaryKey 列。但是,在这种情况下,我们会错过该主键被用作外键的表。

// Sometimes, dependencies might not be obtained for primary keys from the above method. 
// Following is a workaround. However, we don't have
// any information about the foreign table and column name in such cases.
if (PKList.Count == 0)
    using (var cmd = new SqlCommand("SELECT * FROM [" + TablesCmb.Text + "]", Conn))
    {
        DAdapter = new SqlDataAdapter();
        DAdapter.SelectCommand = cmd;
        DAdapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;    // Optional, 
                                                                 // but as a safety measure.

        var dtab = new DataTable();
        DAdapter.FillSchema(dtab, SchemaType.Source);            // Only the schema.

        foreach (DataColumn col in dtab.PrimaryKey)
        {
            PrimaryKeyClass KeyItem = new PrimaryKeyClass();
            KeyItem.PrimaryKeyName = col.ColumnName;
            PKList.Add(KeyItem);
        }
    }

ForeignKeyClass.cs

此类用于保存表的某个外键的信息。此类内部还有一个类,用于实际保存表中每个外键的信息。此类定义如下。ForeignKeyName 是表中作为外键的列名。PrimaryKeyTable 是此列为主键的表名。NameInPrimaryKeyTable 是主表中的对应列名(列名可能不同)。

class FKKeyCriteria
{
    public string ForeignKeyName { get; set; }        // The foreign key column name 
                                                      // of a table.
    public string PrimaryKeyTable { get; set; }       // The table where this foreign key 
                                                      // is used as a primary key.
    public string NameInPrimaryKeyTable { get; set; } // The name that the table uses 
                                                      // this column as a primary key.
}

ForeignKeyClass.cs 本身有一个列表对象(FKList),其中逐渐累积了所有外键。

public List<FKKeyCriteria> FKList = new List<FKKeyCriteria>();

用于累积外键列表的实际方法是该类的GetForeignKeysAndDependencies()方法。此方法实际上执行了以下脚本,该脚本改编自此处

SqlCommand Cmd = new SqlCommand();

//Code adapted from http://stackoverflow.com/questions/483193/
//how-can-i-list-all-foreign-keys-referencing-a-given-table-in-sql-server
Cmd.CommandText = "SELECT PKTABLE_QUALIFIER = CONVERT(SYSNAME, DB_NAME()), " +
                    "PKTABLE_OWNER = CONVERT(SYSNAME, SCHEMA_NAME(O1.SCHEMA_ID)), " +
                    "PKTABLE_NAME = CONVERT(SYSNAME, O1.NAME), " +
                    "PKCOLUMN_NAME = CONVERT(SYSNAME, C1.NAME), " +
                    "FKTABLE_QUALIFIER = CONVERT(SYSNAME, DB_NAME()), " +
                    "FKTABLE_OWNER = CONVERT(SYSNAME, SCHEMA_NAME(O2.SCHEMA_ID)), " +
                    "FKTABLE_NAME = CONVERT(SYSNAME, O2.NAME), " +
                    "FKCOLUMN_NAME = CONVERT(SYSNAME, C2.NAME), " +
                    "UPDATE_RULE = CONVERT(SMALLINT, CASE OBJECTPROPERTY_
                                   (F.OBJECT_ID, 'CnstIsUpdateCascade') " +
                            "WHEN 1 THEN 0 " +
                            "ELSE 1 " +
                            "END), " +
                    "DELETE_RULE = CONVERT(SMALLINT, CASE OBJECTPROPERTY_
                                   (F.OBJECT_ID, 'CnstIsDeleteCascade') " +
                            "WHEN 1 THEN 0 " +
                            "ELSE 1 " +
                            "END), " +
                    "FK_NAME = CONVERT(SYSNAME, OBJECT_NAME(F.OBJECT_ID)), " +
                    "PK_NAME = CONVERT(SYSNAME, I.NAME), " +
                    "DEFERRABILITY = CONVERT(SMALLINT, 7)" +
                    "FROM   SYS.ALL_OBJECTS O1, " +
                    "SYS.ALL_OBJECTS O2, " +
                    "SYS.ALL_COLUMNS C1, " +
                    "SYS.ALL_COLUMNS C2, " +
                    "SYS.FOREIGN_KEYS F " +
                    "INNER JOIN SYS.FOREIGN_KEY_COLUMNS K " +
                    "ON(K.CONSTRAINT_OBJECT_ID = F.OBJECT_ID) " +
                    "INNER JOIN SYS.INDEXES I " +
                    "ON(F.REFERENCED_OBJECT_ID = I.OBJECT_ID " +
                    "AND F.KEY_INDEX_ID = I.INDEX_ID) " +
                    "WHERE O1.OBJECT_ID = F.REFERENCED_OBJECT_ID " +
                    "AND O2.OBJECT_ID = F.PARENT_OBJECT_ID " +
                    "AND C1.OBJECT_ID = F.REFERENCED_OBJECT_ID " +
                    "AND C2.OBJECT_ID = F.PARENT_OBJECT_ID " +
                    "AND C1.COLUMN_ID = K.REFERENCED_COLUMN_ID " +
                    "AND C2.COLUMN_ID = K.PARENT_COLUMN_ID " +
                    "AND CONVERT(SYSNAME, O2.NAME) = '" + TablesCmb.Text + "'";

Cmd.Connection = Conn;
SqlDataAdapter DAdapter = new SqlDataAdapter();
DataTable DTable = new DataTable();
DAdapter.SelectCommand = Cmd;
DAdapter.Fill(DTable);

基本上,它返回与主键相同的表结构,但这次,脚本侧重于外键。

DataDetailsClass.cs

此类用于保存数据库列详细信息的信息。此类内部还有一个类,用于实际保存表中每个单独列的信息。此类定义如下。成员的用途可以通过其名称自明。

class DataDetailsKeyCriteriaClass
{
    public string ColumnName { get; set; }
    public long ColumnSize { get; set; }
    public string DataType { get; set; }
    public string SqlDataType { get; set; }
    public string Mandatory { get; set; }
    public string PrimaryKey { get; set; }
    public string UniqueKey { get; set; }
    public string ForeignKey { get; set; }
    public string Description { get; set; }
    public string Example { get; set; }
    public string RangeFrom { get; set; }
    public string RangeTo { get; set; }
    public string Notes { get; set; }
    public string Computed { get; set; }
}

最后一个成员“Computed”是此版本新添加的,用于表示该列是否为计算列。

DataDetailsClass.cs 本身有一个列表对象(ColumnList),其中逐渐累积了上述类的每个对象。

public List<DataDetailsKeyCriteriaClass> ColumnList = new List<DataDetailsKeyCriteriaClass>();

以下是用于累积列列表的实际方法。在操作上,我们的主窗体将调用此方法,并传递表的が主键、外键、唯一键。它还传递一个out变量用于计算每条记录占用的空间的大致估算;该方法在此变量中返回总大小。此方法基本上读取表的架构信息;它使用CommandBehavior.SchemaOnlyGetSchemaTable()方法(如下所示)。

public void GetColumnDetails(List<PKKeyCriteria> PKList, 
       List<FKKeyCriteria> FKList, List<string> UniqueIndexList, 
       out string DiskSpace, EXAMPLE_CHOICE ExampleChoiceParam)
{
    DiskSpace = "";
    List<string> ComputedColumns = new List<string>();
    GetComputedColumns(ref ComputedColumns);
    using (var cmd = new SqlCommand("SELECT * FROM [" + TableName + "]", Conn))
    using (var DReader = cmd.ExecuteReader(CommandBehavior.SchemaOnly))
    {
        DataTable table = DReader.GetSchemaTable();   // Get the column meta-data only.

        int Index = 0;
        foreach (DataRow row in table.Rows)
        {
            DataDetailsKeyCriteriaClass Details = new DataDetailsKeyCriteriaClass();
            Details.ColumnName = row["ColumnName"].ToString();

            if (ComputedColumns.FindIndex(p => p == Details.ColumnName) == -1) // If the 
                                              // current column is not a computed column.
            {
                Details.ColumnSize = Convert.ToInt64(row["ColumnSize"]);
                Details.DataType = ((Type)row["DataType"]).Name;
                Details.SqlDataType = row["DataTypeName"].ToString();

                Details.Description = GetKeyValueFromAppConfig
                         (Details.SqlDataType + "_Description", Details.ColumnSize);
                Details.Example = ExampleChoiceParam == EXAMPLE_CHOICE.HardCoded ? 
                         GetKeyValueFromAppConfig(Details.SqlDataType + "_Example") : 
                         Details.Example = GetLiveExample(Details.ColumnName);

                Details.RangeFrom = GetKeyValueFromAppConfig
                                    (Details.SqlDataType + "_RangeFrom");
                Details.RangeTo = GetKeyValueFromAppConfig
                                  (Details.SqlDataType + "_RangeTo");

                Details.Mandatory = 
                        row["AllowDBNull"].ToString().Equals("True") ? "No" : "Yes";

                Index = PKList.FindIndex
                        (Item => Item.PrimaryKeyName.Equals(Details.ColumnName));
                Details.PrimaryKey = (Index != -1) ? "Yes" : "No";

                Index = UniqueIndexList.FindIndex(Item => Item.Equals(Details.ColumnName));
                Details.UniqueKey = (Index != -1) ? "Yes" : "No";

                Index = FKList.FindIndex
                        (Item => Item.ForeignKeyName.Equals(Details.ColumnName));
                Details.ForeignKey = (Index != -1) ? "Yes" : "No";

                Details.Computed = "No";
            }
            else
            {
                Details.Computed = "Yes";
                if (ThisIsAPersistedComputedColumn(Details.ColumnName)) // If only 
                // the computed column is a persisted one, then it would occupy disk space.
                    Details.ColumnSize = Convert.ToInt64(row["ColumnSize"]);
            }
            ColumnList.Add(Details);
        }
    }

    DiskSpace = RetrieveDiskSpaceDetails();
}

总之,我们试图在列列表(“ColumnList”)中列出所有列。我们仅使用CommandBehavior.SchemaOnly获取架构。同时,我们获取当前表中可能存在的计算列列表(在列表“ComputedColumns”中)。这是在以下方法中完成的。工作部分改编自此处

private void GetComputedColumns(ref List<string> ComputedColumns)
{
    using (SqlCommand cmd = new SqlCommand("SELECT * FROM sys.columns 
           WHERE is_computed = 1 AND object_id = OBJECT_ID('" + TableName + "')", Conn))
    using (SqlDataReader DReader = cmd.ExecuteReader())
    {
        while (DReader.Read())
            ComputedColumns.Add(DReader["name"].ToString());
    }
}

在此之后,我们进入列填充操作——如果当前列不是计算列,则加载当前列的所有详细信息。

计算列的大小取决于它是否已持久化。“Persisted”顾名思义需要物理存储。因此,如果这是一个持久化的计算列,那么SchemaOnly 读取器返回的列大小将被放置在那里,否则为0

是否是持久化的计算列在以下方法中进行检查。工作代码改编自此处

private bool ThisIsAPersistedComputedColumn(string ColumnName)
{
    using (SqlConnection Conn2 = new SqlConnection(Conn.ConnectionString))
    {
        Conn2.Open();
        using (SqlCommand cmd = new SqlCommand
        ("SELECT t.name as TableName, c.name as ColumnName FROM sys.tables t 
        INNER JOIN sys.computed_columns c on c.object_id = t.object_id " +
                   "WHERE t.name = '" + TableName + "' AND c.is_persisted = 1", Conn2))
        using (SqlDataReader DReader = cmd.ExecuteReader())
        {
            while (DReader.Read())
                if (DReader["ColumnName"].ToString().Equals(ColumnName)) 
                   return true;   // The computed column is listed as a persisted column.
        }
    }
    return false;
}

请注意,布尔类型“AllowDBNull”已更改为“Yes/No”。主键、唯一键和外键列也根据它们在之前获取并作为参数传递给此方法的相关列表中的可用性更改为“Yes/No”。另请注意总大小的增加,这是一个非常简单的计算。但是,作为免责声明,这是一个非常粗略的估算,一条记录的实际大小很可能因记录的内容而异。

DescriptionExampleRangeFromRangeTo成员由从App.config获取的固定值填充。实际上,描述应该由用户编辑,以阐述列的用途。然而,这也可以在Notes列中进行阐述。用户可以自行决定在哪里阐述。

示例的加载方式有两种——根据用户的选择,用户是希望从配置文件加载,还是希望从真实数据加载。这正在这一行进行检查

Details.Example = ExampleChoiceParam == EXAMPLE_CHOICE.HardCoded ? 
                  GetKeyValueFromAppConfig(Details.SqlDataType + "_Example") : 
                  Details.Example = GetLiveExample(Details.ColumnName);

从配置文件加载示例

配置文件中有代表SQL Server数据类型描述、示例和范围的值。这些值是根据可用的sql数据类型命名的。例如,对于“varchar”和“real”类型,我们在App.config中有以下条目。我从此处列出了类型和描述。如果您发现缺少类型和相应的示例、描述和范围,您可以根据需要将其添加到App.config中。这可能对SQL Server的未来版本是必需的。

<add key="varchar_Example" value="Hello"/>
<add key="varchar_Description" value="Max characters"/>

<add key="real_Example" value="123"/>
<add key="real_RangeFrom" value="-3.40E + 38"/>
<add key="real_RangeTo" value="+3.40E + 38"/>

因此,很容易找到对应的数据类型示例、范围和描述。对于string类型,长度从columnsize中获取并放置在那里(使用下面的第一个重载)。

private string GetKeyValueFromAppConfig(string Key, long ColumnSize)
{
    // If the data type is not found, then it returns null, which ultimately displays blank.
    string Description = ConfigurationManager.AppSettings[Key];

    // If a description is found, and it is a description for string types 
    // (determined by 'Max' at the beginning), then insert the size.
    if (!string.IsNullOrEmpty(Description) && Description.Substring(0, 3).Equals("Max")) 
        Description = Description.Insert(4, ColumnSize.ToString() + " ");
    return Description;
}

对于示例和范围,调用第二个重载。

private string GetKeyValueFromAppConfig(string Key)
{
    return ConfigurationManager.AppSettings[Key]; // If the data type is not found, 
                                // then it returns null, which ultimately displays blank.
}

从实时数据加载示例

如果用户选择从实时数据填充示例,则调用以下方法。它实际上返回给定列的第一个非null值;这就是给定列的真实数据。最多返回50个字符,因为示例可能相当长。一个有趣的点是,我们必须使用第二个连接,因为第一个连接正在被调用方法使用;我们不能使用同一个连接。

private string GetLiveExample(string ColumnName)
{
    using (SqlConnection Conn2 = new SqlConnection(Conn.ConnectionString))
    {
        Conn2.Open();
        using (SqlCommand cmd = new SqlCommand("SELECT TOP 1 " + 
        ColumnName + " FROM [" + TableName + "] WHERE " + ColumnName + 
        " IS NOT NULL", Conn2))
        using (SqlDataReader DReader = cmd.ExecuteReader())
            while (DReader.Read())  // Shortening examples - string examples 
                                    // can be quite large.
                if (DReader[ColumnName].ToString().Length > 50)
                    return DReader[ColumnName].ToString().Substring(0, 50);
                else
                    return DReader[ColumnName].ToString();
    }
    return "";
}

示例列是可编辑的,因此如果用户对填充的示例不满意,可以自行输入合适的示例。

磁盘空间计算

监控数据库服务器计算机上的磁盘空间非常重要,以免其耗尽。虽然这取决于许多因素,包括数据和日志文件的物理状态,但该工具旨在提供对表所需空间的粗略了解。

在填充列之后,使用SQL Server的两个方法来实现此目的。以下代码显示了Microsoft建议的物理统计信息计算方法(sys.dm_db_index_physical_stats)的使用。

private string RetrieveDiskSpaceDetails()
{
    StringBuilder SBuilder = new StringBuilder();
    double NumRows = 0;
    // http://www.sqlserver-dba.com/2013/07/calculate-sql-row-size.html
    using (SqlCommand cmd = new SqlCommand("select * 
           from sys.dm_db_index_physical_stats(DB_ID(N'" + DBName + "'), 
           OBJECT_ID(N'" + TableName + "'), NULL, NULL , 'DETAILED');", Conn))
    //using (SqlCommand cmd = new SqlCommand("dbcc showcontig 
                              ('" + TableName + "') with tableresults", Conn))
    using (SqlDataReader DReader = cmd.ExecuteReader())
        while (DReader.Read())
        {
            NumRows = Convert.ToInt32(DReader["record_count"]);
            long TotalSpace = (long)(NumRows * 
                              Convert.ToDouble(DReader["avg_record_size_in_bytes"]));
            SBuilder.Append("Stats (acc. physical state) from [sys.dm_db_index_physical_stats]
             :\n============================================================\n");
            SBuilder.Append("Total records: " + NumRows + "." +
                "\nAvg record size: " + DReader["avg_record_size_in_bytes"] + " bytes." +
                "\nMin record size: " + DReader["min_record_size_in_bytes"] + " bytes." +
                "\nMax record size: " + DReader["max_record_size_in_bytes"] + " bytes." +
                "\nTotal Disk space occupied: " + TotalSpace + " bytes = " +
                Math.Round((double)TotalSpace / 1024, 2) + " KB = " +
                Math.Round((double)TotalSpace / (1024 * 1024), 4) + " MB.\n\n\n");
            break;
        }
...............................
...............................

上述方法包括许多计算因素,如页和碎片。另一个内置SP(sp_spaceused)用于空间计算,它可以表示表中实际分配的空间。

using (SqlCommand cmd = new SqlCommand("EXEC sp_spaceused N'" + TableName + "'", Conn))
using (SqlDataReader DReader = cmd.ExecuteReader())
    while (DReader.Read())
    {
        double SpaceReserved = Convert.ToInt32(DReader["reserved"].ToString().Substring
                               (0, DReader["reserved"].ToString().Length - 3));  // In KB.
        double DataSpace = Convert.ToInt32(DReader["data"].ToString().Substring
        (0, DReader["data"].ToString().Length - 3)) * 1024;  // In bytes; data is in KB.
        double IndexSpace = Convert.ToInt32(DReader["index_size"].ToString().Substring
        (0, DReader["index_size"].ToString().Length - 3)) * 1024;  // In bytes; data is in KB.

        SBuilder.Append("Stats (acc. reservation) from [sp_spaceused] SP:
                        \n============================================================\n");

        if (NumRows == 0)
            SBuilder.Append("Total allocated space (including index): " + 
                             SpaceReserved + " bytes." +
                "\nAvg record size (including index): 0 bytes." +
                "\nAvg record size (excluding index): 0 bytes.");
        else
            SBuilder.Append("Total allocated space (inc. index): " + SpaceReserved + " KB." +
                "\nAvg record size (inc. index): " + 
                Math.Round((DataSpace + IndexSpace) / NumRows, 2) + " bytes." +
                "\nAvg record size (ex. index): " + Math.Round(DataSpace / NumRows, 2) + 
                " bytes.");
    }

我不是SQL专家,所以我不太确定一个方法是否应该比另一个方法使用,或者在特定环境中应用某个方法是否有任何环境意义。此版本包括两者,以便对空间需求有更好的整体了解。

以下是所有三种方法检索的统计信息的快照(请右键单击,复制并粘贴到mspaint中查看更大的快照)

SortableDataDetailsListClass.cs

数据网格的一个困难是,如果它绑定到列表,则不允许排序(通过单击列标题)。解决方法是使用自定义的可排序列表,该列表实现IComparer 接口,然后使用排序后的列表重新绑定数据网格。我们将列和排序顺序传递给构造函数。

private readonly string _memberName = string.Empty; // the member name to be sorted
private readonly SortOrder _sortOrder = SortOrder.None;

public SortableDataDetailsListClass(string memberName, SortOrder sortingOrder)
{
    _memberName = memberName;
    _sortOrder = sortingOrder;
}

IComparer Compare()方法实际上通过比较列表中的项来执行排序。

public int Compare(DataDetailsKeyCriteriaClass Details1, DataDetailsKeyCriteriaClass Details2)
{
    if (_sortOrder != SortOrder.Ascending)
    {
        var tmp = Details1;
        Details1 = Details2;
        Details2 = tmp;
    }

    switch (_memberName)
    {
        case "ColumnName":
            return Details1.ColumnName.CompareTo(Details2.ColumnName);
        case "ColumnSize":
            return Details1.ColumnSize.CompareTo(Details2.ColumnSize);
        case "DataType":
            return Details1.DataType.CompareTo(Details2.DataType);
....................................
....................................
        case "Description":
            if (Details1.Description == null || Details2.Description == null) return -1;
            return Details1.Description.CompareTo(Details2.Description);
....................................
....................................

请注意,这不是一种高效的方法,因为我们硬编码了列名。此外,我们需要对可能包含NULL值的列进行NULL检查(否则会抛出运行时异常)。当然,我们可以选择一种通用方法。

IComparer.Compare()方法的实际讨论超出了本文的范围和目的。因此,我将不深入探讨。总之,这有助于我们的数据网格对项进行排序。

SortableFKListClass.cs 和 SortablePKListClass.cs

这些类的用途与上述类(SortableDataDetailsListClass.cs)相同。只是它们用于排序我们的主键和外键列表。

ExcelOutputClass.cs

此类有一个名为OutputToExcel()的单一方法,用于将我们的数据网格导出到Excel。我将不详细介绍这部分,因为网上有很多关于如何从数据网格导出数据到Excel的解决方案(请参阅参考文献)。它只是选择一个导出文件并保存。我将重点介绍一些格式化问题。“Yes”单元格在Excel文件中被突出显示为绿色。这是在以下代码中完成的

// storing Each row and column value to excel sheet
for (int i = 0; i < ColumnsGridView.Rows.Count; i++)
    for (int j = 0; j < ColumnsGridView.Columns.Count; j++)
        if (ColumnsGridView.Rows[i].Cells[j].Value != null)
        {
            worksheet.Cells[i + 2, j + 1] = ColumnsGridView.Rows[i].Cells[j].Value.ToString();
            if (ColumnsGridView.Rows[i].Cells[j].
                Value.ToString().Equals("Yes"))    // Back colour of "Yes" cells to green.
                worksheet.Cells[i + 2, j + 1].Interior.Color = 
                                ColorTranslator.ToOle(Color.Green);
        }

标题被格式化为白色字体,灰色背景。这是在以下代码中完成的

// formatting the header.
Microsoft.Office.Interop.Excel.Range formatRange;
formatRange = worksheet.get_Range("A1");
formatRange.EntireRow.Font.Bold = true;
formatRange.EntireRow.Interior.Color = ColorTranslator.ToOle(Color.Blue); // Header backcolour
                                                                          // to blue.
formatRange.EntireRow.Font.Color = ColorTranslator.ToOle(Color.White);    // Header forecolour
                                                                          // to white.

最后,单元格宽度被设置为Autofit,以便所有内容可见。这是在以下代码中完成的

formatRange = worksheet.get_Range("A:" + 
              (char)(ColumnsGridView.Columns.Count - 1 + 'A'));  // The range of columns.
formatRange.Columns.AutoFit();

Excel样本输出看起来像下面的快照

第二部分:加载数据库和表

程序开始时连接到数据库。连接字符串在App.config中指定。您需要将其更改为您的数据库。请注意,我们只指定了服务器名称,没有目录(表)。

<connectionStrings>
    <add name="DBConnectionString" connectionString="Data Source=.;
        Integrated Security=true" providerName="System.Data.SqlClient" />
</connectionStrings>

构造函数中的以下代码列出了服务器的所有数据库。数据库名称来自SYS.DATABASES

Conn = new SqlConnection(ConfigurationManager.ConnectionStrings
                        ["DBConnectionString"].ToString());
Conn.Open();

List<string> DBNames = new List<string>();
using (SqlCommand cmd = new SqlCommand("SELECT name from sys.databases", Conn))
using (IDataReader DR = cmd.ExecuteReader())
{
    while (DR.Read())
        DBNames.Add(DR[0].ToString());
}
DatabasesCmb.DataSource = DBNames.OrderBy
         (P => P.ToString()).ToList();  // Sort the items alphabetically and display.

加载完数据库后,我们将数据库中的所有表加载到显示中。我们使用SqlConnectionStringBuilder 类来修改我们现有的连接字符串,以插入目录(即表名),该目录是从表组合框中选取的。然后,我们仅从连接中获取架构(即表的结构),使用SqlClientMetaDataCollectionNames.Tables。请注意,即使我们选择仅表,这也会检索视图。表架构加载到我们的数据表schema中。

SqlConnectionStringBuilder SBuilder = new SqlConnectionStringBuilder
       (ConfigurationManager.ConnectionStrings["DBConnectionString"].ToString());
SBuilder.InitialCatalog = DatabasesCmb.Text;
Conn = new SqlConnection(SBuilder.ToString());
Conn.Open();

DataTable schema = Conn.GetSchema(SqlClientMetaDataCollectionNames.Tables);
List<string> TableNames = new List<string>();
foreach (DataRow row in schema.Rows)
    if (row[3].Equals("BASE TABLE"))    // Only add tables, not views.
       TableNames.Add(row[2].ToString());
TablesCmb.DataSource = TableNames.OrderBy
       (P => P.ToString()).ToList();  // Sort the items alphabetically and display.

schema 包含以下四个项目。TABLE_CATALOG是表或视图的名称,TABLE_SCHEMAschema名称,TABLE_NAME不言而喻,TABLE_TYPE是目录的类型(表/视图)。

下面显示了架构表的两个示例记录。看,左边是视图,右边是表。由于我们只想列出表,所以在上面的代码中有一个IF条件(if (row[3].Equals("BASE TABLE"))。

此时,我们已完成数据库和表的加载。

第三部分:创建字典

首先,我们列出表的唯一键。这是一项轻松的操作,因为它们可以从架构中轻松获得。为此,我们将索引的检索限制在给定表上,否则它将带出数据库中的所有索引。Restrictions是一个字符串数组,第三个索引是要限制的表名(有关Restrictions的详细信息,请访问MSDN)。这次,我们使用SqlClientMetaDataCollectionNames.IndexColumns从连接对象(应用了Restrictions)检索索引列。

// Specify the restrictions.
string[] restrictions = new string[4];
restrictions[2] = TablesCmb.Text;

DataTable UniqueKeys = 
          Conn.GetSchema(SqlClientMetaDataCollectionNames.IndexColumns, restrictions);

List<string> UniqueIndexList = new List<string>();
foreach (DataRow UniqueKey in UniqueKeys.Rows)
    UniqueIndexList.Add(UniqueKey["column_name"].ToString());

您可以在SQL Server Management Studio (SSMS)中找到表的唯一列。如果您打开唯一索引的属性,您将在那里看到唯一列。例如,请看下面的快照

我们程序检索到的对应唯一列显示如下。可以看到它成功检索了所有唯一列(ApplicationId)。除了在SSMS中获得的内容之外,主键列(UserId)默认是唯一列的一部分。

我们的列表变量是以下四个

List<DataDetailsKeyCriteriaClass> ColumnList;
List<PKKeyCriteria> PKList;
List<FKKeyCriteria> FKList;
List<string> UniqueIndexList = new List<string>();

接下来,我们列出表的が主键和外键。成功列出后,我们列出所有列的详细信息。最后,我们将列表绑定到相应的数据网格。

PrimaryKeyClass PrimaryKeyObj = new PrimaryKeyClass(TablesCmb.Text, Conn);
PrimaryKeyObj.GetPrimaryKeysAndDependencies();
PKList = PrimaryKeyObj.PKList;
PKGridView.DataSource = PKList;

ForeignKeyClass ForeignKeyObj = new ForeignKeyClass(TablesCmb.Text, Conn);
ForeignKeyObj.GetForeignKeysAndDependencies();
FKList = ForeignKeyObj.FKList;
FKGridView.DataSource = FKList;

DataDetailsClass DataDetailsObj = 
                 new DataDetailsClass(DatabasesCmb.Text, TablesCmb.Text, Conn);
if (ViewHardCodedExample.Checked)
    DataDetailsObj.GetColumnDetails(PKList, FKList, UniqueIndexList, 
                   out TotalSize, DataDetailsClass.EXAMPLE_CHOICE.HardCoded);
else if (ViewLiveExample.Checked)
    DataDetailsObj.GetColumnDetails(PKList, FKList, UniqueIndexList, 
                   out TotalSize, DataDetailsClass.EXAMPLE_CHOICE.Live);
ColumnList = DataDetailsObj.ColumnList;
ColumnsGridView.DataSource = ColumnList;

绑定后,“Yes”的单元格将以绿色背景高亮显示。我们在网格的CellFormatting()事件中进行了此操作。

private void ColumnsGridView_CellFormatting
             (object sender, DataGridViewCellFormattingEventArgs e)
{
    // Code adapted from http://stackoverflow.com/
    // questions/16105718/datagridview-changing-cell-background-color
    if (e.Value != null && e.Value.ToString().Equals("Yes"))
        e.CellStyle.BackColor = Color.Green;
}

排序

此功能在第一个版本中缺失;现在已提供。要排序,只需单击数据网格的列标题即可。我们使用ColumnHeaderMouseClick()事件来处理排序,如下所示

private void ColumnsGridView_ColumnHeaderMouseClick
             (object sender, DataGridViewCellMouseEventArgs e)
{
    SorGrid(ColumnsGridView.Columns[e.ColumnIndex], ColumnsGridView);
}

private void SorGrid(DataGridViewColumn Col, DataGridView GridView)
{
    System.Windows.Forms.SortOrder sortOrder = 
           Col.HeaderCell.SortGlyphDirection == System.Windows.Forms.SortOrder.Ascending
                                ? System.Windows.Forms.SortOrder.Descending
                                : System.Windows.Forms.SortOrder.Ascending;

    switch (GridView.Name)
    {
        case "ColumnsGridView":
            ColumnList.Sort(new SortableDataDetailsListClass(Col.Name, sortOrder)); break;
        case "PKGridView":
            PKList.Sort(new SortablePKListClass(Col.Name, sortOrder)); break;
        case "FKGridView":
            FKList.Sort(new SortableFKListClass(Col.Name, sortOrder)); break;
    }

    GridView.Refresh();
    Col.HeaderCell.SortGlyphDirection = sortOrder;  // Display up arrow or down arrow.
} 

它获取排序顺序,更改字形图标(向上箭头/向下箭头),并使用相应的可排序绑定列表重新排序当前列表。最后,它刷新数据网格。

环境

代码是在Visual Studio 2015中设计的,.NET Framework版本为4.5。在SQL Server 2012的数据库上进行了测试。

关注点

如果一个表有一个正在操作的数据适配器打开,那么另一个数据适配器就不能使用同一个连接在同一个表上进行操作。如果使用另一个SQL连接,一切都会正常。

在使用SQL身份验证(UIDpassword)连接SQL Server时,发现了一个有趣的事情。如果连接字符串从配置文件读取,那么在第一次成功连接尝试后,密码将从连接对象中消失。因此,使用同一个连接对象进行任何进一步的操作都会导致连接被拒绝(因为password丢失)。为了解决此类问题,应再次从config文件中读取连接字符串。我不确定这是否总是会发生。

未来工作

这个工具可以被大大使用和扩展,以满足了解数据库的更大需求。一个明显的改进领域是磁盘空间计算。

整合日志文件问题可能是一个很好的改进领域。

另一件事可能是填充一个组合框以加载网络上的所有SQL Server实例(System.Data.Sql.SqlDataSourceEnumerator),并选择所需的实例,而不是每次都更改配置文件。但是,这会产生SQL身份验证问题。

还有机会通过更好的消息和解决方案来更好地处理连接超时错误。

仅限于管理员访问的限制可能会导致不满,并且可以对其进行改进,使其能够使用不具有管理员访问权限的SQL身份验证。但是,我非常怀疑这一点能否成功,因为它需要访问通常只提供给管理员的架构集合。

免责声明

该工具未能从“ReportServerTempDB”表中获取合法密钥。我不确定为什么会发生这种情况。实际上,SP_FKEYS未能列出此数据库中的任何主键,即使存在主键。也许它根本不在乎这个数据库,因为它是一个临时数据库(顾名思义)?我将尝试找出原因并在后续历史中发布。请关注。另外,如果您发现原因,请在评论部分告知大家。任何其他问题、建议、改进都欢迎,请随时评论。

磁盘空间因许多因素而异。前一个版本使用“DBCC ShowContig”进行磁盘空间计算。但根据Microsoft的说法,这将在未来版本中被弃用。因此,此版本使用了建议的方法(sys.dm_db_index_physical_stats)。另一个方法(SP: sp_spaceused)并行使用,以获取有关表中实际分配的磁盘空间的信息。只是为了承认磁盘空间无法非常精确地测量,这只是提供了一个整体的粗略估计。

此项目的另一个限制是它需要管理员访问权限,因为它直接使用“SqlClientMetaDataCollectionNames”通过连接对象访问架构信息。

致谢

摘要

该项目旨在列出SQL Server表中所有列的名称、类型、键类型、键关系(引用)、描述、示例、范围,以及在操作时表占用的总物理空间。

参考文献

历史

  • 2016年9月23日
    • 首次发布
  • 2016年10月25日
    • 代码重构 - 将任务委托给类
    • 增加了点击列标题进行排序的功能(点击网格的标题对项进行排序),响应式设计(以适应不同的分辨率)。
  • 2016年11月23日
    • 重构磁盘空间计算,重构从实时数据加载示例,解决了 Unicode 和计算列的问题。
  • 2016年11月25日
    • SQL 身份验证 bug 修复
    • 界面更改
    • 将已弃用的“DBCC ShowContig”替换为最新的“sys.dm_db_index_physical_stats
    • 界面更改快照
    • 添加了一个新快照
    • 移除了 1 个旧参考
    • 添加了 2 个新参考
    • 部分更改 - 关注点、免责声明、磁盘空间计算、未来工作、致谢和参考文献
© . All rights reserved.