Is it possible to link objects on a unique column? - java

I'm using Hibernate 3.2.7.ga and I'm wondering if the following is possible.
Let's say there are 3 objects: Person, Town and Mayor.
When registering a Person in the application, you're asked to enter his or her town of residence. This field is saved as a String.
In another part of the application, you can register a Town (with it's name, number of inhabitants, etc.). After the Town is registered, you can add a Mayor to this town.
However, it is not desired to create a Town object for every town that is entered with a Client. An object is only desired when a Mayor needs to be registered.
I'm wondering if, just bij using the String field in Person, you can get the Mayor of the Town the Person lives in.
I'm thinking along these lines of code:
public class Client.java
{
#Id
public Long id;
#Column(nullable = true, ??? )
public String townOfResidence;
public Mayor getMayor()
{
...
}
}
public class Town.java
{
#Id
public Long id;
#Column(nullable = false, unique = true)
public String name;
#OneToOne
public Mayor mayor;
}
I just tried thinking along the following lines:
#Transient
private Town townObject;
#ManyToOne(fetch = FetchType.LAZY, cascade = javax.persistence.CascadeType.ALL, targetEntity = Town.class, optional = true)
#JoinColumn(referencedColumnName = "town")
public Town getTown()
{
return townObject;
}

It is more an ERD issue than an ORM one.
Either you have a relationship between Person and Town or you don't. If you have, you can only select values in the reference attribute that are in keys in the referenced table, or null (if valid). If you don't have the relationship, you have no restrictions.
Of course, you do not need to have a relationship to try to mix them in a query/JPQL. Without relationship, your program may offer the list of Town as possible values for the Person town, without forcing them to pick one. Or you may do something like
SELECT t FROM Town t WHERE t.name = ?
And pass the person town there. Of course too, then there is no security that it will be not null.
An alternative approach might be having two columns, one as a reference to a Town (allowing null) and other as a free text. If the town is in the table you use the reference, if it is not use the String. Of course, this complicates your program logic considerably.

With this design, you'll need to use a query to get the mayor of the town of the person:
select mayor from Town town
inner join town.mayor mayor
where town.name = :theTownOfResidenceOfTheClient
This query will have to be executed as part of the method of a DAO/Repository class. The mayor can't be available in a getMayor() method of the Client object.

Related

Query in repository not translated correctly

I am quite a beginner with Spring Data, and I have to code my first difficult query :)
I am making a reservation engine for an event. My data model is composed by:
a RoomType entity defining a possible configuration for a room (e.g. double, triple, quadruple)
a Room entity representing the actual Room
a RoomArrangement entity defining all the possible RoomTypes for a Room (e.g. the room 7 can be configured as Triple or Double room)
a RoomAssignment entity representing the actual configuration chosen for a room after having been reserved
FKs are configured this way
RoomType <--* RoomArrangement *--> Room <-- RoomAssignment
(see code below for Entity definition)
I need to find the Room without an Assignment that has the highest Priority (= nearest to 1) for a given RoomType.
I have configured Spring as below.
RoomType.java
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "room_type_id")
private List<RoomArrangement> roomArrangements;
RoomArrangement.java
#ManyToOne(targetEntity = RoomType.class)
#JoinColumn(name = "room_type_id", nullable = false)
private RoomType roomType;
#ManyToOne(targetEntity = Room.class)
#JoinColumn(name = "room_id", nullable = false)
private Room room;
#Column(name = "priority")
private Integer priority;
Room.java
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "room_id")
private List<RoomArrangement> roomArrangements;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "room", cascade = CascadeType.ALL)
private RoomAssignment assignment;
RoomArrangementRepository.java
RoomArrangement findFirstByRoomTypeAndRoom_AssignmentIsNullOrderByPriorityAsc(RoomType rt);
The query above is translated to
select
roomarrang0_.id as id1_3_,
roomarrang0_.priority as priority2_3_,
roomarrang0_.room_id as room_id3_3_,
roomarrang0_.room_type_id as room_typ4_3_
from
room_arrangements roomarrang0_
left outer join
rooms room1_ on roomarrang0_.room_id=room1_.id
where
roomarrang0_.room_type_id=9
and
(room1_.id is null)
order by
roomarrang0_.priority asc
limit 1;
The issues are two:
I do not know where the where clause
(room1_.id is null)
comes from
I do not know where the "AndRoom_AssignmentIsNull" clause has gone
Should I "invert" the OneToOne relationship and put the FK on the Room class?
Thanks for your help!
Lorenzo
I've tried to apply some of the suggestions, and "turned" the query on the RoomRepository.
The query came like this:
Room findFirstByRoomArrangements_RoomTypeAndAssignmentIsNullOrderByRoomArrangements_PriorityAsc(RoomType rt);
We come to the same problem:
select
room0_.id as id1_6_,
room0_.room_name as room_nam2_6_
from
rooms room0_
left outer join
room_arrangements roomarrang1_ on room0_.id=roomarrang1_.room_id
where
roomarrang1_.room_type_id=?
and
(room0_.id is null)
order by
roomarrang1_.priority asc
limit ?;
I think the problem lies in the fact that the one-to-one relationship between Room and RoomAssignment is represented on the database with a FK on the room_assignments table to the rooms table.
I will try to put the FK on the rooms table instead of on the room_assignments table and see if something changes.
If you need to find a room without an without an Assignment, shouldn't you be looking that in the Room repository?
You said that you have to find a Room but you are returning a RoomArrangement in a RoomAssignmentRepository. This is quite confusing.
Let's assume you are in right place, that means RoomRepository and as you said:
I need to find the Room without an Assignment that has the highest Priority (= nearest to 1) for a given RoomType.
try to use the following method name
Room findByRoomArrangementRoomTypeAndRoomAssignmentIsNullOrderByPriorityAsc(RoomType rt)
Made it!
In the end, the problem lied in the fact that the one-to-one relationship between Room and RoomAssignment was mapped by an FK from RoomAssignment to Room. Apparently, Spring Data didn't manage this configuration properly: the Assignment is null was translated to room_assignment.room_id = null and, since room_id was an FK to room.id, to room.id = null.
Inverting the FK mapping, the query is translated to
select
room0_.id as id1_6_,
room0_.assignment_id as assignme3_6_,
room0_.room_name as room_nam2_6_
from
rooms room0_
left outer join room_arrangements roomarrang1_ on room0_.id=roomarrang1_.room_id
where
roomarrang1_.room_type_id=?
and (room0_.assignment_id is null)
order by
roomarrang1_.priority asc
limit ?
which correctly returns what I needed.

Spring / JPA join on namespaced key

I am creating a Spring 4 / Spring Data application for an existing database. The database structure and data are defined by a closed source software.
One aspect of the existing system is that you can create a comment on any other item in the system. This means, that an article, a document, a media file (all entities in the system) can have any number of comments, and each comment is exactly for one entity in the system. All comments are in the same comment table.
The way this is implemented is that the table comment has a column comment_for that holds a concatenated/namespaced/prefixed reference to the actual entity it is a comment for. The current system seems to just builds the join query by prefixing the primary key with the table name:
+----+-------------------+----------------+
| id | comment_for | comment |
+----+-------------------+----------------+
| 1| article:12345 | This is nice...|
| 2| document:42 | Cool doc! |
+----+-------------------+----------------+
This sample shows two comments, one for an Article with an article.id of 12345 and one for a document with document.id of 42. I created #Entities matching the database tables and the corresponding Repository Interfaces with the query methods I need.
I would like to make use of Spring Data Repositories / Entities to populate the collections of my entities with the corresponding comments, like this (pseudocde) for Entity Article.
#OneToMany(mappedBy = "comment_for", prefix = "article:")
private List<Comment> comment = new ArrayList<>();
I only need it unidirectional. My entities (at the moment Article, Document and Mediafile) should hold a collection of their comments. I don't need comments to hold a reference back to the entity.
Is there a way to do this? The resulting SQL query should be something like
SELECT * FROM .... WHERE comment.comment_for = concat('<entityname>:', <entity>.id);
I looked at #JoinColumn but I can't modify the used value for the join, only the column name. The only solution I have at the moment are manual #Querys on the CommentRepository Interface, which gives me an ArrayList of all comments for a certain Entity / ID combination. But I would like to have the comments automatically joined as part of my Business Entity.
Update : It looks like I am able to split the namespace and id from comment_for into two new columns without interrupting the existing software. The two columns are now comment_for_id and comment_for_entityname
You could also break out comment_for to contain only the id like your entities. Adding an additional column like entity_type would allow you to avoid duplicate id values between different entities.
Also you could use #JoinColumn on the owner side of the relationship between Entity and Comments. It looks like in your case that would be the Comment entity/table, since there are many comments per each entity.
Example:
#Entity
#NamedQueries({ #NamedQuery(name = "Comments.findAll", query = "select o from Comments o") })
#IdClass(CommentsPK.class)
public class Comments implements Serializable {
private static final long serialVersionUID = 4787438687752132432L;
#Id
#Column(name = "COMMENT_TEXT", nullable = false, length = 30)
private String commentText;
#Id
#Column(name = "ENTITY_TYPE", nullable = false, length = 30)
private String entityType;
#ManyToOne
#Id
#JoinColumn(name = "COMMENT_FOR")
private EntityDemo entityDemo;
Note that I set the combination of all three fields as the primary key, I am not sure what criteria is used as the PK in your current set up.
Here is an example of an Entity. The attributes have been made up for the purpose of demonstration.
#Entity
#NamedQueries({ #NamedQuery(name = "EntityDemo.findAll", query = "select o from EntityDemo o") })
#Table(name = "ENTITY_DEMO")
public class EntityDemo implements Serializable {
private static final long serialVersionUID = -8709368847389356776L;
#Column(length = 1)
private String data;
#Id
#Column(nullable = false)
private BigDecimal id;
#OneToMany(mappedBy = "entityDemo", cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private List<Comments> commentsList;

Result query to attribute

I'm a beginner on JPA and I don't know pass result of a query to attribute entity.
Let me explain in one example:
I have an entity called Team, and another called Players:
The player is children of Team.
Class Team:
public class Team{
...
(relationship with Player has been hidden)
...
#Column(name = "AMOUNT_PLAYERS")
private Short amountPlayers;
#Column(name = "AMOUNT_FIRSTSTRING_PLAYERS")
private Short amountFirstStringPlayers;
#Column(name = "AMOUNT_SECONDSTRING_PLAYERS")
private Short amountSecondStringPlayers;
...
}
Class Player:
Public class Player{
...
#Column("STATUS_PLAYER")
private Short statusPlayer;
...
}
I have two questions about this.
1) In the amountPlayers, I want number total of player from this team;
In the amountFirstStringPlayers, I want number total of players (first-string) with statusPlayer of Player equals 1;
And in the amountSecondStringPlayers, I want number total of players (second-string) with statusPlayer of Player equals 2;
How I can get this values when I Find entity with JPA.
Example:
SELECT team FROM Team team
When I execute this, I want amountPlayers, amountFirstStringPlayers and amountSecondStringPlayers.
2) Put the resultQuery in an attribute of an entity, on an entity is a good practice?
You can use hibernate formula which are very convenient for these cases where you dont really need to persist these computed datas.
ex :
#Formula("(select count(*) from Player p where p.team_id = id)")
private Short amountPlayers;
#Formula("(select count(*) from Player p where p.team_id = id and p.statusPLayer=1)")
private Short amountFirstStringPlayers;
#Formula("(select count(*) from Player p where p.team_id = id and p.statusPLayer=2)")
private Short amountSecondStringPlayers;
Formula accepts sql string as parameter and here in the example id is the id of the current entity. You must replace with your entity id name and column.
If you don't want to use Formula you can also compute these values in memory based on players relation in your team entity.
ex:
#OneToMany(mappedBy="team")
private List<Player> players; // Here your onetomany association
public short getAmountPlayers(){
return players!= null ? players.size() : 0;
}

Hibernate: create HQL Query with Attribute values from Sets

I want to create a HQL Query that can access Attributes of a Set of spezified Objects, let me explain via a short example:
Class Organization
public class Organization ...{
private int orgid;
private Set<DomainValue> languages = new HashSet<language>(0);
private Set<Address> adresses = new HashSet<Address>(0);
...
}
Class Address
public class Address implements java.io.Serializable {
private int addressId;
private String city;
private String postalCode;
private String streetName;
private String houseNumber;
...
}
Language
public class Orgunitlanguage implements java.io.Serializable {
private int orgLanguageId;
private Orgunit orgunit;
private Integer languageCd;
...
}
These examples are code snippets of working hibernate POJOs. So i have an organization that can have multiple addresses and languages.
I want the user to specify the search criteria, but limit them to one of each kind, so only one language, one postalcode etc.
lets say the user wants english organizations with a housenumber 22
so i would build a hql query like this:
"from organization o where o.languages.languageCd = 1 AND o.addresses.housenumber = 22"
Well and that dosen't work (illegal syntax) how do i access these Sets in the right way? Keep in mind i want to access a specific attribute and not just the whole object (which is really easy).
I can't seem to find a documentation that i understand so a little explaination would be nice.
Proper way to query on collections would be like this
from Organization o join o.languages l join o.addresses a where l.languageCd = 1 AND a.housenumber = 22
However, this query will return any organization that has at least one language with languageCd = 1 and at least one address with housenumber = 22. It will not filter out the languages and addresses that don't fit the criteria. Check this answer for a little more explanation on this.

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.

Categories