You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Query on subclass entity ("Table-per-subclass with discriminator" mapping) uses subclass' table alias with base class' key column name in join to collection in a dynamic component
#2094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
We have the following mappings for an abstract "EducationOrganization" with a derived "School".
Note that the PK column for EducationOrganization is EducationOrganizationId, but in the derived entities, the key column name changes to match the derived entity name -- SchoolId for School, LocalEducationAgencyId for LocalEducationAgency, etc.
Note that the "left outer join" to SchoolCategory is performed from the base entity (EducationOrganization aka school0_) using EducationOrganizationId.
However, when we execute an HQL query on the dynamic component's collection, like the following:
from School a left join fetch a.Extensions.Sample b wherea.IdIN (:ids)
... the following FROM clause is generated (SQL join clause is invalid -- joins SchoolExtension using the derived table's alias with the base table's PK column name):
System.Data.SqlClient.SqlException (0x80131904): Invalid column name 'EducationOrganizationId'.
Note that this time, the "left outer join" to SchoolExtension is performed using the concrete entity's alias (School aka school0_1_), but still using EducationOrganizationId (the base table's primary key).
Previously we had mapped this relationship as "Table per subclass" without discriminators -- and it worked correctly (using the base entity's alias of school0_ for the joins):
After debugging NHibernate, here is what I have found:
With the "Table per subclass" mapping (without discriminator), it uses the JoinedSubclassEntityPersister.
With the "Table per subclass, using discriminator" mapping, it uses the SingleTableEntityPersister.
The alias used in the join is built by the ToColumns method in the BasicEntityPropertyMapping class, which looks like this:
// class BasicEntityPropertyMappingpublicoverridestring[]ToColumns(stringalias,stringpropertyName){returnbase.ToColumns(this.persister.GenerateTableAlias(alias,this.persister.GetSubclassPropertyTableNumber(propertyName)),propertyName);}
As you can see in the method below (from AbstractEntityPersister), if the persister's GetSubclassPropertyTableNumber returns a 0, the alias will be the root alias (i.e. the EducationOrganization table). If the return value is 1, the alias will be school0_1_ (the School table). (The property passed in is always the base table's key -- EducationOrganizationId.)
// class AbstractEntityPersisterpublicstringGenerateTableAlias(stringrootAlias,inttableNumber){if(tableNumber==0)returnrootAlias;StringBuilderstringBuilder=newStringBuilder().Append(rootAlias);if(!rootAlias.EndsWith("_"))stringBuilder.Append('_');returnstringBuilder.Append(tableNumber).Append('_').ToString();}
Both persisters (JoinedSubclassEntityPersister and SingleTableEntityPersister) use the GetSubclassPropertyTableNumber method inherited from AbstractEntityPersister:
// class AbstractEntityPersisterpublicvirtualintGetSubclassPropertyTableNumber(stringpropertyPath){stringpropertyName=StringHelper.Root(propertyPath);ITypetype=this.propertyMapping.ToType(propertyName);if(type.IsAssociationType){IAssociationTypeassociationType=(IAssociationType)type;if(associationType.UseLHSPrimaryKey)return0;if(type.IsCollectionType)propertyName=associationType.LHSPropertyName;}inti=System.Array.IndexOf<string>(this.SubclassPropertyNameClosure,propertyName);if(i!=-1)returnthis.GetSubclassPropertyTableNumber(i);return0;}
It is clear that the intent (from these 2 methods) in AbstractEntityPersister is that the "root" table will have an index of 0. Properties that represent associations are explicitly given the 0 to join using the base class.
However, there is a discrepancy between the two persisters with how the tables are ordered in their internal arrays.
For the JoinedSubclassEntityPersister, I see the following in the subclassTableNameClosure field:
index
name
0
edfi.School
1
edfi.EducationOrganization
For the SingleTableEntityPersister, I see the following in the subclassTableNameClosure table:
index
name
0
edfi.EducationOrganization
1
edfi.School
Note that they are reversed, and I think that based on the intent of having the "root" table in position 0, that the JoinedSubclassEntityPersister actually has it wrong/backwards (but ironically it is this implementation that works because there is another problem GetSubclassPropertyTableNumber method which happens to "correct" this problem).
When the GetSubclassPropertyTableNumber is invoked, here are the results:
a.SchoolCategories
a.Extensions.Sample
JoinedSubclassEntityPersister
0 (it's an association)
0 (School's table index)
SingleTableEntityPersister
0 (it's an association)
1 (School's table index)
Remember, the return value has the following impact on the generated JOIN:
TableNumber
Resulting Join
0
on school0_.EducationOrganizationId=sample1_.SchoolId
1
on school0_1_.EducationOrganizationId=sample1_.SchoolId
Property path SchoolCategories is an association, so it always returns 0 and is joined correctly using school0_.EducationOrganizationId.
Property path Extensions.Sample (which actually represents an association) is trimmed by the method to the root value of Extensions. This trimmed path represents a component, so the property lookup proceeds, as follows:
On the JoinedSubclassEntityPersister, this column is found in the "School" table, which is in the 0 position of the table name closure, and so the "correct" value of 0 is returned.
On the SingleTableEntityPersister, this column is found in the "School" table (correctly), which is in the 1 position of the table name closure, and so the "incorrect" value of 1 is returned.
Essentially, I believe there are 2 problems here:
JoinedSubclassEntityPersister seems to have the table order reversed in its table name closure, so that the derived table is in the first position instead of the "root" table.
The GetSubclassPropertyTableNumber method assumes that the provided property path should always be trimmed to the "root" before evaluation. However, our dynamic component (Extensions) can have associations (Sample), and those associations should be evaluated for what they are rather than just the containing property for the component. As associations, they would be given the "root" alias, but as members of a component, it depends on the position of the derived table in the closure.
In the end, when evaluating the a.Extensions.Sample property path, the JoinedSubclassEntityPersister seems to return the correct value of 0 because these two (perceived) bugs cancel each other out. The method returns 0 because that is where the Extensions property is found (on the "edfi.School" table in position 0).
While in the SingleTableEntityPersister, the method returns 1 because it is evaluating the Extensions component rather than the Extensions.Sample association. What we really need here is to do the join on the association Extensions.Sample, and so a 0 should be returned.
Here is my suggested fix for the GetSubclassPropertyTableNumber method:
stringpropertyName=StringHelper.Root(propertyPath);ITypetype=this.propertyMapping.ToType(propertyName);// BEGIN SUGGESTED CODE// If root path is a component, revert to using the full property pathif(type.IsComponentType){propertyName=propertyPath;type=this.propertyMapping.ToType(propertyPath);}// END SUGGESTED CODEif(type.IsAssociationType){
As for the discrepancy in the order of the entries of the table name closures between JoinedSubclassEntityPersister and the SingleTableEntityPersister, I don't have any suggestions -- but it doesn't look right to me.
The text was updated successfully, but these errors were encountered:
We have the following mappings for an abstract "EducationOrganization" with a derived "School".
When we execute an HQL query like the following:
... the following
FROM
clause is generated (with correct joins):Note that the "left outer join" to SchoolCategory is performed from the base entity (EducationOrganization aka
school0_
) usingEducationOrganizationId
.However, when we execute an HQL query on the dynamic component's collection, like the following:
... the following
FROM
clause is generated (SQL join clause is invalid -- joins SchoolExtension using the derived table's alias with the base table's PK column name):We get the following exception in this case:
Note that this time, the "left outer join" to SchoolExtension is performed using the concrete entity's alias (School aka
school0_1_
), but still usingEducationOrganizationId
(the base table's primary key).Previously we had mapped this relationship as "Table per subclass" without discriminators -- and it worked correctly (using the base entity's alias of
school0_
for the joins):After debugging NHibernate, here is what I have found:
JoinedSubclassEntityPersister
.SingleTableEntityPersister
.ToColumns
method in theBasicEntityPropertyMapping
class, which looks like this:As you can see in the method below (from
AbstractEntityPersister
), if the persister'sGetSubclassPropertyTableNumber
returns a0
, the alias will be the root alias (i.e. the EducationOrganization table). If the return value is1
, the alias will beschool0_1_
(the School table). (The property passed in is always the base table's key --EducationOrganizationId
.)Both persisters (
JoinedSubclassEntityPersister
andSingleTableEntityPersister
) use theGetSubclassPropertyTableNumber
method inherited fromAbstractEntityPersister
:It is clear that the intent (from these 2 methods) in
AbstractEntityPersister
is that the "root" table will have an index of0
. Properties that represent associations are explicitly given the0
to join using the base class.However, there is a discrepancy between the two persisters with how the tables are ordered in their internal arrays.
For the
JoinedSubclassEntityPersister
, I see the following in thesubclassTableNameClosure
field:For the
SingleTableEntityPersister
, I see the following in thesubclassTableNameClosure
table:Note that they are reversed, and I think that based on the intent of having the "root" table in position 0, that the
JoinedSubclassEntityPersister
actually has it wrong/backwards (but ironically it is this implementation that works because there is another problemGetSubclassPropertyTableNumber
method which happens to "correct" this problem).When the
GetSubclassPropertyTableNumber
is invoked, here are the results:Remember, the return value has the following impact on the generated JOIN:
on school0_.EducationOrganizationId=sample1_.SchoolId
on school0_1_.EducationOrganizationId=sample1_.SchoolId
Property path
SchoolCategories
is an association, so it always returns0
and is joined correctly usingschool0_.EducationOrganizationId
.Property path
Extensions.Sample
(which actually represents an association) is trimmed by the method to the root value ofExtensions
. This trimmed path represents a component, so the property lookup proceeds, as follows:JoinedSubclassEntityPersister
, this column is found in the "School" table, which is in the0
position of the table name closure, and so the "correct" value of0
is returned.SingleTableEntityPersister
, this column is found in the "School" table (correctly), which is in the1
position of the table name closure, and so the "incorrect" value of1
is returned.Essentially, I believe there are 2 problems here:
JoinedSubclassEntityPersister
seems to have the table order reversed in its table name closure, so that the derived table is in the first position instead of the "root" table.GetSubclassPropertyTableNumber
method assumes that the provided property path should always be trimmed to the "root" before evaluation. However, our dynamic component (Extensions
) can have associations (Sample
), and those associations should be evaluated for what they are rather than just the containing property for the component. As associations, they would be given the "root" alias, but as members of a component, it depends on the position of the derived table in the closure.In the end, when evaluating the
a.Extensions.Sample
property path, theJoinedSubclassEntityPersister
seems to return the correct value of0
because these two (perceived) bugs cancel each other out. The method returns0
because that is where theExtensions
property is found (on the "edfi.School" table in position 0).While in the
SingleTableEntityPersister
, the method returns1
because it is evaluating theExtensions
component rather than theExtensions.Sample
association. What we really need here is to do the join on the associationExtensions.Sample
, and so a0
should be returned.Here is my suggested fix for the
GetSubclassPropertyTableNumber
method:As for the discrepancy in the order of the entries of the table name closures between
JoinedSubclassEntityPersister
and theSingleTableEntityPersister
, I don't have any suggestions -- but it doesn't look right to me.The text was updated successfully, but these errors were encountered: