11 kwietnia 2007

Adnotacja @Column i elementy precision oraz scale

Sherkan zadał ciekawe pytanie na grupie pl.comp.lang.java - EJB3.0 precyzja double. Poczatkowo wydawało się, że jest to po prostu pytanie, które wymaga doczytania specyfikacji Java Persistence API (a dokładniej części specyfikacji EJB 3.0 - JSR 220: Enterprise JavaBeans,Version 3.0 Java Persistence API, którą relacjonuję od pewnego czasu) i...wysłać odpowiedź. Podjąłem się wyzwania doczytania specyfikacji i ku mojemu zdziwieniu, okazało się, że pytanie nie należało do trywialnych (jak i pozostałe od sherkana). Tkwiło tam kilka ciekawych informacji dotyczących specyfikacji Java Persistence (JPA).

Zacznijmy od początku.

W specyfikacji JPA (rozdział 2.1.6 Mapping Defaults for Non-Relationship Fields or Properties strona 23, którego relację przedstawiłem we wpisie Java Persistence - Rozdział 2 Entities - kontynuacja lektury rozdziału) zapisano (po moim przetłumaczeniu):

Jeśli typ atrybutu (pola lub właściwości) encji należy do wymienionych typów, wtedy pole traktowane jest jakby było udekorowane adnotacją @Basic:
  • typy podstawowe w Javie
  • typy opakowujące typy podstawowe
  • java.lang.String
  • java.math.BigInteger
  • java.math.BigDecimal
  • java.util.Date
  • java.util.Calendar
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp
  • byte[]
  • Byte[]
  • char[]
  • Character[]
  • typy wyliczeniowe (enums)
  • typy implementujące interfejs Serializable
Specyfikacja odwołuje się dalej do rozdziałów 9.1.18 - 9.1.21. W rozdziale 9.1.18 Basic Annotation czytamy:

Adnotacja @Basic jest najprostszym sposobem mapowania do kolumny w tablicy. Wymienione są wspomniane wyżej typy dla atrybutów trwałych, które mogą być udekorowane adnotacją @Basic. Użycie adnotacji @Basic jest opcjonalne.

Adnotacja @Basic nie prowadzi nas do żadnej odpowiedzi na pytanie sherkana, której szukać należy w innej adnotacji - @Column.

Adnotacja @Column opisana jest w rozdziale 9.1.5 Column Annotation, gdzie można przeczytać:

Adnotacja @Column jest używana do określenia kolumny i jej właściwości dla atrybutu trwałego.

Adnotacja @Column składa się z wielu elementów, z których na uwagę (w celu udzielenia odpowiedzi sherkanowi) zasługują następujące:
  • String columnDefinition() default "";
  • int precision() default 0;
  • int scale() default 0;
3 wyżej wymienione elementy uściślają mapowanie między klasą encji a bazą danych. Należy rozważyć kilka przypadków ze względu na istnienie schematu bazodanowego odpowiadającego encji - struktury bazodanowe już istnieją, będą tworzone podczas uruchamiania aplikacji bądź będą modyfikowane na potrzeby aplikacji. Mimo, że specyfikacja wyraźnie nadmienia (rozdział 9 Metadata for Object/Relational Mapping strona 163), że:

Mechanizm tworzenie skryptów DDL jest opcjonalną funkcjonalnością dostawcy trwałości. W celu zachowania pełnej zgodności ze specyfikacją aplikacja nie powinna polegać na niej.

Mechanizm generowania DDL stał się de facto standardem i każdy dostawca trwałości udostępnia go poprzez konfigurację jednostki utrwalania w persistence.xml (plik konfiguracyjny dostawcy trwałości).

Zajmijmy się przypadkiem, kiedy schemat bazy danych jest tworzony (bądź modyfikowany) na podstawie modelu obiektowego. Przypadek istnienia schematu bazy danych nie jest interesujący w tym kontekście, gdyż wszystkie cechy tabel są już ustalone.

Problem: Chcielibyśmy, aby liczby zmiennoprzecinkowe były zapisywane w postaci - 5 cyfr całkowitych (przed przecinkiem) oraz 2 cyfry ułamkowe (po przecinku), np. liczba 12345,6789 byłaby zapisana jako 12345,67.

