Hibernate maps with discrimination column or class name as key - java

I've been pulling my hair out over this for the best part of a day now, and simply can't find any answers to this problem.
I've got a PostgreSQL schems that looks like this:
+---------+ 1-n +-------------+ 1-1 +------+
| Product |-------->| ProductSpec |-------->| Spec |
+---------+ +-------------+ +------+
This represents a one to many relationship between a Product and its list of Specifications (The reason I don't just use a foreign key in the specifications table into the products table is because specifications can belong to things that aren't in the product inheritance tree, those links are represented by other intersection tables).
Each Specification is a subclass of a Specification class (Weight, Length, NumberOfThings, and so on), with the name of the subclass in question being stored in the Spec table. Each product has a collection of specifications, but each subclass of specification can only appear once. A product can only have one weight (though if you need a weight for the actual product, and a shipping weight for the courier to calculate shipping charges, you can simply subclass ActualWeight and ShippingWeight from the Weight specification).
Using the simplest case, a Set in the Product class, I'm able to construct the object graph correctly from a Hibernate query of the products table. I want to use a Map instead, however, so I can address specific specifications directly. The plan was to use the class name as the key, but I'm having serious issues trying to get it to work. I'm unable to figure out how to use the Java class name as the key, and trying to use the class name as stored in the database as the map key is proving problematic.
As currently implemented, I'm able to query the specifications, and the products individually (if I comment out the code implementing the mapping between products and specifications). I can also query the products with the specifications embedded if I use a set, but if I use a map with the MapKey set to be the specifications class name, I get an exception.
Sep 01, 2013 1:25:55 AM org.hibernate.util.JDBCExceptionReporter
logExceptions WARNING: SQL Error: 0, SQLState: 42P01 Sep 01, 2013
1:25:55 AM org.hibernate.util.JDBCExceptionReporter logExceptions
SEVERE: ERROR: relation "specifications" does not exist Position: 424
I've annotated my (cut down) classes as follows. The product class:
#Entity
#Table (
name="products",
schema="sellable"
)
public abstract class Product extends Sellable {
private Map <String, Specification> specifications = new HashMap <> ();
#OneToMany (fetch = FetchType.EAGER)
#Cascade (CascadeType.SAVE_UPDATE)
#JoinTable (
schema = "sellable",
name = "productspecifications",
joinColumns = {#JoinColumn (name = "sll_id")},
inverseJoinColumns = {#JoinColumn (name = "spc_id")})
#MapKey (name = "className")
private Map <String, Specification> getSpecifications () {
return this.specifications;
}
private Product setSpecifications (Map <String, Specification> specs) {
this.specifications = specs;
return this;
}
}
And the Specification class:
#Entity
#Table (
name="specifications",
schema="sellable",
uniqueConstraints = #UniqueConstraint (columnNames="spc_id")
)
#Inheritance (strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn (name = "spc_classname", discriminatorType=DiscriminatorType.STRING)
public abstract class Specification implements Serializable {
private Integer specId = null;
private String className = null;
#Id
#Column (name="spc_id", unique=true, nullable=false)
#SequenceGenerator (name = "specifications_spc_id_seq", sequenceName = "sellable.specifications_spc_id_seq", allocationSize = 1)
#GeneratedValue (strategy = GenerationType.SEQUENCE, generator = "specifications_spc_id_seq")
public Integer getSpecId () {
return this.specId;
}
private Specification setSpecId (Integer specId) {
this.specId = specId;
return this;
}
#Column (name="spc_classname", insertable = false, updatable = false, nullable = false)
public String getClassName () {
return this.className;
}
private void setClassName (String className) {
this.className = className;
}
}
The DB schema looks like this:
CREATE TABLE sellable.sellables
(
sll_id serial NOT NULL, -- Sellable ID
sll_date_created timestamp with time zone NOT NULL DEFAULT now(), -- Date the item was created
sll_date_updated timestamp with time zone NOT NULL DEFAULT now(), -- Date the item was last updated
sll_title character varying(255) NOT NULL, -- Title of the item
sll_desc text NOT NULL, -- Textual description of the item
CONSTRAINT sellables_pkey PRIMARY KEY (sll_id)
)
CREATE TABLE sellable.products
(
sll_id integer NOT NULL, -- Sellable ID
mfr_id integer NOT NULL, -- ID of the product manufacturer
CONSTRAINT products_pkey PRIMARY KEY (sll_id),
CONSTRAINT products_mfr_id_fkey FOREIGN KEY (mfr_id)
REFERENCES sellable.manufacturers (mfr_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT products_sll_id_fkey FOREIGN KEY (sll_id)
REFERENCES sellable.sellables (sll_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
CREATE TABLE sellable.specifications
(
spc_id serial NOT NULL, -- Specification ID
spc_classname character varying(127) NOT NULL, -- Specification subclass
CONSTRAINT specifications_pkey PRIMARY KEY (spc_id)
)
CREATE TABLE sellable.productspecifications
(
ps_id serial NOT NULL, -- Primary key
sll_id integer NOT NULL, -- Product the specification is linked to
spc_id integer NOT NULL, -- Specification the product is associated with
CONSTRAINT productspecifications_pkey PRIMARY KEY (ps_id),
CONSTRAINT productspecifications_sll_id_fkey FOREIGN KEY (sll_id)
REFERENCES sellable.products (sll_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT productspecifications_spc_id_fkey FOREIGN KEY (spc_id)
REFERENCES sellable.specifications (spc_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT productspecifications_spc_id_key UNIQUE (spc_id)
)
The query that Hibernate generates is listed below (I've not trimmed this the way I have the classes in case there's something in the unabridged query that is an issue). One obvious problem is that it's trying to query the specifications table without inserting the schema name.
select
bicycle0_.sll_id as sll1_0_3_,
bicycle0_2_.sll_date_created as sll2_0_3_,
bicycle0_2_.sll_date_updated as sll3_0_3_,
bicycle0_2_.sll_desc as sll4_0_3_,
bicycle0_2_.sll_title as sll5_0_3_,
bicycle0_1_.mfr_id as mfr2_1_3_,
bicycle0_.btp_id as btp2_2_3_,
manufactur1_.mfr_id as mfr1_4_0_,
manufactur1_.mfr_name as mfr2_4_0_,
specificat2_.sll_id as sll1_5_,
specificat3_.spc_id as spc2_5_,
(select
a9.spc_classname
from
specifications a9
where
a9.spc_id=specificat2_.spc_id) as formula0_5_,
specificat3_.spc_id as spc2_5_1_,
specificat3_.spc_classname as spc1_5_1_,
specificat3_1_.dec_value as dec1_6_1_,
specificat3_2_.bol_value as bol1_7_1_,
specificat3_3_.int_value as int1_8_1_,
specificat3_4_.str_value as str1_9_1_,
bicycletyp4_.btp_id as btp1_3_2_,
bicycletyp4_.btp_name as btp2_3_2_
from
sellable.bicycles bicycle0_
inner join
sellable.products bicycle0_1_
on bicycle0_.sll_id=bicycle0_1_.sll_id
inner join
sellable.sellables bicycle0_2_
on bicycle0_.sll_id=bicycle0_2_.sll_id
left outer join
sellable.manufacturers manufactur1_
on bicycle0_1_.mfr_id=manufactur1_.mfr_id
left outer join
sellable.productspecifications specificat2_
on bicycle0_.sll_id=specificat2_.sll_id
left outer join
sellable.specifications specificat3_
on specificat2_.spc_id=specificat3_.spc_id
left outer join
sellable.specdecimalvalues specificat3_1_
on specificat3_.spc_id=specificat3_1_.spc_id
left outer join
sellable.specbooleanvalues specificat3_2_
on specificat3_.spc_id=specificat3_2_.spc_id
left outer join
sellable.specintegervalues specificat3_3_
on specificat3_.spc_id=specificat3_3_.spc_id
left outer join
sellable.specstringvalues specificat3_4_
on specificat3_.spc_id=specificat3_4_.spc_id
left outer join
sellable.bicycletypes bicycletyp4_
on bicycle0_.btp_id=bicycletyp4_.btp_id
where
bicycle0_.sll_id=?
The problem is in the sub-query, which isn't getting a schema prepended to the specifications table name.
If anyone knows how to either get the query to be correct, or of using the class name directly as the Java map key, I'd appreciate being told.
EDIT: The reason I want to use a map instead of a set is because I want to directly address items in the specifications collection. If I use sets, the queries generated by Hibernate work, but I don't have an index to access the elements by. The API of the Product object hides the fact that the specifications are stored in a collection and provides getters and setters for each individual specification.
If I make the specifications a set, I have to implement the getters and setters like this:
#Transient
public BigDecimal getActualWeight () {
BigDecimal found = null;
for (Specification spec : this.specifications) {
if (spec instanceof ActualWeightSpec) {
found = ((ActualWeightSpec) spec).getValue ();
break;
}
}
return found;
}
public Product setActualWeight (Number value) {
ActualWeightSpec newWeight = new ActualWeightSpec ();
newWeight.setValue (value);
for (Specification spec : this.specifications) {
if (spec instanceof ActualWeightSpec) {
((ActualWeightSpec) spec).setValue (value);
return this;
}
}
this.specifications.add (newWeight);
return this;
}
Having to iterate over a set to get individual specification records seems a really bad way of accessing those records directly.
I did try maintaining a hashmap internally, and having the getter and setter for the specifications accept and return sets with a conversion taking place in the getter and setter. That way I'd only have to take the hit of iterating the specifications once.
private Product setSpecifications (Set <Specification> specs) {
HashMap <String, Specification> specsMap = new HashMap <> ();
for (Specification spec : specs) {
specsMap.put(spec.getClassName (), spec);
}
this.specifications = specsMap;
return this;
}
This didn't work either, causing Hibernate to throw an exception.
SEVERE: illegal access to loading collection

You could use a internal map of specifications, not bothering the database with the map. Do not initialize the map in getters or setters Hibernate uses, but check in your getters (e.g. getActualWeight) if your transient map has been initialized already. If not, iterate the specs once and build the map. Btw, if there are not too many specs, iterating each time should not hurt too much.

Related

Hibernate join-table two composite keys that are both foreign keys

I have two different tables, both of which have composite embedded keys. Both composite keys have in composition the same id A_ID.
I want to join table M with table D in a one to many relationship using a join-table.
The following are some pseudo-java code converted from XML ORM mappings. So please excuse any mistakes written here. The mappings in the final code work so the typos here are not to blame.
#Entity()
public class M {
#EmbeddedId()
private EmbeddedMId id;
#OneToMany(name="d", #JoinTable(name="M-D",
joinColumns={
#JoinColumn(name="M_ID", referencedColumnName="M_ID"),
#JoinColumn(name="A_ID", referencedColumnName="A_ID", table="M")
},
inverseJoinColumns={
#InverseJoinColumn(name="D_ID", referencedColumnName="D_ID"),
#InverseJoinColumn(name="A_ID", referencedColumnName="A_ID", table="D", insertable="false", updatable="false")
}
))
private Set<D> dSet;
}
#Embeddable()
public class EmbeddedMId {
#Basic() private String A_ID;
#Basic() private String M_ID;
}
#Embeddable()
public class EmbeddedDId {
#Basic() private String A_ID;
#Basic() private String D_ID;
}
As you can see, the embeddables both use A_ID therefore we tried to make the 2nd A_ID in the join-table be readonly. The application starts and the mappings seem to be okay.
The problem is whenever I want to insert a new D object in the M entity, hibernate throws an SQL error invalid column index because while the prepared statement is correct as seen bellow, hibernate only provides the first 2 parameters instead of all three. (Values provided by hibernate are (VALID_M_ID, VALID_A_ID) instead of providing 3 values)
INSERT INTO M_D("M_ID", "A_ID", "D_ID") VALUES (?, ?, ?)
If I rename the 2nd inverseJoinColumn to have a new column name and make it insertable/updatable, the problem is solved. But this means that the A_ID is duplicated in both A_ID and A_REPEAT_ID column and this is what I want to avoid.
#InverseJoinColumn(name="A_REPEAT_ID", referencedColumnName="A_ID", table="D")
Is there a way to tell Hibernate that my EmbeddedDId needs to be mapped over the D_ID and A_ID (readonly) correctly when doing the insertions?
I hope my explanation is clear enough, but feel free to ask for any clarifications.
Hibernate version is 5.2.17-FINAL
EDIT
The only other entity that is important in this case is pretty simple. But as requested I'll write it here
#Entity()
public class D {
#EmbeddedId()
private EmbeddedDId id;
/* other basic fields here */
}
I don't think insertable = false, updatable = false does what you want here. If you want the target column A_ID on D to be readonly, then you will have to map the column in the target entity D and specify there that the column is insertable = false, updatable = false but not on this association.

Spring Data JDBC org.springframework.data.mapping.MappingException: Could not read value rental_movie from result set

How to select query filter in one-to-one relationship with Spring Data JDBC ?
Schema looks like this, basically 2 tables where Rental references Movie
drop table if exists rental;
drop table if exists movie;
create table movie
(
id serial primary key,
title text,
description text
);
create table rental
(
movie integer primary key references movie (id),
duration text,
price integer
)
And my code looks like this
#Query("select * from movie where title = :title ")
fun findByTitle(#Param("title") title: String): List<Movie>
But got an exception org.springframework.data.mapping.MappingException: Could not read value rental_movie from result set!
The example project on GitHub.
P.S I am quite new to this and followed this video to learn basics, please help me to do it in proper way
Solution # 1
Use the #Query like this, but still not so good since there can be a lot of columns inside second table
SELECT movie.*,
rental.price AS rental_price,
rental.duration AS rental_duration,
rental.movie AS rental_movie
FROM movie
LEFT OUTER JOIN rental ON rental.movie = movie.id
where movie.title = 'Matrix'
Your solution #1 is currently the correct way to do this.
Query must return columns for all simple properties of the aggregate root, and for all embedded or referenced entities.
If you don't want to do that you can alway specify your own RowMapper or ResultSetExtractor
Let's assume the following classes (similar to those you probably have):
class Movie {
#Id Long id;
String title;
#Embedded
Actor with;
Rental rental;
}
class Actor {
String name;
}
class Rental {
Integer price;
}
Your select needs to return the following columns:
id for the id property of Movie
title for the title property of Movie
rental_price for the price property of Rental. Note the prefix rental comes from the property namerentalnot from the class nameRental`.
rental_movie this column is an artificial id for Rental used to determine if there is a Rental at all or if Movie.rental is null.
The value is irrelevant except for the fact if it is null or not.
This column is not required if Rental has an id column.
name for the property name of the class Actor.
Note: There is no prefix here. If you want a prefix, e.g. because a class is embedded multiple times you have to put that in the #Embedded annotation.
_Note #1: There is no artificial id here.
For embedded classes there is the onEmpty attribute of the #Embedded annotation to control if when all properties are null, if the whole embedded class is null or if the embedded class gets instantiated with all properties set to null.
With Spring Data JDBC 1.x a missing column causes an exception as you have seen.
From Version 2.0 a missing column will be silently ignored and the property not set.
There is an issue to provide an easier way to define by just defining the where clause: https://jira.spring.io/browse/DATAJDBC-225
data class Movie(
#Id
val id: Long?,
val title: String,
val description: String,
val rental: Rental
)
I don't use Kotlin, but I think if you want to query Movie and Rental together, you have to use #OneToOne annotation. Something like(java):
public class Movie {
#OneToOne(mappedBy = "moive", cascade = CascadeType.ALL,
fetch = FetchType.LAZY, optional = false)
private Rental rental;
}
And your Rental class is not right, need movieId.
public class Rental {
...
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "movie_id")
private Movie movie;
}
you could check some code example.

JPA One To One relationship between two columns (not PK and not FK)

I have two tables, one named "Category" and the other "Rule" that are related logically with One to One relationship using a field (code) different than the Primary Key (PK) of table and not phisically managed with a Foreign Key (FK):
CATEGORY
ID (PK) NUMBER
COD_RULE VARCHAR
NAME VARCHAR
.....
RULE
ID (PK) NUMBER
CODE VARCHAR
TYPE VARCHAR
.....
I haven't on Rule table FK to category ID but only unique constraint (the relation is 1 to 1)
Implemented in this way in JPA
public Category implement Serializable {
#Id
#Column (name="ID")
private Long id;
#NotNull
#OneToOne(cascade=CascadeType.ALL)
#JoinColumn(name="CODE_RULE" referencedColumnName="CODE", nullable=false)
private Rule;
#Column (name="NAME")
private String name;
//Getter and Setter methods
.......
}
public Rule implement Serializable {
#Id
#Column (name="ID")
private Long id;
#NotNull
#Column (name="CODE")
private String code;
#Column (name="TYPE")
private String type;
//Getter and Setter methods
.......
}
I need to:
When retrieve Categories obtain also all informations of associated Rule
SELECT c.*, r.type FROM Category c LEFT OUTER JOIN Rule r WHERE c.CODE_RULE = r.CODE
When edit Category maintain aligned CODE_RULE with CODE, so If I change CODE_RULE I would yo change automatically the CODE on Rule
UPDATE Category SET COD_RULE='5A', NAME='Test' WHERE ID=1
UPDATE Rule SET CODE='5A' WHERE CODE='AB'
I see on the specification that:
There are three cases for one-to-one associations: either the
associated entities share the same primary keys values, a foreign key
is held by one of the entities (note that this FK column in the
database should be constrained unique to simulate one-to-one
multiplicity), or a association table is used to store the link
between the 2 entities (a unique constraint has to be defined on each
fk to ensure the one to one multiplicity).
But with this implementation satisfy point 1. But not the point 2.
Suppose that I've already created Category (on ID = 1) and associated rule, when I edit category (having CODE_RULE = CODE = "AB") and change the code to "5A":
#PersistentContext
private EntityManager em;
.......
Category cat = em.find(Category.class, 1L);
cat.setName("Test");
cat.getRule().setCode("5A");
em.merge(cat);
I see that the code has been updated on Rule but not in Category:
BEFORE EDIT
Category (ID, COD_RULE, NAME) --> (1, AB, First Category)
Rule (ID, CODE, TYPE) --> (10, AB, C)
AFTER EDIT
Category (ID, COD_RULE, NAME)--> (1, AB, Test)
Rule (ID, CODE, TYPE) --> (10, 5A, C)
How can I do this work in JPA?
Is this type of operation supported in the JPA specification?
Is there an alternative (i.e. I have to merge before Rule and then Category)?
From your datamodel, it looks more like a Many To One relationship between Category and Rule, given in your data model only restrict each Category can refer to [0..1] Rule, but not restricting how many Categories that a Rule can be referred by.
Based on your comment, it seems that you can change the data model. Normally if it is a ~ToOne relationship, you should have the referring side referring as FK, which looks like this:
(Tables)
CATEGORY (
CATEGORY_ID NUMBER PK,
CATEGORY_CODE VARCHAR, // Unqiue
RULE_ID NUMBER FK to RULE,
... (Don't refer by RULE_CODE!!)
)
RULE (
RULE_ID NUMBER PK,
RULE_CODE VARCHAR, // unique, can be updated
...
)
Entity should look like
class Category {
#Id #Column(name="CATEGORY_ID)
Long id;
#ManyToOne // or #OneToOne if you really insist
#JoinColumn(name="RULE_ID)
Rule rule;
)
(class Rule is straight-forward, I will skip)
The HQL you mentioned should be
// When retrieving Category together with Rule
from Category c join fetch c.rule
for Point 2, as you mentioned in comment, you are trying to align Rule's code with Category's code, when Category's code is updated. This should be implemented as:
class Category {
//.....
public void setCode(String code) {
this.code = code;
this.rule.setCode(code);
}
//....
)
Base on personal experience, when using JPA, life will be much easier to drive data model base on domain model design. It should save a lot of problem caused by "data-model that looks tolerable".

JPA Mapping Multi-Rows with ElementCollection

I'm trying to follow the JPA tutorial and using ElementCollection to record employee phone numbers:
PHONE (table)
OWNER_ID TYPE NUMBER
1 home 792-0001
1 work 494-1234
2 work 892-0005
Short version
What I need is a class like this:
#Entity
#Table(name="Phones")
public class PhoneId {
#Id
#Column(name="owner_id")
long owner_id;
#Embedded
List<Phone> phones;
}
that stores each person's phone numbers in a collection.
Long version
I follow the tutorial code:
#Entity
#Table(name="Phones")
public class PhoneId {
#Id
#Column(name="owner_id")
long owner_id;
#ElementCollection
#CollectionTable(
name="Phones",
joinColumns=#JoinColumn(name="owner_id")
)
List<Phone> phones = new ArrayList<Phone>();
}
#Embeddable
class Phone {
#Column(name="type")
String type = "";
#Column(name="number")
String number = "";
public Phone () {}
public Phone (String type, String number)
{ this.type = type; this.number = number; }
}
with a slight difference that I only keep one table. I tried to use the following code to add records to this table:
public static void main (String[] args) {
EntityManagerFactory entityFactory =
Persistence.createEntityManagerFactory("Tutorial");
EntityManager entityManager = entityFactory.createEntityManager();
// Create new entity
entityManager.getTransaction().begin();
Phone ph = new Phone("home", "001-010-0100");
PhoneId phid = new PhoneId();
phid.phones.add(ph);
entityManager.persist(phid);
entityManager.getTransaction().commit();
entityManager.close();
}
but it keeps throwing exceptions
Internal Exception: org.postgresql.util.PSQLException: ERROR: null
value in column "type" violates not-null constraint Detail: Failing
row contains (0, null, null). Error Code: 0 Call: INSERT INTO Phones
(owner_id) VALUES (?) bind => [1 parameter bound] Query:
InsertObjectQuery(tutorial.Phone1#162e295)
What did I do wrong?
Sadly, i think the slight difference that you only keep one table is the problem here.
Look at the declaration of the PhoneId class (which i would suggest is better called PhoneOwner or something like that):
#Entity
#Table(name="Phones")
public class PhoneId {
When you declare that a class is an entity mapped to a certain table, you are making a set of assertions, of which two are particularly important here. Firstly, that there is one row in the table for each instance of the entity, and vice versa. Secondly, that there is one column in the table for each scalar field of the entity, and vice versa. Both of these are at the heart of the idea of object-relational mapping.
However, in your schema, neither of these assertions hold. In the data you gave:
OWNER_ID TYPE NUMBER
1 home 792-0001
1 work 494-1234
2 work 892-0005
There are two rows corresponding to the entity with owner_id 1, violating the first assertion. There are columns TYPE and NUMBER which are not mapped to fields in the entity, violating the second assertion.
(To be clear, there is nothing wrong with your declaration of the Phone class or the phones field - just the PhoneId entity)
As a result, when your JPA provider tries to insert an instance of PhoneId into the database, it runs into trouble. Because there are no mappings for the TYPE and NUMBER columns in PhoneId, when it generates the SQL for the insert, it does not include values for them. This is why you get the error you see - the provider writes INSERT INTO Phones (owner_id) VALUES (?), which PostgreSQL treats as INSERT INTO Phones (owner_id, type, number) VALUES (?, null, null), which is rejected.
Even if you did manage to insert a row into this table, you would then run into trouble on retrieving an object from it. Say you asked for the instance of PhoneId with owner_id 1. The provider would write SQL amounting to select * from Phones where owner_id = 1, and it would expect that to find exactly one row, which it can map to an object. But it will find two rows!
The solution, i'm afraid, is to use two tables, one for PhoneId, and one for Phone. The table for PhoneId will be trivially simple, but it is necessary for the correct operation of the JPA machinery.
Assuming you rename PhoneId to PhoneOwner, the tables need to look like:
create table PhoneOwner (
owner_id integer primary key
)
create table Phone (
owner_id integer not null references PhoneOwner,
type varchar(255) not null,
number varchar(255) not null,
primary key (owner_id, number)
)
(I've made (owner_id, number) the primary key for Phone, on the assumption that one owner might have more than one number of a given type, but will never have one number recorded under two types. You might prefer (owner_id, type) if that better reflects your domain.)
The entities are then:
#Entity
#Table(name="PhoneOwner")
public class PhoneOwner {
#Id
#Column(name="owner_id")
long id;
#ElementCollection
#CollectionTable(name = "Phone", joinColumns = #JoinColumn(name = "owner_id"))
List<Phone> phones = new ArrayList<Phone>();
}
#Embeddable
class Phone {
#Column(name="type", nullable = false)
String type;
#Column(name="number", nullable = false)
String number;
}
Now, if you really don't want to introduce a table for the PhoneOwner, then you might be able to get out of it using a view. Like this:
create view PhoneOwner as select distinct owner_id from Phone;
As far as the JPA provider can tell, this is a table, and it will support the queries it needs to do to read data.
However, it won't support inserts. If you ever needed to add a phone for an owner who is not currently in the database, you would need to go round the back and insert a row directly into Phone. Not very nice.

How to map a 2-d matrix in Java to Hibernate/JPA?

I have a legacy database I'm trying to redesign into the 21st century. One of the existing data structures involves a particular class which contains a 2-dimensional matrix of values. If I were to reverse-engineer this class from the database, I'd end up with a series of attributes like:
private BigDecimal NODE_1_MATRIX_POS_1_1;
private BigDecimal NODE_1_MATRIX_POS_1_2;
and so on. Since this is a 6x6 matrix, there are a lot of such columns.
I've been looking for a better way, but I'm not sure I'm there. What I'd like to do is something like this:
#Entity
public class TestClass {
#Id
private long id;
#CollectionOfElements
#JoinTable(
name="MATRIX_DATA",
joinColumns=#JoinColumn(name="ENTRY_ID"))
private List<List<BigDecimal>> matrix;
But this fails:
org.hibernate.MappingException: Could not determine type for: java.util.List, at table: MATRIX_DATA, for columns: [org.hibernate.mapping.Column(element)]
Rather than just trying to fix the error, I thought I'd ask around and try to find the right approach to solving this mapping challenge. Has anyone found success and satisfaction mapping multidimensional arrays via JPA?
Rather than just trying to fix the error, I thought I'd ask around and try to find the right approach to solving this mapping challenge. Has anyone found success and satisfaction mapping multidimensional arrays via JPA?
AFAIK, nested collections are not supported by standard JPA. The JPA wiki book has a good section on this topic (I'm quoting only a part of it):
Nested Collections, Maps and Matrices
It is somewhat common in an object
model to have complex collection
relationships such as a List of
Lists (i.e. a matrix), or a Map of
Maps, or a Map of Lists, and so
on. Unfortunately these types of
collections map very poorly to a
relational database.
JPA does not support nested collection relationships, and normally
it is best to change your object model
to avoid them to make persistence and
querying easier. One solution is to
create an object that wraps the nested
collection.
For example if an Employee had a
Map of Projects keyed by a
String project-type and the value a
List or Projects. To map this a
new ProjectType class could be
created to store the project-type and
a OneToMany to Project.
...
And that would be my suggestion. For example:
#Entity
public class TestClass {
#Id
private long id;
#OneToMany(mappedBy="testClass")
private List<MatrixRow> matrix;
}
Where MatrixLine would be (omitting many details):
#Entity
public class MatrixRow {
#Id
private long id;
#ManyToOne
private TestClass testClass;
#CollectionOfElements
private List<BigDecimal> row;
}
Or maybe you could use a custom user type (I'm not too sure how this would work).
Or (after all, you're already using non portable annotations) have a look at this question to see how you could extend Hibernate:
How do I map a nested collection, Map<Key,List<Values>>, with hibernate JPA annotations?
Hibernate Types project
You can map a PostgreSQL multidimensional array using the Hibernate Types project.
You can choose to use a Java array on the entity attribute side or use List.
Database table
For exmaple, assuming you have the following plane database table:
CREATE TABLE plane (
id INT8 NOT NULL,
name VARCHAR(255),
seat_grid seat_status[][],
PRIMARY KEY (id)
)
Where the seat_status is a PostgreSQL enum:
CREATE TYPE seat_status
AS ENUM (
'UNRESERVED',
'RESERVED',
'BLOCKED'
);
JPA entity
You can map the seatGrid column using the EnumArrayType:
#Entity(name = "Plane")
#Table(name = "plane")
#TypeDef(
name = "seat_status_array",
typeClass = EnumArrayType.class
)
public static class Plane {
#Id
private Long id;
private String name;
#Type(
type = "seat_status_array",
parameters = #org.hibernate.annotations.Parameter(
name = "sql_array_type",
value = "seat_status"
)
)
#Column(
name = "seat_grid",
columnDefinition = "seat_status[][]"
)
private SeatStatus[][] seatGrid;
//Getters and setters omitted for brevity
public SeatStatus getSeatStatus(int row, char letter) {
return seatGrid[row - 1][letter - 65];
}
}
So, you need to declare the appropriate Hibernate Type to use. For enums, you need to use the EnumArrayType:
#TypeDef(
name = "seat_status_array",
typeClass = EnumArrayType.class
)
The #Type annotation allows you to pass parameters to the Hibernate Type, like the SQL array class:
#Type(
type = "seat_status_array",
parameters = #org.hibernate.annotations.Parameter(
name = "sql_array_type",
value = "seat_status"
)
)
Testing time
Now, when you persist the following Post entity:
entityManager.persist(
new Plane()
.setId(1L)
.setName("ATR-42")
.setSeatGrid(
new SeatStatus[][] {
{
SeatStatus.BLOCKED, SeatStatus.BLOCKED,
SeatStatus.BLOCKED, SeatStatus.BLOCKED
},
{
SeatStatus.UNRESERVED, SeatStatus.UNRESERVED,
SeatStatus.RESERVED, SeatStatus.UNRESERVED
},
{
SeatStatus.RESERVED, SeatStatus.RESERVED,
SeatStatus.RESERVED, SeatStatus.RESERVED
}
}
)
);
Hibernate will issue the proper SQL INSERT statement:
INSERT INTO plane (
name,
seat_grid,
id
)
VALUES (
'ATR-42',
{
{"BLOCKED", "BLOCKED", "BLOCKED", "BLOCKED"},
{"UNRESERVED", "UNRESERVED", "RESERVED", "UNRESERVED"},
{"RESERVED", "RESERVED", "RESERVED", "RESERVED"}
},
1
)
And, when fetching the entity, everything works as expected:
Plane plane = entityManager.find(Plane.class, 1L);
assertEquals("ATR-42", plane.getName());
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'A'));
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'B'));
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'C'));
assertEquals(SeatStatus.BLOCKED, plane.getSeatStatus(1, 'D'));
assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'A'));
assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'B'));
assertEquals(SeatStatus.RESERVED, plane.getSeatStatus(2, 'C'));
assertEquals(SeatStatus.UNRESERVED, plane.getSeatStatus(2, 'D'));

Categories