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

Support for java.time #159

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open

Support for java.time #159

wants to merge 14 commits into from

Conversation

graynk
Copy link
Contributor

@graynk graynk commented Dec 11, 2018

Hopefully closes #154

Adds support for Java 8 Time API (JSR-310), specifically for LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime and Instant.

Default persisters use JDBC 4.2 API and pass values without conversion.

For DBs that don't fully support 4.2 (H2 and PostgreSQL don't support TIME WITH TIME ZONE, but support TIMESTAMP WITH TIME ZONE) there's a secondary persister for OffsetTime that converts OffsetTime to OffsetDateTime with date part fixed at epoch. There's an overriden FieldConverter for those DBs in ormlite-jdbc, but these persisters can also be enabled by specifying DataType in annotation of the Dao class.
Instant is not a part of 4.2 API, so it converts to/from OffsetDateTime.
For LocalDate|Time classes there are also secondary persisters, that convert java.time values to java.sql values using methods, added in Java 8. These are, again, forced in overriden 'FieldConverter's for those DBs in ormlite-jdbc and can also be enabled by specifying DataType. Someone with a better knowledge of DBs than me should take a look at those, since finding anything concrete in the documentation for all those different DBs is a pain (I presume DB2 and Netezza don't support 4.2, but I'm still not sure. Same for Sqlite).

For every persister there's a check in getSingleton() method that returns null, if there's no corresponding java.time class in classpath to allow compiling for older JDKs. No actual testing was done for that though.

I am not entirely clear on purpose of isArgumentHolderRequired() and moveToNextValue() methods of BaseData, so I aped those off of DateType (aped the tests as well).

New tests will fail for H2 1.2.128, since that version doesn't support 4.2 yet. For the latest H2 (1.4.197) they pass. However, old tests begin to fail, due to some changes in API, though this does not affect this PR, and I will create a separate issue for that.

@graynk
Copy link
Contributor Author

graynk commented Dec 11, 2018

Thanks for this. How is backwards compatibility achieved? Does this force ORMLite to Java 8?

I got an email with this reply, but it no longer seems to be here. I'll reply anyway. The idea for backwards compatibility was returning null in getSingleton methods, when not being able to find java.time classes, so Java 8 is required to compile, but not to run ORMLite.

However, I just got around to actually testing it on Java 1.6 and found out, that I totally botched it, by initializing static singleton at declaration (duh). Lazily initializing it after checking if java.time class exists on classpath seems to work as intended, though that makes the singleton not final, and as such - a rather poor singleton. The constructor is still private and the field is not reassigned anywhere, so I hope it's okay. I will push a commit that fixes this a bit later, sorry.

Copy link
Collaborator

@Bo98 Bo98 left a comment

Choose a reason for hiding this comment

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

Generally seems okay to me.

A few minor points I'll make:

  1. Seems like some of copy-pasting between the classes could be reduced by using inheritance. Particularly the java.sql type versions which could just be wrappers of the superclass.
  2. Is there a reason why LocalTime has nanosecond precision in parseDefaultString but all the other types only have millisecond precision?
  3. A few of the class-level Javadocs refer to the wrong class.
  4. I have my concerns about whether the JDBC 4.2 version or the _SQL type would be the default persister. Seems at first glance to be purely defined by what appears last in DataType.java. Maybe the _SQL persisters should omit the default classes list in the constructor, similar to how UUID_NATIVE behaves? Or at very least leave a comment not to rearrange the order of DataType if you prefer to keep it as it currently is. Still seems dangerous if something calls the singleton before that file does so I think modifying the constructor is safer.

