Skip to content

Commit 579dcb0

Browse files
Generate stable sql objects names
Fix #1769
1 parent 341a864 commit 579dcb0

File tree

7 files changed

+176
-29
lines changed

7 files changed

+176
-29
lines changed

src/NHibernate.Test/MappingTest/TableFixture.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Linq;
2-
using System.Threading;
31
using NHibernate.Dialect;
42
using NHibernate.Mapping;
53
using NUnit.Framework;
@@ -61,5 +59,14 @@ public void SchemaNameQuoted()
6159

6260
Assert.AreEqual("[schema].name", tbl.GetQualifiedName(dialect));
6361
}
62+
63+
[Test]
64+
public void NameIsStable()
65+
{
66+
var tbl = new Table { Name = "name" };
67+
Assert.That(
68+
Constraint.GenerateName("FK_", tbl, null, new[] {new Column("col1"), new Column("col2")}),
69+
Is.EqualTo("FK_IHNJ2ONUQL23X2DYGJ2YBBV3F7A"));
70+
}
6471
}
6572
}

src/NHibernate.Test/NHSpecificTest/NH1399/Fixture.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
using System;
12
using NHibernate.Mapping;
23
using NUnit.Framework;
34

45
namespace NHibernate.Test.NHSpecificTest.NH1399
56
{
6-
[TestFixture]
7+
[TestFixture, Obsolete]
78
public class Fixture
89
{
910
[Test]
@@ -42,4 +43,4 @@ public void UsingTwoInstancesWithSameValuesTheFkNameIsTheSame()
4243
Assert.That(t2Fk_, Is.EqualTo(t2Fk));
4344
}
4445
}
45-
}
46+
}

src/NHibernate/Cfg/Configuration.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4-
using System.Configuration;
54
using System.Diagnostics;
65
using System.IO;
76
using System.Linq;
@@ -1191,6 +1190,13 @@ private void SecondPassCompileForeignKeys(Table table, ISet<ForeignKey> done)
11911190
try
11921191
{
11931192
fk.AddReferencedTable(referencedClass);
1193+
1194+
if (string.IsNullOrEmpty(fk.Name))
1195+
{
1196+
fk.Name = Constraint.GenerateName(
1197+
fk.GeneratedConstraintNamePrefix, table, fk.ReferencedTable, fk.Columns);
1198+
}
1199+
11941200
fk.AlignColumns();
11951201
}
11961202
catch (MappingException me)

src/NHibernate/Mapping/Constraint.cs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Security.Cryptography;
45
using System.Text;
56
using NHibernate.Engine;
67
using NHibernate.Util;
@@ -38,6 +39,142 @@ public IEnumerable<Column> ColumnIterator
3839
get { return columns; }
3940
}
4041

