Skip to content

Conversation

@leonhartX
Copy link
Member

  • add enableJSR305NullCheck (default false) to add Nullable and Nonnull annotation for code check
  • add nullable field in @Column annotation
  • some minor stream api refactoring

@leonhartX leonhartX requested a review from seratch March 4, 2019 04:33
Signed-off-by: leonhartX <ke.xu@smartnews.com>
@seratch
Copy link
Collaborator

seratch commented Mar 4, 2019

Thank you for the great work 👍

I thought that there are several other options to design the config.

Naming

I have a little bit different idea on naming of the new attribute in config. I came up with two options.

Suggestion 1) jsr305AnnotationsRequired

If we go with a single boolean value for this, I prefer the following name in terms of the consistency with jpa1SupportRequired.

Naming consistency is not the only reason why I think my suggestion may be better. In my understanding, the configuration just means adding necessary annotations for null checker. Enabling the checker would be done by other configuration in the projects using this library.

# If you'd like to have JSR-305 annotations in entity classes as well, set the following attribute
#   - boolean value: true if you need to generate Nonnull/Nullable annotation-wired entities as well
jsr305AnnotationsRequired: true

Suggestion 2) enabledJsr305Annotations

Another option may be an excessive work. In general, if we take possible future enhancements into account, using a flag value may lack extensibility. So, specifying the class names to use as below may be fine. With this design, we can add addtional annotations when some users really need it.

# If you'd like to have JSR-305 annotations in entity classes as well, set the following attribute
#   - array of String values: possible values are "Nonnull", "Nullable"
enabledJsr305Annotations: [
  "Nonnull",
  "Nullable"
]

However, I personally don't think such enhancements will be necessary in the future even though I am not an expert on JSR-305 and JSR-308.

In addition, if we really need such extensibility someday, it's also possible to mark the existing flag value as deprecated and introduce new ones at the right time.

So, I would like to suggest to choose Suggestion 1). Could you work on the chagnes if you like the naming? Otherwise, I am fine with your original one, too. You and your team are already the owner of this library. I will respect your decision.

Other Requests

Have a new test for this

If you have time, having a new test for this configuration in CodeGeneratorTest would be better. But it's okay if you skip it. I can work on it when I have time.

Add the new attribute to entityGenConfig.yml examples

Once you decide the name of the new attribute, please add the attribute to the following entityGenConfig.yml. This one is referred from README as an example config file.

Also, having some explanation on it as below would be preferrable.

# If you'd like to have JSR-305 annotations in entity classes as well, set the following attribute
#   - boolean value: true if you need to generate Nonnull/Nullable annotation-wired entities as well
jsr305AnnotationsRequired: true

Thank you for reading this!

@seratch
Copy link
Collaborator

seratch commented Mar 4, 2019

Once you finish working on this, please feel free to ping me.

I'm willing to do re-review (I've already approved, my review is no longer required) and work on new version releases! Of course, it's also fine if you could request Sonatype to have your account in com.smartnews groupId.

@leonhartX
Copy link
Member Author

Thanks, I prefer option 1 too, will take some time to fix.

@leonhartX
Copy link
Member Author

I'm writing tests for this, and found that the current implementation will add the annotation to primitive type either (which is slightly illegal).
So I'm thinking to support a primitive feature like:

  • check whether the type is primitive and store it in Field for future usage.
  • automatically use primitive type for non-null fields instead of the boxing type.

what's your opinion about this?

@leonhartX leonhartX changed the title support JSR305 annotation [WIP]support JSR305 annotation Apr 8, 2019
@leonhartX leonhartX requested a review from kainoku April 8, 2019 02:21
@seratch
Copy link
Collaborator

seratch commented Apr 8, 2019

@leonhartX Your idea looks good to me. 👍

Signed-off-by: leonhartX <ke.xu@smartnews.com>
Signed-off-by: leonhartX <ke.xu@smartnews.com>
@leonhartX
Copy link
Member Author