return OffsetDateTime.parse(defaultStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]x"));
} catch (NumberFormatException e) {
throw SqlExceptionUtil.create("Problems with field " + fieldType +
" parsing default LocalDateTime value: " + defaultStr, e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should say "Instant".

return LocalTime.parse(defaultStr, DateTimeFormatter.ofPattern("HH:mm:ss[.SSSSSS]"));
} catch (NumberFormatException e) {
throw SqlExceptionUtil.create("Problems with field " + fieldType +
" parsing default LocalDateTime value: " + defaultStr, e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should say "LocalTime".

return OffsetDateTime.parse(defaultStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]x"));
} catch (NumberFormatException e) {
throw SqlExceptionUtil.create("Problems with field " + fieldType +
" parsing default LocalDateTime value: " + defaultStr, e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should say "OffsetDateTime".

return OffsetDateTime.parse(defaultStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]x"));
} catch (NumberFormatException e) {
throw SqlExceptionUtil.create("Problems with field " + fieldType +
" parsing default LocalDateTime value: " + defaultStr, e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should say "OffsetTime".

}
return singleton;
}
private LocalDateTimeSqlType() { super(SqlType.DATE, new Class<?>[] { LocalDateTime.class }); }
Copy link
Collaborator

Choose a reason for hiding this comment

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

It doesn't matter by default, but maybe this should be LOCAL_DATE_TIME to be consistent?

* {@link #OFFSET_TIME} so you will need to specify this using {@link DatabaseField#dataType()}.
*
*/
OFFSET_TIME_SQL(OffsetTimeSqlType.getSingleton()),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the name for this is a little confusing since we are not storing this as a java.sql type.

Also, I think the Javadocs for the *_SQL types could be expanded to actually describe what's different about them.

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 agree, this is true for all *_SQL types, but I don't know how to name them differently.

Copy link
Collaborator

Choose a reason for hiding this comment

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

this is true for all *_SQL types

Well the only one called that right now is SQL_DATE and for good reason.

Types more like this situation is DATE_LONG, DATE_INTEGER, BOOLEAN_INTEGER, etc. I'm not sure what the best name is. OFFSET_TIME_TIMESTAMP? Does that sound clumsy?

I'd still modify the Javadoc for these enums to explain their purpose either way.

return OffsetTime.parse(defaultStr, DateTimeFormatter.ofPattern("HH:mm:ss[.SSS]x"));
} catch (NumberFormatException e) {
throw SqlExceptionUtil.create("Problems with field " + fieldType +
" parsing default LocalDateTime value: " + defaultStr, e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should say "OffsetTime".

@graynk
Copy link
Contributor Author

graynk commented Dec 13, 2018

  1. I've added inheritance to reduce code duplication, but I wouldn't call *_SQL types wrappers. After all, I still have to override most of the methods.
  2. I've added same precision to OffsetTime, but as for *_DateTimes - this is the same reason why old DateTypeTest fails on H2 1.4.197. For me, LocalDateTime.now() on Java 8 returns with precision of 3 on Linux machine (but with precision of 6 on Java 11, strangely enough). So DateTimeFormatter formats it with trailing zeroes. However, getString() in new version of H2 returns the string without trailing zeroes, and the formatter fails. I guess I could do .SSS[SSS], but I'm not sure how to handle this issue properly, to be honest. I had to truncate LocalTime to seconds as well, but OffsetTime works no problem. It's a mess.
  3. Should be fixed now as well
  4. I wanted to handle this the same way that DateType right now is used by default and DateStringType and others can be used if needed, but I couldn't find what made DateType special, so I used order of enums. I'd like to do that properly, if you point me in the right direction. UPD: ah, I see what you mean about omitting the class from constructor. Will do.

There's another point I'd like to discuss. What are the downsides of dropping Class.forName() approach, and instead using ternary operator, i.e.
private final static LocalDateType singleton = Double.parseDouble(System.getProperty("java.specification.version")) < 1.8 ? null : new LocalDateType()
The upsides are: singleton is back to being final singleton, it's evaluated once at the start, it works on 1.6, we don't need to check for null on every getSingleton() call and there's no need to throw 10 exceptions every time we launch.
The downsides that I see:

  1. Java radically changes version numbering in the future
  2. Property gets removed or changes, so that it doesn't parse to Double anymore
  3. It's ugly
  4. Something else?

@Bo98
Copy link
Collaborator

Bo98 commented Dec 13, 2018

  1. I've added inheritance to reduce code duplication, but I wouldn't call *_SQL types wrappers. After all, I still have to override most of the methods.

Well you could do things like Date.valueOf((LocalDate)super.parseDefaultString(...)). But yeah, I understand there's only so much that can be done.

For me, LocalDateTime.now() on Java 8 returns with precision of 3 on Linux machine (but with precision of 6 on Java 11, strangely enough).

Interesting. It's documented to have nanosecond precision.

  1. Should be fixed now as well

Did I miss these changes? The class Javadoc for InstantType still refers to LocalDate, for example.

What are the downsides of dropping Class.forName() approach, and instead using ternary operator, i.e.
private final static LocalDateType singleton = Double.parseDouble(System.getProperty("java.specification.version")) < 1.8 ? null : new LocalDateType()

Personally, I prefer feature checking over version checking but others may disagree with me on that.

To answer your points specifically:

Java radically changes version numbering in the future

It has, but it didn't break your check.

Property gets removed or changes, so that it doesn't parse to Double anymore

I think removal is unlikely. It might change, but I think they'd be cautious about it.

It's ugly

Yep. Java feature & version checking in general is a bit ugly.


I've also replied to your *_SQL comment above.

@graynk
Copy link
Contributor Author

graynk commented Dec 13, 2018

Interesting. It's documented to have nanosecond precision.

Quoting the docs for Clock class,

The system factory methods provide clocks based on the best available system clock This may use System.currentTimeMillis(), or a higher resolution clock if one is available.

As this answer on SO put it, the classes are capable of carrying the nanoseconds, but not capturing it. This was changed in Java 9 to microseconds (but still not nano though).

Did I miss these changes? The class Javadoc for InstantType still refers to LocalDate, for example.

Oh, for the love of.. Yeah, I missed it, I changed just the text of the Exceptions, not the javadocs. I'll push it in the next commit, along with clearer explanations for *_SQL types.

Personally, I prefer feature checking over version checking but others may disagree with me on that.

I understand that, but this is not really an exceptional situation, it's a regular check (one that will always throw an exception for each class on older JDKs at that), so IMO it should be an actual if check. Problem is: I don't know a way to check for class existence without throwing an exception. This is the main thing I'm trying to avoid.

It has, but it didn't break your check.

True. But they could use something like 2018.3.1, for example (or add letters), I don't know.

I've also replied to your *_SQL comment above.

I mean, OFFSET_TIME_TIMESTAMP is sort of okay, but then there's LOCAL_DATE_DATE and LOCAL_TIME_TIME and that's just plain confusing. What about adding _TO_ in-between?

@Bo98
Copy link
Collaborator

Bo98 commented Dec 13, 2018

I mean, OFFSET_TIME_TIMESTAMP is sort of okay, but then there's LOCAL_DATE_DATE and LOCAL_TIME_TIME and that's just plain confusing. What about adding TO in-between?

If you wanted to keep _SQL for the others I wouldn't have too much of an issue since the purpose of them is to use java.sql. The underlying SQL type itself is unchanged - LOCAL_DATE_DATE could apply to either class. If you wanted to change it, the only other things I would think would be suitable are _JAVA_SQL, _COMPAT, _LEGACY or similar.

It's just OFFSET_TIME_SQL that I didn't think fit its name since it doesn't use java.sql but rather its purpose is to change the underlying SQL type from TIME to TIMESTAMP.

If you think adding _TO_ would help then sure.

@graynk
Copy link
Contributor Author

graynk commented Dec 14, 2018

In the end I made the call to do a single static check for Java specification version. My reasoning: our target is 1.6 and java.time became available in Java 8. Judging by the fact that java.util.Date is still around, java.time packages won't be deprecated for the foreseeable future. So, we need to check only that specification string is not equal to 1.6 or 1.7, no need to convert version to Double and so possible change in version numbering won't affect this. I think this is a better approach than Class.forName(), for reasons I explained above (Effective Java, Item 57, basically), even if it's not ideal. Of course, it's up to maintainers to make a final call.

I've expanded a bit on javadocs for the new classes (and hopefully fixed all the wrong references), and renamed OffsetTime to OffsetTimeCompat. The problem with formatting time with 0/3/6 precision in *_Time tests is still in the air, but it concerns only conversion from Strings which shouldn't happen anyway, not sure why it's there.

Ideally this should also include new paragraphs to HTML tutorials, but this can be added later in a separate PR.

Copy link
Collaborator

@Bo98 Bo98 left a comment

Choose a reason for hiding this comment

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

Leaving the tests in a broken state by not updating H2 is something that can't be ignored.

I didn't have to adjust too much to get it to almost pass: Bo98@9b93989.

There are a couple of test failures even after the above. However, those are a part of bigger problems around fetching of generated IDs. A problem that is already affecting PostgreSQL, and now seemingly H2. I have a PR ready to fix it and will link it here shortly.

@Override
public Object parseDefaultString(FieldType fieldType, String defaultStr) throws SQLException {
try {
return Timestamp.valueOf(LocalDateTime.parse(defaultStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]")));
Copy link
Collaborator

@Bo98 Bo98 Dec 17, 2018

Choose a reason for hiding this comment

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

This caused problems in the tests for me. A better approach here (and ultimately more user friendly) would be variable length second fractions.

An example of this is here: Bo98@00fed82, along with adjustments to the tests to make sure they are consistently truncated (H2 supports milliseconds but the tests were comparing it to microseconds on my machine).

@Bo98
Copy link
Collaborator

Bo98 commented Dec 17, 2018

#165 for the additional H2 fixes.

@Override
public Object parseDefaultString(FieldType fieldType, String defaultStr) throws SQLException {
try {
return OffsetDateTime.parse(defaultStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]x"));
Copy link
Collaborator

@Bo98 Bo98 Dec 19, 2018

Choose a reason for hiding this comment

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

I've been building tests over on the JDBC side so sorry for not catching everything at once.

This here is going to cause problems as it is incompatible with regular OffsetTimeType.

If we did @DatabaseField(defaultValue = "01:23:45+0100") on OffsetTime, it would work everywhere except Postgres or H2 which uses the compat persister instead.

A better solution here would be to make the date part optional, parse it into an OffsetTime and then convert to OffsetDateTime.

Example: Bo98@bd8ee75 (not tested everywhere but seems to work)

Alternatively, the date can be removed entirely from the parser but you'll then need to also implement resultStringToJava to parse the date part there.

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 believe I've integrated all of your fixes now

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'll have a look come the end of the week when I'm back home.

@mpe85
Copy link

mpe85 commented Apr 18, 2019

Are there any news on this?

@graynk
Copy link
Contributor Author

graynk commented Apr 19, 2019

Are there any news on this?

Well, I'm using a build with these changes in my project with H2 and it works for me, the tests are passing as well. There were some problematic DBs that don't follow JDBC standard and we didn't seem to really settle on a final approach for cleanly dealing with them, so whether this commit is ready for general use is not for me to decide.

@maximillian2
Copy link

Any chance of this to be merged? @j256

@j256
Copy link
Owner

j256 commented Jun 24, 2019

Yeah I need to work on this. Portability is the worry. I'll look at it this week.

@BomBardyGamer
Copy link

Hey, know this is a bit old now, but any updates on this?

@j256
Copy link
Owner

j256 commented Jan 12, 2021

Right now I've not forced the library to go to Java 8 which is the holdup. Yes I know Java 7 is ancient but I'm always reluctant to upgrade my libraries.

@noordawod
Copy link
Collaborator

Right now I've not forced the library to go to Java 8 which is the holdup. Yes I know Java 7 is ancient but I'm always reluctant to upgrade my libraries.

Well, Java 8 is about to be deprecated so I would say it's a good time to upgrade ;)

@BomBardyGamer
Copy link

BomBardyGamer commented Jan 21, 2021

Could we say get this merged into a different branch or something? Say, as a dev build, just so we can use it? Because I kinda really need this. Or you could just do what Exposed did and make this a separate module (e.g. ormlite-java-time) maybe?

Java 8 has already reached the end of LTS by the way, so Java 7 is even further out of support range.

@noordawod
Copy link
Collaborator

I like the idea of a separate module, what do you think @j256? Possible?

@j256
Copy link
Owner

j256 commented Jan 29, 2021

I'd rather not do a whole module for this although maybe that would be easier than the reflection hack alternatives.

@perlun
Copy link

perlun commented Nov 12, 2021

For the record, while waiting on this PR to get finalized, it's quite possible to add support for e.g. java.time.Instant on your own by using code from this branch. You can then register it using e.g. DataPersisterManager.registerDataPersisters(InstantType.getSingleton()).

I could extract the code I have to a GitHub gist/public repo in case anyone's interested. Note: it only supports Instant at the moment, which gets stored as a TIMESTAMP WITHOUT TIME ZONE in my case - using Postgres. Supporting more databases/different field types is probably harder without getting some of this merged, since that would presumably rely on the SqlType support which this PR adds. Here's how I did it in my local implementation for now:

    @Override
    public String getSqlOtherType() {
        return "TIMESTAMP WITHOUT TIME ZONE";
    }

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.

Any plans to support Java 8 Time API?
8 participants