Skip to content

Commit f7bc251

Browse files
Parameterize string type comparer (#1833)
Co-authored-by: Alexander Zaytsev <[email protected]>
1 parent 5e8f2c8 commit f7bc251

File tree

10 files changed

+523
-11
lines changed

10 files changed

+523
-11
lines changed

doc/reference/modules/basic_mapping.xml

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@
5757
We will now discuss the content of the mapping document. We will mainly describe the
5858
document elements and attributes that are used by NHibernate at runtime. The mapping
5959
document also contains some extra optional attributes and elements that affect the
60-
database schemas exported by the schema export tool. (By example the
61-
<literal>column</literal> sub-element of a property.)
60+
database schemas exported by the schema export tool, for example, the
61+
<literal>column</literal> sub-element of a property.
6262
</para>
6363

6464
<sect2 id="mapping-declaration-xmlns">
@@ -3884,6 +3884,63 @@
38843884
</tgroup>
38853885
</table>
38863886

3887+
<para>
3888+
String types use by default .Net default string equality, which is case sensitive and culture
3889+
insensitive. When a string type is used as an identifier, if the underlying database string
3890+
equality semantic differs, it may cause issues. For example, loading a children collection by
3891+
a string parent key stored in a case insensitive column may cause matching children having
3892+
their parent key differing by the case to be ignored by NHibernate by default.
3893+
</para>
3894+
3895+
<para>
3896+
String types comparer can be set for matching the database or column behavior. To set the
3897+
default comparer for all string types, affect by code a <literal>StringComparer</literal> to
3898+
<literal>AbstractStringType.DefaultComparer</literal> static property. To set the comparer used
3899+
only for a specific <literal>&lt;property&gt;</literal> or <literal>&lt;id&gt;</literal>,
3900+
specify its type as a sub-element and supply <literal>IgnoreCase</literal> and/or
3901+
<literal>ComparerCulture</literal> parameters.
3902+
</para>
3903+
3904+
<programlistingco>
3905+
<areaspec>
3906+
<area id="stringcomp1" coords="3 55"/>
3907+
<area id="stringcomp2" coords="4 55"/>
3908+
</areaspec>
3909+
<programlisting><![CDATA[<id name="Id" generator="assigned">
3910+
<type name="string">
3911+
<param name="ComparerCulture">en-US</param>
3912+
<param name="IgnoreCase">true</param>
3913+
</type>
3914+
</id>]]></programlisting>
3915+
<calloutlist>
3916+
<callout arearefs="stringcomp1">
3917+
<para>
3918+
<literal>IgnoreCase</literal>: <literal>true</literal> for case insensitive comparisons.
3919+
Any other value will result in case sensitive comparisons.
3920+
</para>
3921+
</callout>
3922+
<callout arearefs="stringcomp2">
3923+
<para>
3924+
<literal>ComparerCulture</literal>: <literal>Current</literal> for using the application
3925+
current culture, <literal>Invariant</literal> for using the invariant culture,
3926+
<literal>Ordinal</literal> for using ordinal comparison, or any valid culture name for
3927+
using another culture. By default, ordinal comparisons are used.
3928+
</para>
3929+
</callout>
3930+
</calloutlist>
3931+
</programlistingco>
3932+
3933+
<para>
3934+
If you have many properties to map with these parameters, consider using <literal>&lt;typedef&gt;</literal>.
3935+
See <xref linkend="mapping-types-custom"/> for more information.
3936+
</para>
3937+
3938+
<para>
3939+
These settings should be used in order to match the database or column behavior. They are not taken into
3940+
account by the hbm2ddl tool for generating the database schema. (In other words, it will not generate
3941+
matching <literal>collate</literal> statements for SQL-Server.)
3942+
</para>
3943+
38873944
<table>
38883945
<title>Large Object Mapping Types</title>
38893946
<tgroup cols="4">
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using NUnit.Framework;
12+
13+
namespace NHibernate.Test.NHSpecificTest.GH1833
14+
{
15+
using System.Threading.Tasks;
16+
[TestFixture]
17+
public class FixtureAsync : BugTestCase
18+
{
19+
protected override bool AppliesTo(Dialect.Dialect dialect)
20+
{
21+
return dialect.AreStringComparisonsCaseInsensitive;
22+
}
23+
24+
protected override void OnSetUp()
25+
{
26+
using (var session = OpenSession())
27+
using (var transaction = session.BeginTransaction())
28+
{
29+
var e1 = new Entity {Id = "Bob", Name = "Bob"};
30+
session.Save(e1);
31+
32+
var e2 = new Entity {Id = "Sally", Name = "Sally"};
33+
session.Save(e2);
34+
35+
session.Flush();
36+
37+
var c1 = new Child {ParentName = "Bob", Name = "Max"};
38+
session.Save(c1);
39+
40+
var c2 = new Child {ParentName = "sally", Name = "Cindy"};
41+
session.Save(c2);
42+
43+
transaction.Commit();
44+
}
45+
}
46+
47+
protected override void OnTearDown()
48+
{
49+
using (var session = OpenSession())
50+
using (var transaction = session.BeginTransaction())
51+
{
52+
session.CreateQuery("delete from Child").ExecuteUpdate();
53+
session.CreateQuery("delete from Entity").ExecuteUpdate();
54+
55+
transaction.Commit();
56+
}
57+
}
58+
59+
// #1828
60+
[Test]
61+
public async Task GetOnParentWithDifferentCaseCanLoadChildrenAsync()
62+
{
63+
using (var s = OpenSession())
64+
using (var t = s.BeginTransaction())
65+
{
66+
var bob = await (s.GetAsync<Entity>("bob"));
67+
Assert.That(bob, Is.Not.Null);
68+
Assert.That(bob.Children, Has.Count.EqualTo(1));
69+
await (t.CommitAsync());
70+
}
71+
}
72+
73+
// NH-3833
74+
[Test]
75+
public async Task ParentCanLoadChildrenWithDifferentParentCaseAsync()
76+
{
77+
using (var s = OpenSession())
78+
using (var t = s.BeginTransaction())
79+
{
80+
var sally = await (s.GetAsync<Entity>("Sally"));
81+
Assert.That(sally, Is.Not.Null);
82+
Assert.That(sally.Children, Has.Count.EqualTo(1));
83+
await (t.CommitAsync());
84+
}
85+
}
86+
}
87+
}

src/NHibernate.Test/Async/TypesTest/StringTypeFixture.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
//------------------------------------------------------------------------------
99

1010

11+
using System;
12+
using System.Collections.Generic;
1113
using System.Linq;
14+
using NHibernate.Type;
1215
using NUnit.Framework;
1316
using NHibernate.Linq;
1417

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace NHibernate.Test.NHSpecificTest.GH1833
5+
{
6+
class Entity
7+
{
8+
public virtual string Id { get; set; }
9+
public virtual string Name { get; set; }
10+
public virtual ISet<Child> Children { get; set; }
11+
}
12+
13+
class Child
14+
{
15+
public virtual Guid Id { get; set; }
16+
public virtual string Name { get; set; }
17+
public virtual string ParentName { get; set; }
18+
}
19+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using NUnit.Framework;
2+
3+
namespace NHibernate.Test.NHSpecificTest.GH1833
4+
{
5+
[TestFixture]
6+
public class Fixture : BugTestCase
7+
{
8+
protected override bool AppliesTo(Dialect.Dialect dialect)
9+
{
10+
return dialect.AreStringComparisonsCaseInsensitive;
11+
}
12+
13+
protected override void OnSetUp()
14+
{
15+
using (var session = OpenSession())
16+
using (var transaction = session.BeginTransaction())
17+
{
18+
var e1 = new Entity {Id = "Bob", Name = "Bob"};
19+
session.Save(e1);
20+
21+
var e2 = new Entity {Id = "Sally", Name = "Sally"};
22+
session.Save(e2);
23+
24+
session.Flush();
25+
26+
var c1 = new Child {ParentName = "Bob", Name = "Max"};
27+
session.Save(c1);
28+
29+
var c2 = new Child {ParentName = "sally", Name = "Cindy"};
30+
session.Save(c2);
31+
32+
transaction.Commit();
33+
}
34+
}
35+
36+
protected override void OnTearDown()
37+
{
38+
using (var session = OpenSession())
39+
using (var transaction = session.BeginTransaction())
40+
{
41+
session.CreateQuery("delete from Child").ExecuteUpdate();
42+
session.CreateQuery("delete from Entity").ExecuteUpdate();
43+
44+
transaction.Commit();
45+
}
46+
}
47+
48+
// #1828
49+
[Test]
50+
public void GetOnParentWithDifferentCaseCanLoadChildren()
51+
{
52+
using (var s = OpenSession())
53+
using (var t = s.BeginTransaction())
54+
{
55+
var bob = s.Get<Entity>("bob");
56+
Assert.That(bob, Is.Not.Null);
57+
Assert.That(bob.Children, Has.Count.EqualTo(1));
58+
t.Commit();
59+
}
60+
}
61+
62+
// NH-3833
63+
[Test]
64+
public void ParentCanLoadChildrenWithDifferentParentCase()
65+
{
66+
using (var s = OpenSession())
67+
using (var t = s.BeginTransaction())
68+
{
69+
var sally = s.Get<Entity>("Sally");
70+
Assert.That(sally, Is.Not.Null);
71+
Assert.That(sally.Children, Has.Count.EqualTo(1));
72+
t.Commit();
73+
}
74+
}
75+
}
76+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test"
3+
namespace="NHibernate.Test.NHSpecificTest.GH1833">
4+
5+
<class name="Entity">
6+
<id name="Id" generator="assigned">
7+
<type name="string">
8+
<param name="IgnoreCase">true</param>
9+
</type>
10+
</id>
11+
<property name="Name"/>
12+
<set name="Children" >
13+
<key column="ParentName"/>
14+
<one-to-many class="Child"/>
15+
</set>
16+
</class>
17+
18+
<class name="Child">
19+
<id name="Id" generator="guid.comb"/>
20+
<property name="Name"/>
21+
<property name="ParentName"/>
22+
</class>
23+
24+
</hibernate-mapping>

0 commit comments

Comments
 (0)