Oczywiście chcielibyśmy, aby liczba 123456,789 powodowała pojawienie się wyjątku niezgodności zakresu, ale...należy przypomnieć sobie rolę JPA. JPA jest to most pomiędzy danymi obiektowymi a relacyjnymi i niczym więcej (może poza drobnymi dodatkami). Nie ma mowy o kontroli zakresów - nie jest to częścią JPA. Konwersja odbywa się albo na poziomie kodu w Javie (zgodność typu i ich zakresu), albo na poziomie bazy danych, gdzie następuje wprowadzenie danych spoza zakresu i ewentualne zgłoszenie wyjątku. JPA jedynie raportuje błędy, których nie była źródłem, bo pełni jedynie rolę pośrednika (jedynie jest uzasadnione jedynie w tym zdaniu ;-)).

Zanim przejdziemy do przykładów odszukajmy informacji dotyczących elementów adnotacji @Column:
  • int precision - (domyślnie 0) - element opcjonalny określający rozmiar kolumny liczbowej dla typu zmiennoprzecinkowego (o dokładnej wartości), tj. ilość cyfr reprezentujących całą liczbę łącznie z jej opcjonalną częścią ułamkową.
  • int scale - (domyślnie 0) - element opcjonalny określający ilość cyfr po przecinku dla typu zmiennoprzecinkowego (o dokładnej wartości)
  • String columnDefinition - (domyślnie "" - łańcuch pusty) - element opcjonalny określający fragment SQL, który tworzy definicję pola w skryptach tworzących schemat (DDL).
Na szczególną uwagę zasługuje przypis (a już miałem napisać adnotacja) - o dokładnej wartości. Nigdy nie zajmowałem się tymi zagadnieniami, więc przeszukałem Internet w celu znalezienia odpowiedzi co to są liczby o dokładnej wartości.

Dobre wytłumaczenie znalazłem na stronie dokumentacji bazy MySQL - 23.1. Types of Numeric Values i doczytałem, że liczby o dokładnej wartości to 3,14 podczas, gdy ta sama liczba zapisana w postaci 3,14E0 jest już liczbą przybliżoną. Ważne, aby pamiętać, że podanie liczby w postaci skończonego ciągu cyfr (możliwie z przecinkiem) jest liczbą dokładną, a jakakolwiek jej reprezentacja naukowa z mantysą i wykładnikiem jest już jej przybliżeniem.

Zostawiając naukowe wywody na boku, przypomnę o starej, dobrej specyfikacji, która pozwoliła na dostęp do bazy danych na początku pojawienia się tego problemu w Javie - Java Database Connectivity (JDBC). JDBC jest protoplastą JPA. Zmęczeni ciągłym operowaniem klasami JDBC, Hibernate spowodował, że połączenie świata obiektowego i relacyjnego stało się niezwykle proste. Po drodze mieliśmy nietrafioną specyfikację EJB 2.1 wraz z mapowaniem klas na ich reprezentację relacyjną - komponenty encyjne CMP, co przyczyniło się do migracji programistów do Hibernate i tym podobnych rozwiązań, aż w końcu dotarliśmy do EJB 3.0 z JPA. Trochę historii nie zaszkodzi, bo pozwala na zrozumienie aktualnych rozwiązań i odszukanie odpowiedzi. Wracamy, więc do korzeni, czyli JDBC. A dlaczego, ktoś zapyta? Bo podstawą dla działania JPA jest właśnie JDBC i zrozumienie JDBC to początek zrozumienia JPA (być może i na odwrót, ale nie polecam).

Odszukując informacji dotyczących mapowania typów bazodanowych na typy w Javie natrafiłem na dokument Mapping SQL and Java Types - część dokumentacji Java SE 5, w którym czytamy, że rekomendowanym typem w Javie dla typu bazodanowego DOUBLE jest double (8.3.9 DOUBLE) podczas, gdy dla DECIMAL lub NUMERIC będzie to java.math.BigDecimal (8.3.11 DECIMAL and NUMERIC).

Zrozumienie mapowania typów bazodanowych na odpowiadające im typy w Javie to zrozumienie połowy, jeśli nie całego, tematu. Pamiętając, że JPA stanowi jedynie pomost między modelem obiektowym a relacyjnym, a rolę kontrolera typu i zakresu danych przejmuje baza danych, koniecznie należałoby zapoznać się z możliwościami konkretnej bazy danych odnośnie wspomnianych typów bazodanowych - DECIMAL, NUMERIC oraz DOUBLE.

Rozpatrując bazę danych Apache Derby dokumentacja dotycząca wspieranych typów danych - Data types - prezentuje ich możliwości:
  • DECIMAL(precision [, scale]) jest typem o dokładnej wartości i odpowiadającym jej typem w Javie jest java.math.BigDecimal.
  • NUMERIC jest synonimem dla DECIMAL
  • DOUBLE jest synonimem dla DOUBLE PRECISION
  • DOUBLE PRECISION jest typem o przybliżonej wartości, dla której typem w Javie jest java.lang.Double.
Przeglądając dokumentację MySQL w rozdziale 11.2. Numeric Types czytamy opis podobny do dokumentacji Apache Derby, ale na uwagę zasługuje notka:

For maximum portability, code requiring storage of approximate numeric data values should use FLOAT or DOUBLE PRECISION with no specification of precision or number of digits.

, czyli stosowanie precyzji oraz skali dla liczb przybliżonych zmiennoprzecinkowych jest nierekomendowane oraz kolejna ciekawa adnotacja^H^H^Hnotka:

The DECIMAL and NUMERIC data types are used to store exact numeric data values. In MySQL, NUMERIC is implemented as DECIMAL. These types are used to store values for which it is important to preserve exact precision, for example with monetary data.

, co oznacza, że DECIMAL oraz NUMERIC są idealnymi kandydatami do przechowywania dokładnych wartości liczb zmiennoprzecinkowych, np. w zastosowaniach finansowych.

Zapoznając się z dokumentacją PostgreSQL czytamy w rozdziale 8.1. Numeric Types jest bardzo zbliżona do definicji typów w MySQL oraz Derby.

Istotne jest, aby pamiętać, że zaczynamy wkraczać na grunt nieustandaryzowany, tj. istnieje standard SQL-92, który jest standardem dla typów w bazach danych, jednakże istnieją rozszerzenia, które mogą udoskonalić nasz model relacyjny i z których warto czasami korzystać (chociaż okazuje się, że w tym konkretnym przypadku bazy otwarte są zgodne). Własnie z tego powodu - istnienia rozszerzeń - dodano element columnDefinition do adnotacji @Column. Jeśli chcielibyśmy wpłynąć na ostateczne zapytanie SQL, które będzie tworzyło, zapisywało, czy odczytywało dane z/do bazy danych możemy skorzystać z elementu columnDefinition. Przy jego zastosowaniu nie ma znaczenia domyślne mapowanie typów w Javie na odpowiadające im typy w bazie danych, ponieważ są one nadpisywane przez własną definicję pola określoną w elemencie columnDefinition.

Biorąc pod uwagę dyskusję jaka rozpoczęła się na grupie Apache OpenJPA dev nt. temat - @Column with precision and scale - how does it work? okazuje się, że nawet zastosowanie typu BigDecimal, aby wymusić zastosowanie precision oraz scale nie ma zastosowania (błąd w implementacji? - OPENJPA-213 @Column with precision and scale should result in NUMERIC(precision, scale)). Zresztą zaraz się o tym przykonamy.

Na koniec warto zaznajomić się z dokumentacja samego typu java.math.BigDecimal. Na uwagę zasługuje fakt, że nie istnieje konstruktor, który utworzyłby liczbę typu BigDecimal z podanymi precyzją oraz skalą. Dziwne?!

Przyjrzyjmy się kilku przykładom i ich działaniu z dostawcami Apache OpenJPA 0.9.7-SNAPSHOT, TopLink Essentials 2.0 BUILD 40 oraz Hibernate EntityManager 3.3.1.

Załóżmy, że zdefiniowaliśmy encję PracownikSpecjalny (która rozszerza encję Osoba) z atrybutem pensja typu double.

@Entity
public class PracownikSpecjalny extends Osoba {

private double pensja;

public double getPensja() {
return pensja;
}

public void setPensja(double pensja) {
this.pensja = pensja;
}
}

Dla Apache OpenJPA jest to informacja, aby stworzyć następujące zapytanie SQL:

157 derbyPU INFO [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.DerbyDictionary".
...
3141 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 18662247 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DOUBLE, tytul VARCHAR(255), PRIMARY KEY (numer))

TopLink Essentials utworzy następujące zapytanie:

[TopLink Info]: 2007.04.11 11:37:02.890--ServerSession(18926678)--Thread(Thread[main,5,main])--TopLink, version: Oracle TopLink Essentials - 2.0 (Build 40 (03/30/2007))
[TopLink Fine]: 2007.04.11 11:37:04.593--Thread(Thread[main,5,main])--Detected Vendor platform: oracle.toplink.essentials.platform.database.JavaDBPlatform
...
[TopLink Fine]: 2007.04.11 11:37:05.078--ServerSession(18926678)--Connection(12539221)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA FLOAT, PRIMARY KEY (NUMER))

natomiast Hibernate EntityManager najpierw odmówił posłuszeństwa, kiedy zdefiniowałem parametr

<property name="hibernate.use_sql_comments" value="true"/>

w persistence.xml, co spowodowało utworzenie komentarzy w zapytaniach

11:38:27,578 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
11:38:27,578 DEBUG SQL:393 -
/* insert pl.jaceklaskowski.jpa.entity.Projekt
*/ insert
into
Projekt
(chair_numer, rodzajProjektu, nazwa)
values
(?, ?, ?)
Hibernate:
/* insert pl.jaceklaskowski.jpa.entity.Projekt
*/ insert
into
Projekt
(chair_numer, rodzajProjektu, nazwa)
values
(?, ?, ?)
11:38:27,578 DEBUG AbstractBatcher:476 - preparing statement
11:38:27,593 DEBUG JDBCExceptionReporter:69 - could not insert: [pl.jaceklaskowski.jpa.entity.Projekt] [/* insert pl.jaceklaskowski.jpa.entity.Projekt */ insert into Projekt (chair
_numer, rodzajProjektu, nazwa) values (?, ?, ?)]
ERROR 42X01: Syntax error: Encountered "/" at line 1, column 1.
at org.apache.derby.iapi.error.StandardException.newException(Unknown Source)
at org.apache.derby.impl.sql.compile.ParserImpl.parseStatement(Unknown Source)
at org.apache.derby.impl.sql.GenericStatement.prepMinion(Unknown Source)
at org.apache.derby.impl.sql.GenericStatement.prepare(Unknown Source)
at org.apache.derby.impl.sql.conn.GenericLanguageConnectionContext.prepareInternalStatement(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedPreparedStatement.(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedPreparedStatement20.(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedPreparedStatement30.(Unknown Source)
at org.apache.derby.jdbc.Driver30.newEmbedPreparedStatement(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedConnection.prepareStatement(Unknown Source)
at org.apache.derby.impl.jdbc.EmbedConnection.prepareStatement(Unknown Source)
at org.hibernate.jdbc.AbstractBatcher.getPreparedStatement(AbstractBatcher.java:497)
at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:94)
at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:87)
at org.hibernate.jdbc.AbstractBatcher.prepareBatchStatement(AbstractBatcher.java:218)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2220)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2656)
at org.hibernate.action.EntityInsertAction.execute(EntityInsertAction.java:52)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:248)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:232)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:139)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:338)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:54)
at pl.jaceklaskowski.jpa.BaseTest.utworzPracownikow(BaseTest.java:100)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.testng.internal.MethodHelper.invokeMethod(MethodHelper.java:552)
at org.testng.internal.Invoker.invokeMethod(Invoker.java:411)
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:785)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:114)
at org.testng.TestRunner.privateRun(TestRunner.java:693)
at org.testng.TestRunner.run(TestRunner.java:574)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:241)
at org.testng.SuiteRunner.run(SuiteRunner.java:145)
at org.testng.TestNG.createAndRunSuiteRunners(TestNG.java:901)
at org.testng.TestNG.runSuitesLocally(TestNG.java:863)
at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.executeTestNG(TestNGDirectoryTestSuite.java:195)
at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.execute(TestNGDirectoryTestSuite.java:133)
at org.apache.maven.surefire.Surefire.run(Surefire.java:132)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:290)
at org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:818)

, a po usunięciu parametru pojawiło się upragnione zapytanie SQL tworzące tabelę dla encji Osoba:

11:46:01,734 INFO Dialect:152 - Using dialect: org.hibernate.dialect.DerbyDialect
...
11:46:40,359 DEBUG SchemaExport:303 -
create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja double,
tytul varchar(255),
primary key (numer)
)

Jak widać dla TopLinka pensja stała się kolumną o typie FLOAT, gdzie dla OpenJPA oraz Hibernate jest to DOUBLE. Jeden z detali implementacyjnych do zanotowania.