When adding tests, the createdAt column of Blog has been reported to be a non null field.
But I checked the database schema and seems it's nullable (actually has null values), do you have any idea about this? @seratch
Maybe just some issue about H2 database...

P.S: I checked both NULLABLE and IS_NULLABLE from the DatabaseMetaData, both said the column is non null.

@seratch
Copy link
Collaborator

seratch commented Apr 8, 2019

Are you talking about this column? It seems to be a non-null column. https://github.com/smartnews/jpa-entity-generator/blob/v0.99.5/src/test/java/com/example/unit/DatabaseUtil.java#L23

@leonhartX
Copy link
Member Author

Yes, so how can this test passed without setting the createAt field?
https://github.com/smartnews/jpa-entity-generator/blob/master/sample/src/test/java/com/example/RepositoryTest.java#L29

@seratch
Copy link
Collaborator

seratch commented Apr 8, 2019

hmm, I have been familiar with h2's imperfect behaviers but this one is new to me. We all expect the insertion queries in the following code example result in errors but the reality is that h2 database silently inserts current_timestamp for us. Anyway, the metaedata API seems to be working correctly.

// libraryDependencies += "com.h2database" % "h2" % "1.4.199"

import java.sql._
Class.forName("org.h2.Driver")
val conn = DriverManager.getConnection("jdbc:h2:file:./sample/db/blog;MODE=MySQL;TRACE_LEVEL_FILE=2;TRACE_LEVEL_SYSTEM_OUT=2", "USER", "")
val stmt = conn.createStatement()
stmt.executeUpdate("""
create table blog (id integer primary key auto_increment not null, name varchar(30), active tinyint default 0, created_at timestamp not null)
""")

val stmt = conn.createStatement()
stmt.executeUpdate("""insert into blog (name, created_at) values ('tech blog', null);""")
stmt.executeUpdate("""insert into blog (name, created_at) values ('corporate blog', now());""")

val rs = stmt.executeQuery("select * from blog")
while(rs.next()) {
  println(rs.getInt("ID") + ", " + rs.getString("NAME") + ", " + rs.getTimestamp("CREATED_AT"))
}

val meta = conn.getMetaData
val rs = meta.getColumns(null, "", "blog", "%")
while(rs.next()) {
  println(rs.getString("COLUMN_NAME") + " " + rs.getString("IS_NULLABLE"))
}
scala> import java.sql._
import java.sql._

scala> Class.forName("org.h2.Driver")
res0: Class[_] = class org.h2.Driver

scala> val conn = DriverManager.getConnection("jdbc:h2:file:./sample/db/blog;MODE=MySQL;TRACE_LEVEL_FILE=2;TRACE_LEVEL_SYSTEM_OUT=2", "USER", "")
2019-04-08 21:21:28 database: opening /Users/kazuhirosera/tmp/jdbc/sample/db/blog (build 199)
2019-04-08 21:21:29 database: opened /Users/kazuhirosera/tmp/jdbc/sample/db/blog
2019-04-08 21:21:29 database: connecting session #3 to /Users/kazuhirosera/tmp/jdbc/sample/db/blog
2019-04-08 21:21:29 jdbc[3]: 
/*SQL */SET TRACE_LEVEL_SYSTEM_OUT 2;
2019-04-08 21:21:29 jdbc[3]: 
/*SQL */SET TRACE_LEVEL_FILE 2;
2019-04-08 21:21:29 jdbc[3]: 
/**/Connection conn0 = DriverManager.getConnection("jdbc:h2:file:./sample/db/blog;MODE=MySQL;TRACE_LEVEL_FILE=2;TRACE_LEVEL_SYSTEM_OUT=2", "USER", "");
conn: java.sql.Connection = conn0: url=jdbc:h2:file:./sample/db/blog user=USER

scala> val stmt = conn.createStatement()
stmt: java.sql.Statement = stat0

scala> stmt.executeUpdate("""
     | create table blog (id integer primary key auto_increment not null, name varchar(30), active tinyint default 0, created_at timestamp not null)
     | """)
