Skip to content
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.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HHH-18693 Hibernate Processor does not handle inner types #9098

Merged

Conversation

cigaly
Copy link
Contributor

@cigaly cigaly commented Oct 16, 2024

See Jira Issue HHH-18693

This is still work in progress, not to be merged with main branch, at least not immediately.

For each (static) inner non-private class one meta-model class will be generated.

Naming is as follows. If we have:

class A {
    ...
    @Entity
    static class B {
        ...
    }
    ...
}

Generated class will be named A_B_


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.


@cigaly
Copy link
Contributor Author

cigaly commented Oct 16, 2024

Some existing test classes were changed to make inner classes non-private. Otherwise such class could not be referenced from meta model class.

@gavinking
Copy link
Member

Thanks for working on this @cigaly !

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from 965c5f0 to c20fa62 Compare October 23, 2024 14:14
@gavinking
Copy link
Member

gavinking commented Oct 23, 2024

Some existing test classes were changed to make inner classes non-private. Otherwise such class could not be referenced from meta model class.

I think the Processor is still going to have to be able to elegantly handle private inner classes; we don't want to simply disallow that.

So I'm not quite sure what "elegantly" means here ... perhaps it just means "skip them".

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from c20fa62 to a8fb6dd Compare October 24, 2024 06:28
@cigaly
Copy link
Contributor Author

cigaly commented Oct 24, 2024

So I'm not quit sure what "elegantly" means here ... perhaps it just means "skip them".

Other than disallowing private classes (either inner or top-level) , excluding them from metamodel generation is only solution that will not cause Java compilation errors. And, of course, there is always possibility that private static class could be referenced by either top level class containing private class or other inner class contained by same top level class.

Now only one test class has to be changed to avoid compilation errors is org.hibernate.orm.test.schematools.PrimaryKeyColumnOrderTest.EntityId. Here metemodel generated from

	public static class EntityId implements Serializable {
		private int A;
		private int B;
	}

will result in

public abstract class PrimaryKeyColumnOrderTest_EntityId_ {
	public static final String A = "A";
	public static final String B = "B";
	public static volatile SingularAttribute<EntityId, Integer> A;
	public static volatile SingularAttribute<EntityId, Integer> B;
	public static volatile EmbeddableType<EntityId> class_;
}

which is definitely something Java is happy with. Perhaps some mechanism can be added to prevent such duplications, but until then this class should be slightly changed to let tests to run.

@gavinking
Copy link
Member

gavinking commented Oct 24, 2024

Now only one test class has to be changed to avoid compilation errors is

Hurm.... so that's actually a hole in the algorithm defined by the spec, isn't it? We should fix that, in both Processor, and in the spec. For example, we could say that A -> _A.

@cigaly
Copy link
Contributor Author

cigaly commented Oct 24, 2024

This is more like one of those "tricky" questions for entertainment pages of some Java magazine. Something like "how are named getters and setters in a class with int property named abc and String property named Abc".

It seems that in current code only problematic are properties with name consisting of single uppercase letter. To fix that (at least, until some other solution) I will change method org.hibernate.processor.util.StringUtil#getUpperUnderscoreCaseFromLowerCamelCase by inserting right after method declaration lines

		if (lowerCamelCaseString.length()== 1 && lowerCamelCaseString.toUpperCase().equals( lowerCamelCaseString )) {
			return "_" + lowerCamelCaseString;
		}

(or, maybe, to use underscore as a suffix?)

If nothing else, this will allow implement metamodel class generation for inner classes, without touching any core (main or test) code.

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from a8fb6dd to 83e4d1e Compare October 24, 2024 07:40
@gavinking
Copy link
Member

Yeah but you can just use Character.isUpperCase() 😜

@cigaly
Copy link
Contributor Author

cigaly commented Oct 24, 2024

Yes, you are right. It seems that I am still sleepy :-)

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from 83e4d1e to 3d9520b Compare October 24, 2024 07:45
@cigaly cigaly marked this pull request as ready for review October 25, 2024 09:31
@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from f920687 to c650224 Compare November 4, 2024 16:28
@gavinking
Copy link
Member

@cigaly can you give me permission to push to your branch?

@gavinking
Copy link
Member

Or, whatever, don't worry, I can open a new PR myself, I guess.

@cigaly
Copy link
Contributor Author

cigaly commented Nov 4, 2024

@cigaly can you give me permission to push to your branch?

Of course ... only I hope that I did not done something completely different :-)

@gavinking
Copy link
Member

Or, whatever, don't worry, I can open a new PR myself, I guess.

Heh, I actually typed these comments on the wrong PR. I was meaning to post on the MySQL PR.

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from c650224 to 99774e7 Compare November 16, 2024 20:38
Copy link
Member

@gavinking gavinking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @cigaly, looks pretty reasonable, but see my comments.

Comment on lines 374 to 377
if ( !included( element )
|| hasAnnotation( element, Constants.EXCLUDE )
|| hasPackageAnnotation( element, Constants.EXCLUDE ) ) {
// skip it completely
}
else if ( isEntityOrEmbeddable( element ) && !element.getModifiers().contains( Modifier.PRIVATE )) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could just say that the processor completely skips private inner types. WDYT of this @cigaly:

Suggested change
if ( !included( element )
|| hasAnnotation( element, Constants.EXCLUDE )
|| hasPackageAnnotation( element, Constants.EXCLUDE ) ) {
// skip it completely
}
else if ( isEntityOrEmbeddable( element ) && !element.getModifiers().contains( Modifier.PRIVATE )) {
if ( !included( element )
|| hasAnnotation( element, Constants.EXCLUDE )
|| hasPackageAnnotation( element, Constants.EXCLUDE )
|| element.getModifiers().contains( Modifier.PRIVATE ) ) {
// skip it completely
}
else if ( isEntityOrEmbeddable( element ) ) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. And for maybe will be good idea to keep access modifier for non private classes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And for maybe will be good idea to keep access modifier for non private classes?

You mean generate the metamodel with the same visibility? Yeah, that makes sense I suppose...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing ... in PR #9220 id class is generated as inner class of parent meta model class. Here meta model class for each inner class is generated as separate class. I guess that now is good time to decide if this should stay that way or will be better idea to generate them as inner classes as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer it to be an inner class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I will try to refactor this to make things that way some time next week.

@@ -714,7 +728,7 @@ private void writeIndex() {
.createResource(
StandardLocation.SOURCE_OUTPUT,
ENTITY_INDEX,
entityName,
URLEncoder.encode(entityName, UTF_8),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take look at the Person in org.hibernate.orm.test.hql.QuotedIdentifierTest. There entity name is The Person (guess that this is not forbidden by the specification) and processing (and/or compilation) is crashing with message:

java.lang.IllegalArgumentException: Invalid relative name: The Person

If you like, I can write similar test for Hibernate Processor. Of course, if such entity name is allowed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see. But then I think we need to make the same change in ProcessorSessionFactory.findEntityByUnqualifiedName()?

if such entity name is allowed.

Well that is a great question. It looks like the JPA spec is silent on the format of entity names, except to say that they are case-sensitive, and unique in a PU.

I think, however, that we can infer that they are not supposed to have spaces in them, because then we would not be able to parse the query language.

I think I should open an issue against the spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, ProcessorSessionFactory.findEntityByUnqualifiedName() will work fine, since entity name is key for entityNameMappings. And such entity can be, despite its really 'ugly' name, used in HQL queries if properly quoted. QuotedIdentifierTest is testing exactly that. I've just tried to name class 'The Person?', and it is working fine.
But, I guess that spec should be changes anyway. Although, I guess that some hardcore Star Trek fans will be disappointed if not able to name entites using Klingon alphabet :-) [It is working now, BTW]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed that change and excluded all "troublemakers". Unfortunately, I can not find wither where this entity index is specified or used, Problem with such "ugly" entity names can not be solved in Hibernate, because finally it is used in com.sun.tools.javac.file.JavacFileManager#getFileForOutput. That name must be encoded to pass as constructor parameter for java.net.URI, but must not be encoded to allow file to be created with proper entity name. Something that Joseph Keller called "Catch 22" :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to worry about indexing such pathological entity names. They can't be used in queries anyway (since the query can't be parsed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jakartaee/persistence#686

For the record.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be used if quoted. But I agree that that such names should not be used. Maybe only to put some disclaimer about that for those who like to sue company for mot specifically saying that cat can not be put in microwave oven to dry? :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that Hibernate will happily accept non-ASCII letters in entity (and/or class) names (PostgreSQL even in column names), so only it should be not big "sacrifice" to not allow special characters

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be used if quoted.

Ahhh uuhmmm, yeah, OK sure. I forgot HQL has quoted identifiers (JPQL doesn't).

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from 255abee to 68552a8 Compare November 22, 2024 06:27
@cigaly
Copy link
Contributor Author

cigaly commented Nov 22, 2024

Code changes to generate metadata for inner class inside other metadata class instead of new metadata class file for each managed class.

Note: This code will surely need some (beauty and other) changes.

@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from b9a0bcf to 3a5dd6b Compare November 22, 2024 14:44
return metamodel;
}

protected void init() {

Check notice

Code scanning / CodeQL

Missing Override annotation Note

This method overrides
AnnotationMetaEntity.init
; it is advisable to add an Override annotation.
@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch 3 times, most recently from 11a8adc to b481c7f Compare November 23, 2024 22:09
@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch 3 times, most recently from 78e1fce to 121c85e Compare November 24, 2024 13:12
Copy link
Member

@gavinking gavinking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've tried this out and it looks like it's working great!

@cigaly could you do me the favor of squashing it down to two or three commits so that I can merge it?

Thanks!!!

cigaly and others added 3 commits December 4, 2024 17:35
          Test case for Jakarta Data processing with inner classes
          Changed existing test class to properly check generated metamodel class
          @Exclude-ing "troublemakers" with illegal URI character(s) in entity name
          Generated metadata for inner class A.B is A_.B_
          Path source for inner class is identical to path source for enclosing class
… non-private classes

          Generaed metadate class for inner class A.B is A_.B_
@cigaly cigaly force-pushed the HHH-18693-Hibernate_processor-Inner_classes branch from 121c85e to 5a6ab87 Compare December 4, 2024 16:51
@gavinking gavinking merged commit d3a1ebd into hibernate:main Dec 4, 2024
22 of 23 checks passed
@gavinking
Copy link
Member

Thanks so much @cigaly, this is great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants