These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 9 posts ] 
Author Message
 Post subject: Case Sensitive Primary Keys
PostPosted: Thu Sep 14, 2006 3:01 pm 
Regular
Regular

Joined: Tue May 31, 2005 3:18 pm
Posts: 117
Location: Houston
I have a table where the primary key is a string (yuck, I know -- I can't change it).

Currently it is managed mostly by custom scripts, so this key gets polluted all over the databse, sometimes in differing case. So you'll have "domain\joe.user" in the User table, and "DOMAIN\Joe.User" as a foreign key reference in another table. Using joins this works fine because SQL is case sensitive.

The problem using NHibernate to manage this is that it compares strings to match things up.

So I have a schema like this:

Code:
USER
login          name
-----------------------------
dummy      Dummy User


APPLICATIONS
applicationid        name
---------------------------------------------
app1                   Application 1
app2                   Application 2

USER_APPS
login                  applicationid
---------------------------------------------
dummy              app1
DuMmY              app2


This is easy to understand; a Many-to-Many between Users and Applications. But when I load this User in NHibernate, User.Applications.Count returns 1... NOT 2

So I read these forums and there is an old thread which talks about this. Sergey suggested using a Custom Mapping Type. So I created a CaseInsensitiveStringType that implements the Equals method by first comparing the .ToLower() of the 2 strings.

Here's that class:
Code:
Public Class CaseInsensitiveStringType
    Implements IUserType

    Private SQL_TYPES() As NHibernate.SqlTypes.SqlType = {New NHibernate.SqlTypes.StringSqlType()}

    Public Function DeepCopy(ByVal value As Object) As Object Implements NHibernate.IUserType.DeepCopy
        Return value
    End Function

    Public Function Equals1(ByVal x As Object, ByVal y As Object) As Boolean Implements NHibernate.IUserType.Equals
        'are they both strings?
        If Not (TypeOf (x) Is String AndAlso TypeOf (y) Is String) Then
            Return False
        End If

        'are they equal (case insensitive)
        Return x.ToString().ToLower().Equals(y.ToString.ToLower())
    End Function

    Public ReadOnly Property IsMutable() As Boolean Implements NHibernate.IUserType.IsMutable
        Get
            Return False
        End Get
    End Property

    Public Function NullSafeGet(ByVal rs As System.Data.IDataReader, ByVal names() As String, ByVal owner As Object) As Object Implements NHibernate.IUserType.NullSafeGet
        'get the value, if it is null, set the string to be emtpy
        Dim idx As Int16 = rs.GetOrdinal(names(0))
        Dim val As String

        If rs.IsDBNull(idx) Then
            val = String.Empty
        Else
            val = rs.GetString(idx)
        End If

        Return val
    End Function

    Public Sub NullSafeSet(ByVal cmd As System.Data.IDbCommand, ByVal value As Object, ByVal index As Integer) Implements NHibernate.IUserType.NullSafeSet
        'we don't care about nulls here
        Dim param As IDataParameter = cmd.Parameters(index)
        param.Value = value
    End Sub

    Public ReadOnly Property ReturnedType() As System.Type Implements NHibernate.IUserType.ReturnedType
        Get
            Return GetType(String)
        End Get
    End Property

    Public ReadOnly Property SqlTypes() As NHibernate.SqlTypes.SqlType() Implements NHibernate.IUserType.SqlTypes
        Get
            Return SQL_TYPES
        End Get
    End Property
End Class


I change my mapping files like this:

User.hbm.xml
Code:
<class name="User" table="[User]">
    <id name="LoginId" column="LoginId" type="MyProject.CaseInsensitiveStringType, MyProject" length="50" access="nosetter.camelcase-underscore">
      <generator class="assigned" />     
    </id>

    <!-- main properties -->
    <property name="FirstName" column="firstName" type="String" length="50" />
    <property name="LastName" column="lastName" type="String" length="50" />
   
    <set name="applications" lazy="true" access="NHibernate.Generics.GenericAccessor, NHibernate.Generics" table="AppUserXRef">
      <key column="LoginId" />
      <many-to-many class="Application" column="ApplicationId" />
    </set>

  </class>


and Application.hbm.xml
Code:
<class name="Application" table="Application" proxy="Application" lazy="true">

    <id name="ApplicationId" type="MyProject.CaseInsensitiveStringType, MyProject" length="50" column="ApplicationId" access="nosetter.camelcase-underscore">
      <column name="ApplicationId" />
      <generator class="assigned" />
    </id>
   
    <property name="Name" type="String" length="100" not-null="true" column="Description" />
    <property name="Url" type="String" length="100" not-null="false" column="Url" />

  </class>


I'm still getting the same results. Am I approaching my custom type the right way?

_________________
------------------------------
Ben Scheirman
http://www.flux88.com


Top
 Profile  
 
 Post subject:
PostPosted: Fri Sep 15, 2006 10:54 am 
Regular
Regular

Joined: Tue May 31, 2005 3:18 pm
Posts: 117
Location: Houston
Here is a test that shows what I'm talking about:
Code:
<Test()> _
    Public Sub TestLoginIdCaseDoesntMatter()

        Using (New TransactionScope())

            Dim conn As IDbConnection = TestHelper.GetDbConnection()

            Try
                conn.Open()       
                Dim cmd As IDbCommand = conn.CreateCommand()

                'insert dummy user
                cmd.CommandText = "INSERT INTO [User](LoginId, FirstName, LastName, Email, CreatedBy) VALUES('test_user_with_case', 'test', 'user', 'user@email.com', 'test');"
                cmd.ExecuteNonQuery()

                'insert a couple applications
                cmd.CommandText = "INSERT INTO Application(ApplicationId, Description, CreatedBy) VALUES('TEST_APP', 'testapplication', 'test');"
                cmd.ExecuteNonQuery()

                cmd.CommandText = "INSERT INTO Application(ApplicationId, Description, CreatedBy) VALUES('TEST2_APP', 'testapplication2', 'test');"
                cmd.ExecuteNonQuery()

                'add app references to user (1 with diferring case for loginid)
                cmd.CommandText = "INSERT INTO [AppUserXRef](ApplicationId, LoginId) VALUES('TEST_APP', 'TesT_UsEr_WiTh_CaSE');"
                cmd.ExecuteNonQuery()

                cmd.CommandText = "INSERT INTO [AppUserXRef](ApplicationId, LoginId) VALUES('TEST2_APP', 'test_user_with_case');"
                cmd.ExecuteNonQuery()

                Dim session As NHibernate.ISession = NHibernateUtil.GetSession(conn)

                Dim u As User = New UserRepository(session).Load("test_user_with_case")
                Assert.AreEqual(2, u.Applications.Count) //fails

            Finally
                If conn.State = ConnectionState.Open Then
                    conn.Close()
                End If
            End Try

        End Using

    End Sub

_________________
------------------------------
Ben Scheirman
http://www.flux88.com


Top
 Profile  
 
 Post subject:
PostPosted: Tue Sep 19, 2006 3:11 am 
Senior
Senior

Joined: Mon Aug 21, 2006 9:18 am
Posts: 179
Hi Ben,
I blogged about implementing a generic Composite user type at http://geekswithblogs.net/opiesblog/archive/2006/08/13/87880.aspx.
I have used this on all my Value Objects for mapping and it has worked great so far. I use ICompositeUserType in lieu of IUSer type for HQL support.
The added benefit of being able to use HQL on the CompositeUserTYpe properties drove me to use this.

I think the listing on the blog has a slight bug I have fixed. Let me know if you need me to send you the latest version.

It may not be exactly what you need, but might help.

_________________
If this helped...please remember to rate it!


Top
 Profile  
 
 Post subject:
PostPosted: Tue Sep 19, 2006 10:12 am 
Regular
Regular

Joined: Tue May 31, 2005 3:18 pm
Posts: 117
Location: Houston
I don't think ICompositeUserType would be of help because I'm just comparing a string.

I'm also not sure where this check is failing...

I added my user type as the primary key type of the User.hbm.xml and that should be all, correct? Any foreign key references should use that type to do it's comparison.....

I'm also curious to see how visible this will be with DEBUG level logging, b/c right now it's very difficult to see the forest through all the trees...

_________________
------------------------------
Ben Scheirman
http://www.flux88.com


Top
 Profile  
 
 Post subject:
PostPosted: Tue Sep 19, 2006 1:07 pm 
Senior
Senior

Joined: Mon Aug 21, 2006 9:18 am
Posts: 179
Hi Ben,
I use IComposite instead of IUserType for all custom types even if it is just one property.
Looking at your code seems like what you have would work with IUserType tho...Tho in your Application.hbm.xml you have the column for your id specificed twice...once in the <id tag and again as a <column tag.
Today on my break I'll dummy a quick example using my GenericComposite implementation and see if I can get it to work...I'm curious.


MIKE

_________________
If this helped...please remember to rate it!


Top
 Profile  
 
 Post subject:
PostPosted: Tue Sep 19, 2006 2:20 pm 
Regular
Regular

Joined: Tue May 31, 2005 3:18 pm
Posts: 117
Location: Houston
oops... I didn't realize about the column being defined twice. I don't think it hurt anything though, as all my tests are still passing.

_________________
------------------------------
Ben Scheirman
http://www.flux88.com


Top
 Profile  
 
 Post subject:
PostPosted: Mon Sep 25, 2006 10:20 am 
Regular
Regular

Joined: Tue May 31, 2005 3:18 pm
Posts: 117
Location: Houston
I still haven't solved this issue. I have tried removing NHibernate.Generics and used a bag instead and I get the same results, so that dll isn't the culprit.

I also tried using a custom IComparer that returned a case-insensitive comparison on the Applications collection and it still fails on the same line.

Does anybody have any other ideas that might work here?

_________________
------------------------------
Ben Scheirman
http://www.flux88.com


Top
 Profile  
 
 Post subject:
PostPosted: Wed Sep 27, 2006 6:07 pm 
Regular
Regular

Joined: Tue May 31, 2005 3:18 pm
Posts: 117
Location: Houston
I upgraded to 1.20beta1 and included the source in my project. With (a lot of) Ayende Rahien's help we identified a bug in IUserType for keys.

He submitted a JIRA for it here: http://jira.nhibernate.org/browse/NH-732

For the time-being I just make every value lowercase in the NullSafeGet implementation of IUserType.

This way all of my string are loaded as lowercase and nhibernate doesn't know the difference. A fairly decent workaround with few ramifications.

If you are having the same issue I was, please vote on the bug at JIRA.

_________________
------------------------------
Ben Scheirman
http://www.flux88.com


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 28, 2006 3:39 pm 
Senior
Senior

Joined: Mon Aug 21, 2006 9:18 am
Posts: 179
Glad you got it figured out. Sorry I couldn't help more.

MIKE

_________________
If this helped...please remember to rate it!


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 9 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.