2019-04-08 21:21:34 jdbc[3]: 
/*SQL t:9*/create table blog (id integer primary key auto_increment not null, name varchar(30), active tinyint default 0, created_at timestamp not null);
res1: Int = 0

scala> val stmt = conn.createStatement()
stmt: java.sql.Statement = stat1

scala> stmt.executeUpdate("""insert into blog (name, created_at) values ('tech blog', null);""")
2019-04-08 21:21:43 jdbc[3]: 
/*SQL #:1 t:2*/insert into blog (name, created_at) values ('tech blog', null);
res2: Int = 1

scala> stmt.executeUpdate("""insert into blog (name, created_at) values ('corporate blog', now());""")
2019-04-08 21:21:48 jdbc[3]: 
/*SQL #:1*/insert into blog (name, created_at) values ('corporate blog', now());
res3: Int = 1

scala> val rs = stmt.executeQuery("select * from blog")
2019-04-08 21:21:52 jdbc[3]: 
/*SQL #:2 t:1*/select * from blog;
rs: java.sql.ResultSet = rs0: org.h2.result.LocalResultImpl@630777f4 columns: 4 rows: 2 pos: -1

scala> while(rs.next()) {
     |   println(rs.getInt("ID") + ", " + rs.getString("NAME") + ", " + rs.getTimestamp("CREATED_AT"))
     | }
1, tech blog, 2019-04-08 21:21:43.024
2, corporate blog, 2019-04-08 21:21:48.164

scala> 

scala> val meta = conn.getMetaData
meta: java.sql.DatabaseMetaData = dbMeta0: conn0: url=jdbc:h2:file:./sample/db/blog user=USER