42+
/// <summary>
43+
/// Generate a name hopefully unique using the table and column names.
44+
/// Static so the name can be generated prior to creating the Constraint.
45+
/// They're cached, keyed by name, in multiple locations.
46+
/// </summary>
47+
/// <param name="prefix">A name prefix for the generated name.</param>
48+
/// <param name="table">The table for which the name is generated.</param>
49+
/// <param name="referencedTable">The referenced table, if any.</param>
50+
/// <param name="columns">The columns for which the name is generated.</param>
51+
/// <returns>The generated name.</returns>
52+
/// <remarks>Hybrid of Hibernate <c>Constraint.generateName</c> and
53+
/// <c>NamingHelper.generateHashedFkName</c>.</remarks>
54+
public static string GenerateName(
55+
string prefix, Table table, Table referencedTable, IEnumerable<Column> columns)
56+
{
57+
// Use a concatenation that guarantees uniqueness, even if identical names
58+
// exist between all table and column identifiers.
59+
var sb = new StringBuilder("table`" + table.Name + "`");
60+
if (referencedTable != null)
61+
sb.Append("references`" + referencedTable.Name + "`");
62+
63+
// Ensure a consistent ordering of columns, regardless of the order
64+
// they were bound.
65+
// Clone the list, as sometimes a set of order-dependent Column
66+
// bindings are given.
67+
var alphabeticalColumns = new List<Column>(columns);
68+
alphabeticalColumns.Sort(ColumnComparator.Instance);
69+
foreach (var column in alphabeticalColumns)
70+
{
71+
var columnName = column == null ? "" : column.Name;
72+
sb.Append("column`").Append(columnName).Append("`");
73+
}
74+
// Hash the generated name for avoiding collisions with user choosen names.
75+
// This is not 100% reliable, as hashing may still have a chance of generating
76+
// collisions.
77+
var name = prefix + HashName(sb.ToString());
78+
79+
// Hibernate uses an algorithm yielding names shorter than 30 characters. But we cannot
80+
// use it (see HashName). And also we have DB limited to even less (Informix)...
81+
if (name.Length > 30)
82+
{
83+
// This, of course, increases the collision risk.
84+
name = name.Substring(0, 30);
85+
}
86+
87+
return name;
88+
}
89+
90+
#region Name generation support methods
91+
92+
/// <summary>
93+
/// Hash a constraint name. Convert the hash digest to base 32
94+
/// (full alphanumeric) for shortening the hash string representation
95+
/// while keeping it suitable for db names.
96+
/// </summary>
97+
/// <param name="name">The name to be hashed.</param>
98+
/// <returns>The hased name.</returns>
99+
private static string HashName(string name)
100+
{
101+
// Hibernate uses MD5, but with .Net this would throw on FIPS enabled machine.
102+
// As a consequence generated names will be quite longer.
103+
using (var hasher = SHA256.Create())
104+
{
105+
var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(name));
106+
// Converting to base 32 for shortening the name.
107+
// Hibernate uses base 35, but we do not have a native implementation
108+
// in .Net, and base 32 is easier to implement.
109+
return ToBase32String(hash);
110+
}
111+
}
112+
113+
// Adapted from https://stackoverflow.com/a/7135008/1178314
114+
// Changed for not padding with "="
115+
private static string ToBase32String(byte[] input)
116+
{
117+
if (input == null || input.Length == 0)
118+
{
119+
throw new ArgumentNullException(nameof(input));
120+
}
121+
122+
var charCount = (int) Math.Ceiling(input.Length / 5d) * 8;
123+
var result = new StringBuilder(charCount);
124+
125+
byte nextChar = 0, bitsRemaining = 5;
126+
127+
foreach (var b in input)
128+
{
129+
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
130+
result.Append(ValueToChar(nextChar));
131+
132+
if (bitsRemaining < 4)
133+
{
134+
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
135+
result.Append(ValueToChar(nextChar));
136+
bitsRemaining += 5;
137+
}
138+
139+
bitsRemaining -= 3;
140+
nextChar = (byte)((b << bitsRemaining) & 31);
141+
}
142+
143+
// If we didn't end with a full char
144+
if (result.Length != charCount)
145+
{
146+
result.Append(ValueToChar(nextChar));
147+
}
148+
149+
return result.ToString();
150+
}
151+
152+
private static char ValueToChar(byte b)
153+
{
154+
if (b < 26)
155+
{
156+
return (char)(b + 65);
157+
}
158+
159+
if (b < 32)
160+
{
161+
return (char)(b + 24);
162+
}
163+
164+
throw new ArgumentException("Byte is not a value Base32 value.", "b");
165+
}
166+
167+
private class ColumnComparator : IComparer<Column>
168+
{
169+
public static readonly ColumnComparator Instance = new ColumnComparator();
170+
171+
public int Compare(Column col1, Column col2) {
172+
return StringComparer.Ordinal.Compare(col1?.Name, col2?.Name);
173+
}
174+
}
175+
176+
#endregion
177+
41178
/// <summary>
42179
/// Adds the <see cref="Column"/> to the <see cref="ICollection"/> of
43180
/// Columns that are part of the constraint.

src/NHibernate/Mapping/DenormalizedTable.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
2-
using System.Collections;
32
using NHibernate.Util;
43
using System.Collections.Generic;
4+
using System.Linq;
55

