I'm new to hibernate and quite new to MySQL too.
I have the following two tables:
CREATE TABLE storeman.user (
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(80) NOT NULL,
display_name VARCHAR(50),
password CHAR(41),
active BOOLEAN NOT NULL DEFAULT FALSE,
provisional BOOLEAN NOT NULL DEFAULT FALSE,
last_login TIMESTAMP,
PRIMARY KEY (id),
UNIQUE INDEX (email)
);
CREATE TABLE storeman.user_preferences (
id INT NOT NULL AUTO_INCREMENT,
notify_login BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
CONSTRAINT id_foreign FOREIGN KEY (id) REFERENCES user (id) ON DELETE CASCADE
);
In Eclipse, with hibernate tools I have generated the domain code classes. User.java looks like this (siplified):
#Entity
#Table(name = "user", catalog = "storeman", uniqueConstraints = #UniqueConstraint(columnNames = "email"))
public class User implements java.io.Serializable {
private Integer id;
[...]
private UserPreferences userPreferences;
public User() {
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return this.id;
}
[...]
#OneToOne(fetch = FetchType.LAZY, mappedBy = "user")
public UserPreferences getUserPreferences() {
return this.userPreferences;
}
}
My issue is with getUserPreferences: of course, that would return null if creating a new user or reading from the db where the corresponding row in the user_detail table does not exist. This is correct, however it forces me to check if userPreferences is not null before accessing its members. And from a coding point of view it is not so handy. So I changed User.getUserPreferences method like this, to get a default value:
#OneToOne(fetch = FetchType.LAZY, mappedBy = "user")
public UserPreferences getUserPreferences() {
if (this.userPreferences==null)
this.userPreferences = new UserPreferences();
return this.userPreferences;
}
This is working fine, however if I ever would need to re-generate domain code (User.java) with hibernate tools, that change will be lost. So my question is: is there a way (even by modifying mySQL table/relationships) to automatically have userPreferences always set?
There is no way to do this outside of your code (at least not that I can think of), with some configuration or something like that.
One thing you can do is to initialize the relation when you declare it
private UserPreferences userPreferences = new UserPreferences()
but that also won't survive code regeneration. The only other way I can think of is to put this initialization code into some util method so you can maintain it there regardless of regeneration of entity code.
UserUtils.getUserPreferences(User user)
However, this would only work for the code you write, if some framework needs it you will again get null values because it will not use your util method (the first approach is better in this case).
Do bear in mind that, when you initialize this object on a managed entity, the new object will be persisted into the database.
User user = userDAO.getUser(id);
user.getUserPreferences(); // this code initializes the relation (new UserPreference())
After these lines, you will get a row in user_preferences table if cascade is configured in that manner, or you will get an exception complaining about transient entity found in entity you are trying to persist.
All that being said, is it really that hard to check if something is null, especially if by business rules it is allowed to be null?
Related
I am working on a Spring Boot application using Spring Data JPA and Hibernate mapping and I have the following problem.
I have this network table:
CREATE TABLE IF NOT EXISTS public.network
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
name character varying(50) COLLATE pg_catalog."default" NOT NULL,
description text COLLATE pg_catalog."default",
CONSTRAINT network_pkey PRIMARY KEY (id)
)
and this chain table:
CREATE TABLE IF NOT EXISTS public.chain
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
name character varying(50) COLLATE pg_catalog."default" NOT NULL,
fk_chain_type bigint NOT NULL,
description text COLLATE pg_catalog."default",
CONSTRAINT chain_pkey PRIMARY KEY (id),
CONSTRAINT "chain_to_chain_type_FK" FOREIGN KEY (fk_chain_type)
REFERENCES public.chain_type (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
These 2 tables are related to each other by a MANY TO MANY relationship, implmemented by this network_chain table:
CREATE TABLE IF NOT EXISTS public.network_chain
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
fk_network_id bigint NOT NULL,
fk_chain_id bigint NOT NULL,
CONSTRAINT network_chain_pkey PRIMARY KEY (id),
CONSTRAINT chain_id_fk FOREIGN KEY (fk_chain_id)
REFERENCES public.chain (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID,
CONSTRAINT network_id_fk FOREIGN KEY (fk_network_id)
REFERENCES public.network (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
Basically the fk_network_id field represents the id of a specific record into the network table while the fk_chain_id represents the id of a specific record into the chain table.
I mapped these DB tables with the following Hibernate entity classes, first of all I created this Network class mapping the network table:
#Entity
#Table(name = "network")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Network implements Serializable {
private static final long serialVersionUID = -5341425320975462596L;
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(name = "name")
private String name;
#Column(name = "description")
private String description;
#ManyToMany(cascade = { CascadeType.MERGE })
#JoinTable(
name = "network_chain",
joinColumns = { #JoinColumn(name = "fk_network_id") },
inverseJoinColumns = { #JoinColumn(name = "fk_chain_id") }
)
Set<Chain> chainList;
}
As you can see it contains the #ManyToMany annotation using the #JoinTable annotation in order to join my network table with my chain table (mapped by the Chain entity class) using the previous network_chain table (implementing the MANY TO MANY relationship).
So in this #JoinTable annotation I am specifying:
The merging table implementing the MANY TO MANY relationship: network_chain.
the two FK on this table that are fk_network_id and fk_chain_id.
Then I have this Spring Data JPA repository class named NetworkRepository:
public interface NetworkRepository extends JpaRepository<Network, Integer> {
/**
* Retrieve a Network object by its ID
* #param id of the network
* #return the retrieve Network object
*/
Network findById(String id);
/**
* Retrieve a Network object by its name
* #param name of the network
* #return a Network object
*/
Network findByName(String name);
/**
* Retrieve the list of all the possible networks
* #return a List<Network> object: the list of the all the networks
*/
List<Network> findAll();
}
Finally I created a JUnit test class containing a method in order to test the previous repository findAll() method, this one:
#SpringBootTest()
#ContextConfiguration(classes = GetUserWsApplication.class)
#TestMethodOrder(OrderAnnotation.class)
public class NetworkTest {
#Autowired
private NetworkRepository networkRepository;
/**
* Retrieve the networks list
*/
#Test
#Order(1)
public void findAllTest() {
List<Network> networksList = this.networkRepository.findAll();
assertTrue(networksList.size() == 5, "It retrieved 5 networks");
Set<Chain> networkChain = networksList.get(0).getChainList();
assertTrue(networkChain != null, "The network chains list are not null");
}
}
The problem is that the findAll() method execute this SQL statement( I can see it into my stacktrace):
Hibernate:
select
network0_.id as id1_6_,
network0_.description as descript2_6_,
network0_.name as name3_6_
from
network network0_
and retrieve the expected List object but as you can see in the following printscreen my chainList field give an error:
It seems that it have not retrieved the chainList from my MANY TO MANY table (infact thre previous Hibernate statement seems not perform any join on the network_chain and then chain tables).
I also tried to directly access to this field by this line (my idea was that maybe Hibernate performed this join when the access to this field is explicitly performed:
Set<Chain> networkChain = networksList.get(0).getChainList();
It seems that this com.sun.jdi.InvocationException exceptions happens when I try to retrieve this field. Basically it seems that the JOINS between the MANY TO MANY table and the on the chain table is never performed.
Why? What is wrong with my code? What am I missing? How can I try to fix it?
This is happening because you have an extra column (id) in the table (network_chain) which is responsible for the many-to-many relationship.
The #JoinTable annotation will support the definition of only 2 fields, usually the ids of the 2 tables. The 2 ids together, will be the primary key for the join table, also known as a composite primary key.
In your case, you will have to create an entity to represent the table network_chain.
#Entity
#Table(name = "network_chain")
public class NetworkChain {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#ManyToOne
private Network network;
#ManyToOne
private Chain chain;
}
public class Network implements Serializable {
//somewhere in the code
#OneToMany
List<NetworkChain> networkChains;
}
public class Chain implements Serializable {
//somewhere in the code
#OneToMany
List<NetworkChain> networkChains;
}
Now, the entity NetworkChain has the extra column id as the primary key and hibernate will do the proper joins to fetch the data.
Out of topic but not sure why there is a javadoc for the findAll() method which is usually part of the JpaRepository and weird findById as well..
public class Primary{
private long id;
#Id
#GeneratedValue(
strategy = GenerationType.IDENTITY
)
#Column(
name = "id"
)
public void getId(){return id;}
//other vars
}
public class Secondary{
//other vars
private Primary primary;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "primary_id", unique = true)
public Primary getPrimary(){return primary;}
}
From this, it is pretty easy to get a Primary Object from a Secondary Object. However, how do I get Secondary Object from a Primary Object without selecting twice in Hibernate?
The Primary class should look like this:
#OneToMany(mappedBy="primary", cascade={CascadeType.ALL})
private List<Secondary> secondaries;
// getter and setter
And you should make a small modification in Secondary:
#JoinColumn(name = "id", unique = true)
public Primary getPrimary(){return primary;}
Cause join column should refer to the joined field(id) in Primary.
The answer could be quite different based on what you are looking for.
Based on your current mapping, assuming you have a Primary instance on hand, you can get its corresponding Secondary by querying. E.g. by HQL:
from Secondary s where s.primary = :primary
and pass in your primary as the parameter.
If you are looking for way to navigate from a Primary object instance, you could have created a bi-directional mapping:
class Primary {
#OneToMany(mappedBy="primary", ...)
private Set<Secondary> secondaries;
}
class Secondary {
#ManyToOne
private Primary primary;
}
You could refer to my (very old) answer on related question on how to define it. https://stackoverflow.com/a/13812047/395202
However, simply having a bi-directional relationship DOES NOT avoid "selecting twice", if your "selecting twice" means running 2 SQL queries in DB.
To reduce such round-trip, there are several way to solve. First one is to declare the relationship as EAGER fetch. However this is a way that I don't usually recommend so I won't go deeper.
Another (imho, more appropriate) way is to do a join fetch when you are fetching Primary. To fetch the Primary instance with its related Secondary instances, use a HQL like :
from Primary p left join fetch p.secondaries where ....
Add this Setter method in the Secondary Class
public Primary getPrimary() {
return primary;
}
And get the primary object from Secondary Class
I'm learning Hibernate (Spring) and facing strange issue with removing child entities from the parent one.
Here is what I have:
Parent entity:
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "company_id", referencedColumnName = "id")
List<CompanyObject> companyObjects;
}
Child entity:
#Entity
public class CompanyObject {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#Enumerated(EnumType.STRING)
ObjectType type;
#ManyToOne
#JoinColumn(name = "company_id")
Company company;
}
Here is my table definitions:
CREATE TABLE `company` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8
CREATE TABLE `company_object` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`company_id` bigint(20) NOT NULL,
`type` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
KEY `FK__company` (`company_id`),
CONSTRAINT `FK__company` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
And, also, I have the following update method:
// some code here
public void update(CompanyDto dto) {
Company company = repository.getCompanyById(companyId);
repository.save(dto.merge(company));
}
// some code here
public class CompanyDto {
private List<CompanyObjectDto> companyObjects = new ArrayList<>();
public Company merge(Company company) {
company.getCompanyObjects().clear();
for (CompanyObjectDto dto : companyObjects) {
company.getCompanyObjects().add(dto.to(company));
}
return company;
}
}
public class CompanyObjectDto {
ObjectType type;
public CompanyObject to(Company company) {
CompanyObject object = new CompanyObject();
object.setType(this.getType());
object.setCompany(company);
return object;
}
}
And as soon as I launch update method, I get the following error: java.sql.SQLWarning: Column 'company_id' cannot be null. I investigated this a little bit and found out that if I comment out company.getCompanyObjects().clear(); string it works ok, so it seems there is some problem with cascading delete action to company objects.
Could, please, somebody point me to my mistakes? Thanks.
You have mapped your entities Company and CompanyObject bidirectionally, i.e. both entities have a relation to the other entity.
In this case, there should only be one #Joincolumn and one entity must be selected as the owning entity, with the other entity marking the relation with a 'mappedby' (see http://docs.oracle.com/javaee/6/api/javax/persistence/ManyToOne.html).
You are getting error because you are removing object's from List and then use the same List as a reference to your Company object. See below code :
private List<CompanyObjectDto> companyObjects = new ArrayList<>(); //Stmt 1
Above code is used to define list which will be reference in your below code :
company.getCompanyObjects().clear(); //It will clear out all objects
for (CompanyObjectDto dto : companyObjects) { //Iterating over empty list defined in stmt 1.
company.getCompanyObjects().add(dto.to(company));
}
So your foreign key will always be null which is not permitted and throws exception.
And your code works when you comment out List#clear line because in that scenario, list already have some referenced objects which didn't modify.
I'm relatively new to JPA and Hibernate and am trying to see how the #OneTo One annotation works, let's say I have an entity "Task" with the following relation:
#OneToOne
#JoinColumn(name = "manager_id")
private Manager manager;
And there's the entity "Manager":
#Entity
#Table(name = "manager")
public class Manager {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
public Manager() {
}
When I run the test file along with the "hibernate.hbm2ddl.auto" set to "update" I get a Many to One relation in the database (as you can see, there is no unique constraint of any kind that'd make it a one to one relation):
CREATE TABLE IF NOT EXISTS `timesheet`.`task` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`completed` BIT(1) NOT NULL,
`description` VARCHAR(255) NULL DEFAULT NULL,
`manager_id` BIGINT(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX `FK3635851B178516` (`manager_id` ASC),
CONSTRAINT `FK3635851B178516`
FOREIGN KEY (`manager_id`)
REFERENCES `timesheet`.`manager` (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
To be sure of this I tried adding two records with the same manager id and were indeed added, I also tried setting the unique constraint like "#Table(name = "Task",uniqueConstraints = #UniqueConstraint(columnNames =..." but no luck.
So Why is this happening and what's exactly the pros of using #OneToOne annotaion if no application logic is applied to validate this?
Also, Is there any chance that Hibernate is not able to do the DDL generation properly?
(I know that generation of schemas through hibernate is only meant for testing)
In a unidirectional relationship you will get the expected unique constraint if you mark it as "optional=false". You also get it if you set the join column explicitly as unique, of course.
So either
#OneToOne(optional=false)
#JoinColumn(name = "manager_id")
private Manager manager;
or
#OneToOne
#JoinColumn(name = "manager_id", unique=true)
private Manager manager;
So why do you need to mark it as not optional?
My guess is that, when a value is optional, the column can contain many null values, but in many databases this can not be done when a unique constraint is present. You can do it in MySQL though, so maybe the Hibernate generator is not taking the database into account in this case (a bug?).
See a discussion about MySQL handling of nulls here.
I had this issue too and I just needed to add the referenced column so I can get a generated table:
#Entity(name = "news")
public class News extends BaseEntity {
#Column(length = 500)
private String title;
#Column(length = 2000)
private String description;
#OneToOne(optional = false, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "file_id", referencedColumnName = "id", unique = true)
private Picture picture;
}
I'm using Hibernate 3.5.2-FINAL with annotations to specify my persistence mappings. I'm struggling with modelling a relationship between an Application and a set of Platforms. Each application is available for a set of platforms.
From all the reading and searching I've done, I think I need to have the platform enum class be persisted as an Entity, and to have a join table to represent the many-to-many relationship. I want the relationship to be unidirectional at the object level, that is, I want to be able to get the list of platforms for a given application, but I don't need to find out the list of applications for a given platform.
Here are my simplified model classes:
#Entity
#Table(name = "TBL_PLATFORM")
public enum Platform {
Windows,
Mac,
Linux,
Other;
#Id
#GeneratedValue
#Column(name = "ID")
private Long id = null;
#Column(name = "NAME")
private String name;
private DevicePlatform() {
this.name = toString();
}
// Setters and getters for id and name...
}
#Entity
#Table(name = "TBL_APP")
public class Application extends AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "NAME")
protected String _name;
#ManyToMany(cascade = javax.persistence.CascadeType.ALL)
#Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE})
#JoinTable(name = "TBL_APP_PLATFORM",
joinColumns = #JoinColumn(name = "APP_ID"),
inverseJoinColumns = #JoinColumn(name = "PLATFORM_ID"))
#ElementCollection(targetClass=Platform.class)
protected Set<Platform> _platforms;
// Setters and getters...
}
When I run the Hibernate hbm2ddl tool, I see the following (I'm using MySQL):
create table TBL_APP_PLATFORM (
APP_ID bigint not null,
PLATFORM_ID bigint not null,
primary key (APP_ID, PLATFORM_ID)
);
The appropriate foreign keys are also created from this table to the application table and platform table. So far so good.
One problem I'm running into is when I try to persist an application object:
Application newApp = new Application();
newApp.setName("The Test Application");
Set<DevicePlatform> platforms = EnumSet.of(Platform.Windows, Platform.Linux);
newApp.setPlatforms(platforms);
applicationDao.addApplication(newApp);
What I would like to happen is for the appropriate rows in the Platform table to created, i.e. create a row for Windows and Linux, if they don't already exist. Then, a row for the new application should be created, and then the mapping between the new application and the two platforms in the join table.
One issue I'm running into is getting the following runtime exception:
2010-06-30 13:18:09,382 6613126-0 ERROR FlushingEventListener Could not synchronize database state with session org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.model.Platform
Somehow, the platform set is not being persisted when I try to persist the application. The cascade annotations are supposed to take care of that, but I don't know what's wrong.
So my questions are:
Is there a better way to model what I want to do, e.g. is using an Enum appropriate?
If my model is alright, how do I properly persist all of the objects?
I've been struggling with this for hours, and I've tried to recreate all of the code above, but it might not be complete and/or accurate. I'm hoping someone will point out something obvious!
You should decide whether your Platform is an entity or not.
If it's an entity, it can't be an enum, because list of possible platforms is stored in the database, not in the application. It should be a regular class with #Entity annotation and you will have a normal many-to-many relation.
If it isn't an entity, then you don't need TBL_PLATFORM table, and you don't have a many-to-many relation. In this case you can represent a set of Platforms either as an integer field with bit flags, or as a simple one-to-many relation. JPA 2.0 makes the latter case simple with #ElementCollection:
#ElementCollection(targetClass = Platform.class)
#CollectionTable(name = "TBL_APP_PLATFORM",
joinColumns = #JoinColumn(name = "APP_ID"))
#Column(name = "PLATFORM_ID")
protected Set<Platform> _platforms;
-
create table TBL_APP_PLATFORM (
APP_ID bigint not null,
PLATFORM_ID bigint not null, -- the ordinal number of enum value
primary key (APP_ID, PLATFORM_ID)
);
and enum Platform without annotations.
Simple use below mapping on your entity. Suppose that we have:
public enum TestEnum { A, B }
Then in your Entity class:
#ElementCollection(targetClass = TestEnum.class)
#CollectionTable(
name = "yourJoinTable",
joinColumns = #JoinColumn(name = "YourEntityId")
)
#Column(name = "EnumId")
private final Set<TestEnum> enumSet= new HashSet<>();
The following example shows what the situation is when Module is an entity and Langue is an enum.
#Entity
public class Module {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String libelle;
#ElementCollection(targetClass = Langue.class, fetch = FetchType.EAGER)
#CollectionTable(name = "link_module_langue",
joinColumns = #JoinColumn(name = "module_id", referencedColumnName = "id"))
#Enumerated(EnumType.STRING)
#Column(name = "langue")
private Set<Langue> langues;
}
public enum Langue {
FRANCAIS, ANGLAIS, ESPAGNOLE
}
You should create link_module_langue table, please see the following sql code :
CREATE TABLE `link_module_langue` (
`module_id` BIGINT(20) NOT NULL,
`langue` VARCHAR(45) NOT NULL,
PRIMARY KEY (`module_id`, `langue`),
CONSTRAINT `module_fk`
FOREIGN KEY (`module_id`)
REFERENCES `module` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE);
NB: Langue is not an entity and would not have its own table.