Sprawdźmy jak zachowają się dostawcy JPA po skorzystaniu z adnotacji @Column i elementami precision oraz scale. Udekorujmy atrybut pensja adnotacją @Column.

@Column(precision = 5, scale = 2)
public double getPensja() {
return pensja;
}

Okazuje się, że nie ma to żadnego wpływu na skrypty SQL tworzone przez dostawców.

Skorzystajmy zatem z elementu columnDefinition.

@Column(precision = 5, scale = 2, columnDefinition = "DECIMAL(7,3)")
public double getPensja() {
return pensja;
}

, czyli pensja będzie 7-cyfrową liczbą z 3 liczbami po przecinku.
Apache OpenJPA

2938 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 21781303 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DECIMAL(7,3), tytul VARCHAR(255), PRIMARY KEY (numer))

TopLink Essentials:

[TopLink Fine]: 2007.04.11 11:54:02.578--ServerSession(18926678)--Connection(12539221)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA DECIMAL(7,3), PRIMARY KEY (NUMER))

oraz Hibernate EntityManager

11:55:00,000 DEBUG SchemaExport:303 -
create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja DECIMAL(7,3),
tytul varchar(255),
primary key (numer)
)

, czyli wszyscy dostawcy skorzystali z konfiguracji kolumny poprzez element columnDefinition adnotacji @Column.

Sprawdźmy w jaki sposób dostawcy zareagują na atrybut pensja o typie java.math.BigDecimal.

public BigDecimal getPensja() {
return pensja;
}

OpenJPA:

2953 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 21781303 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DOUBLE, tytul VARCHAR(255), PRIMARY KEY (numer))

czyli ujawnia się błąd OpenJPA, który nie toleruje BigDecimal (przynajmniej dla Derby).
TopLink:

[TopLink Fine]: 2007.04.11 12:05:47.593--ServerSession(18926678)--Connection(30987167)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA DECIMAL, PRIMARY KEY (NUMER))

oraz Hibernate:

create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja numeric(19,2),
tytul varchar(255),
primary key (numer)
)

Jak widać dowolność w interpretacji typu BigDecimal jest ogromna.

Na koniec sprawdźmy jak zachowają się nasi dostawcy JPA z typem BigDecimal oraz adnotacją @Column z elementami precision oraz scale.

@Column(precision = 7, scale = 3)
public BigDecimal getPensja() {
return pensja;
}

OpenJPA:

2968 derbyPU TRACE [main] openjpa.jdbc.SQL - executing stmnt 18662247 CREATE TABLE Osoba (numer BIGINT NOT NULL,
dzienImienin TIMESTAMP, dzienUrodzin TIMESTAMP, imie VARCHAR(255),
kraj VARCHAR(255), nazwisko VARCHAR(255), wersja INTEGER,
pensja DOUBLE, tytul VARCHAR(255), PRIMARY KEY (numer))

OpenJPA upiera się przy swoim i nie reaguje na elementy precision oraz scale adnotacji @Column.
TopLink:

[TopLink Fine]: 2007.04.11 12:10:09.281--ServerSession(18926678)--Connection(12539221)--Thread(Thread[main,5,main])--CREATE TABLE OSOBA (NUMER BIGINT NOT NULL, DTYPE VARCHAR(31),
DZIENURODZIN DATE, DZIENIMIENIN DATE, IMIE VARCHAR(255),
KRAJ VARCHAR(255), WERSJA INTEGER, NAZWISKO VARCHAR(255), TYTUL VARCHAR(255),
PENSJA DECIMAL(7,3), PRIMARY KEY (NUMER))

oraz Hibernate

12:28:47,640 DEBUG SchemaExport:303 -
create table Osoba (
DTYPE varchar(31) not null,
numer bigint not null,
dzienImienin date,
dzienUrodzin date,
imie varchar(255),
kraj varchar(255),
nazwisko varchar(255),
wersja integer not null,
pensja numeric(7,3),
tytul varchar(255),
primary key (numer)
)

Poza OpenJPA wszyscy zachowali się zgodnie z oczekiwaniem, tj. elementy precision oraz scale adnotacji @Column dla atrybutu o typie BigDecimal wpłynęły na ostateczny skrypt tworzący tabelę Osoba.

Dzięki Sherkan za temat!