66
namespace NHibernate.Mapping
77
{
@@ -56,21 +56,19 @@ public override IEnumerable<Index> IndexIterator
5656
public override void CreateForeignKeys()
5757
{
5858
includedTable.CreateForeignKeys();
59-
IEnumerable includedFks = includedTable.ForeignKeyIterator;
60-
foreach (ForeignKey fk in includedFks)
59+
var includedFks = includedTable.ForeignKeyIterator;
60+
foreach (var fk in includedFks)
6161
{
62-
// NH Different behaviour (fk name)
63-
CreateForeignKey(GetForeignKeyName(fk), fk.Columns, fk.ReferencedEntityName);
62+
CreateForeignKey(
63+
Constraint.GenerateName(
64+
fk.GeneratedConstraintNamePrefix,
65+
this,
66+
null,
67+
fk.Columns),
68+
fk.Columns, fk.ReferencedEntityName);
6469
}
6570
}
6671

67-
private string GetForeignKeyName(ForeignKey fk)
68-
{
69-
// (the FKName length, of H3.2 implementation, may be too long for some RDBMS so we implement something different)
70-
int hash = fk.Name.GetHashCode() ^ Name.GetHashCode();
71-
return string.Format("KF{0}", hash.ToString("X"));
72-
}
73-
7472
public override Column GetColumn(Column column)
7573
{
7674
Column superColumn = base.GetColumn(column);

src/NHibernate/Mapping/ForeignKey.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections;
21
using System.Collections.Generic;
32
using System.Text;
43
using NHibernate.Util;
@@ -233,5 +232,7 @@ public bool IsReferenceToPrimaryKey
233232
{
234233
get { return referencedColumns.Count == 0; }
235234
}
235+
236+
public string GeneratedConstraintNamePrefix => "FK_";
236237
}
237238
}

src/NHibernate/Mapping/Table.cs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -808,15 +808,9 @@ public virtual ForeignKey CreateForeignKey(string keyName, IEnumerable<Column> k
808808
if (fk == null)
809809
{
810810
fk = new ForeignKey();
811-
if (!string.IsNullOrEmpty(keyName))
812-
{
813-
fk.Name = keyName;
814-
}
815-
else
816-
{
817-
fk.Name = "FK" + UniqueColumnString(kCols, referencedEntityName);
818-
//TODO: add referencedClass to disambiguate to FKs on the same columns, pointing to different tables
819-
}
811+
// NOTE : if the name is null, we will generate an implicit name during second pass processing
812+
// after we know the referenced table name (which might not be resolved yet).
813+
fk.Name = keyName;
820814
fk.Table = this;
821815
foreignKeys.Add(key, fk);
822816
fk.ReferencedEntityName = referencedEntityName;
@@ -837,8 +831,8 @@ public virtual ForeignKey CreateForeignKey(string keyName, IEnumerable<Column> k
837831

838832
public virtual UniqueKey CreateUniqueKey(IList<Column> keyColumns)
839833
{
840-
string keyName = "UK" + UniqueColumnString(keyColumns);
841-
UniqueKey uk = GetOrCreateUniqueKey(keyName);
834+
var keyName = Constraint.GenerateName( "UK_", this, null, keyColumns);
835+
var uk = GetOrCreateUniqueKey(keyName);
842836
uk.AddColumns(keyColumns);
843837
return uk;
844838
}
@@ -851,11 +845,14 @@ public virtual UniqueKey CreateUniqueKey(IList<Column> keyColumns)
851845
/// <returns>
852846
/// An unique string for the <see cref="Column"/> objects.
853847
/// </returns>
848+
// Since v5.2
849+
[Obsolete("Use Constraint.GenerateName instead.")]
854850
public string UniqueColumnString(IEnumerable uniqueColumns)
855851
{
856852
return UniqueColumnString(uniqueColumns, null);
857853
}
858854

855+
[Obsolete("Use Constraint.GenerateName instead.")]
859856
public string UniqueColumnString(IEnumerable iterator, string referencedEntityName)
860857
{
861858
// NH Different implementation (NH-1399)

0 commit comments

Comments
 (0)