scala> val rs = meta.getColumns(null, "", "blog", "%")
2019-04-08 21:21:57 jdbc[3]: 
/*SQL l:2534 #:4 t:18*/SELECT TABLE_CAT, TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE, TYPE_NAME, COLUMN_SIZE, BUFFER_LENGTH, DECIMAL_DIGITS, NUM_PREC_RADIX, NULLABLE, REMARKS, COLUMN_DEF, SQL_DATA_TYPE, SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE, SCOPE_CATALOG, SCOPE_SCHEMA, SCOPE_TABLE, SOURCE_DATA_TYPE, IS_AUTOINCREMENT, IS_GENERATEDCOLUMN FROM (SELECT s.SYNONYM_CATALOG TABLE_CAT, s.SYNONYM_SCHEMA TABLE_SCHEM, s.SYNONYM_NAME TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.TYPE_NAME, c.CHARACTER_MAXIMUM_LENGTH COLUMN_SIZE, c.CHARACTER_MAXIMUM_LENGTH BUFFER_LENGTH, c.NUMERIC_SCALE DECIMAL_DIGITS, c.NUMERIC_PRECISION_RADIX NUM_PREC_RADIX, c.NULLABLE, c.REMARKS, c.COLUMN_DEFAULT COLUMN_DEF, c.DATA_TYPE SQL_DATA_TYPE, ZERO() SQL_DATETIME_SUB, c.CHARACTER_OCTET_LENGTH CHAR_OCTET_LENGTH, c.ORDINAL_POSITION, c.IS_NULLABLE IS_NULLABLE, CAST(c.SOURCE_DATA_TYPE AS VARCHAR) SCOPE_CATALOG, CAST(c.SOURCE_DATA_TYPE AS VARCHAR) SCOPE_SCHEMA, CAST(c.SOURCE_DATA_TYPE AS VARCHAR) SCOPE_TABLE, c.SOURCE_DATA_TYPE, CASE WHEN c.SEQUENCE_NAME IS NULL THEN CAST(?1 AS VARCHAR) ELSE CAST(?2 AS VARCHAR) END IS_AUTOINCREMENT, CASE WHEN c.IS_COMPUTED THEN CAST(?2 AS VARCHAR) ELSE CAST(?1 AS VARCHAR) END IS_GENERATEDCOLUMN FROM INFORMATION_SCHEMA.COLUMNS c JOIN INFORMATION_SCHEMA.SYNONYMS s ON s.SYNONYM_FOR = c.TABLE_NAME AND s.SYNONYM_FOR_SCHEMA = c.TABLE_SCHEMA WHERE s.SYNONYM_CATALOG LIKE ?3 ESCAPE ?7 AND s.SYNONYM_SCHEMA LIKE ?4 ESCAPE ?7 AND s.SYNONYM_NAME LIKE ?5 ESCAPE ?7 AND c.COLUMN_NAME LIKE ?6 ESCAPE ?7 UNION SELECT TABLE_CATALOG TABLE_CAT, TABLE_SCHEMA TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE, TYPE_NAME, CHARACTER_MAXIMUM_LENGTH COLUMN_SIZE, CHARACTER_MAXIMUM_LENGTH BUFFER_LENGTH, NUMERIC_SCALE DECIMAL_DIGITS, NUMERIC_PRECISION_RADIX NUM_PREC_RADIX, NULLABLE, REMARKS, COLUMN_DEFAULT COLUMN_DEF, DATA_TYPE SQL_DATA_TYPE, ZERO() SQL_DATETIME_SUB, CHARACTER_OCTET_LENGTH CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE IS_NULLABLE, CAST(SOURCE_DATA_TYPE AS VARCHAR) SCOPE_CATALOG, CAST(SOURCE_DATA_TYPE AS VARCHAR) SCOPE_SCHEMA, CAST(SOURCE_DATA_TYPE AS VARCHAR) SCOPE_TABLE, SOURCE_DATA_TYPE, CASE WHEN SEQUENCE_NAME IS NULL THEN CAST(?1 AS VARCHAR) ELSE CAST(?2 AS VARCHAR) END IS_AUTOINCREMENT, CASE WHEN IS_COMPUTED THEN CAST(?2 AS VARCHAR) ELSE CAST(?1 AS VARCHAR) END IS_GENERATEDCOLUMN FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_CATALOG LIKE ?3 ESCAPE ?7 AND TABLE_SCHEMA LIKE ?4 ESCAPE ?7 AND TABLE_NAME LIKE ?5 ESCAPE ?7 AND COLUMN_NAME LIKE ?6 ESCAPE ?7) ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION {1: 'NO', 2: 'YES', 3: '%', 4: 'PUBLIC', 5: 'blog', 6: '%', 7: '\\'};
rs: java.sql.ResultSet = rs1: org.h2.result.LocalResultImpl@6d351180 columns: 24 rows: 4 pos: -1

scala> while(rs.next()) {
     |   println(rs.getString("COLUMN_NAME") + " " + rs.getString("IS_NULLABLE"))
     | }
ID NO
NAME YES
ACTIVE YES
CREATED_AT NO

@leonhartX
Copy link
Member Author

I see, so I'll just set the createdAt field to fix these tests.

Signed-off-by: leonhartX <ke.xu@smartnews.com>
@seratch
Copy link
Collaborator

seratch commented Apr 20, 2019

It seems to be good enough. If you're ready to merge this, please feel free to merge this. Also, if you would like me to work on a new version release, I'm willing to work on it.

@leonhartX leonhartX changed the title [WIP]support JSR305 annotation support JSR305 annotation Apr 21, 2019
@leonhartX leonhartX merged commit 5a19369 into master Apr 21, 2019
@leonhartX leonhartX deleted the nullable-annotation branch April 21, 2019 23:53
@leonhartX
Copy link
Member Author

Merged the PR but still not sure how to release (I'm requesting the permission on sonatype), so really appreciate if you could do a release this time. 🙇

@seratch
Copy link
Collaborator

seratch commented Apr 22, 2019

Will do.

@seratch
Copy link
Collaborator

seratch commented Apr 22, 2019

I just published the following jars. They'll be automatically synced with Maven Central artifacts shortly.

The operations are easy. Running two commands + Clicking buttons several times on Sonatype management console.
https://github.com/smartnews/jpa-entity-generator/blob/master/README.md#how-to-release-new-version

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.

3 participants