I am learning Hibernate, and training with below example using one-to-many, many-to-one and many-to-many associations.
Simplifying for the sake of the query:
-
I have Java classes User, Country and Languages. One User can only live in one Country (so relation is many-to-one) and can speak several Languages (so relation is many-to-many)
-
I have database tables ‘users’, ‘countries’, ‘languages’ and ‘users_languages’. The ‘country_id’ is stored in the ‘users’ table (because there is only one per user), and the several ‘language_id’ and ‘user_id’ are stored in the ‘users_languages’ table.
-
I have prefilled the tables ‘countries’ and ‘languages’, as these are fixed values, so that I do not want to add/remove any of these values from these tables. They will be associated to the different users.
The schema of the tables (I removed columns for brevity):
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`country_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`),
CONSTRAINT `u_country_fk` FOREIGN KEY (`country_id`) REFERENCES `countries` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
)
CREATE TABLE `countries` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name_UNIQUE` (`name`)
)
CREATE TABLE `languages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`language` varchar(15) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `language_UNIQUE` (`language`)
)
CREATE TABLE `users_languages` (
`user_id` int(11) NOT NULL,
`language_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`language_id`),
CONSTRAINT `ul_language_fk` FOREIGN KEY (`language_id`) REFERENCES `languages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `ul_user_fk` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
)
As commented, I already inserted the needed rows in tables ‘countries’ and ‘languages’ so that those will be the only available for the users.
The corresponding Java entities:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@NaturalId
@Column(name = "username", nullable = false, unique = true)
private String username;
@ManyToOne
@JoinColumn(name="country_id",
foreignKey = @ForeignKey(name = "u_country_fk"))
private Country country;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@LazyCollection(LazyCollectionOption.FALSE)
@JoinTable(
name = "users_languages",
joinColumns = @JoinColumn(name = "user_id", nullable = false),
inverseJoinColumns = @JoinColumn(name = "language_id", nullable = false)
)
private List<Language> languages;
public User() {
this.languages = new ArrayList<>();
}
public User(String username, Country country) {
this.username = username;
this.country = country;
this.languages = new ArrayList<>();
}
public void addLanguage(Language language) {
this.languages.add(language);
language.getUsers().add(this);
}
public void removeLanguage(Language language) {
this.languages.remove(language);
language.getUsers().remove(this);
}
public void removeAllLanguages() {
this.languages.clear();
}
// getters and setters omitted
}
@Entity
@Table(name = "countries")
public class Country {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@NaturalId
@Column(name = "name", nullable = false, unique = true)
private String name;
// @OneToMany(cascade = CascadeType.ALL)
// @JoinColumn(name="id")
// private Set<User> users;
public Country() {
// users = new HashSet<>();
}
public Country(String name) {
this.name = name;
// users = new HashSet<>();
}
// getters and setters omitted
}
@Entity
@Table(name = "languages")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@NaturalId
@Column(name = "language", nullable = false, unique = true)
private String language;
@ManyToMany(mappedBy = "languages")
Set<User> users;
public Language() {
users = new HashSet<>();
}
public Language(String language) {
this.language = language;
users = new HashSet<>();
}
// getters and setters omitted
}
When creating a new user, I have had issues with the @GeneratedValue, because in the batch I have to store several languages. Finally the one which worked for me is GenerationType.IDENTITY, but I read about the extra query required by Hibernate to know the next id to use. Furthermore, because the batch, if I create a new object Language (which already exists in the database), there is an issue with already existing unique key (I expected that Hibernate could manage this automatically). The same for new object Country. Therefore, instead of creating new objects, as the corresponding rows already exist in the database, I am getting them from the database, to associate them to the user being created. But I think that Hibernate will do again the same queries (check the existence in the database of the objects Language and Country being associated to the user), so that probably I am duplicating the number of required queries, reducing the performance. See code snippet below:
public int addUser(SessionFactory factory, String username, String countryName, String[] languages) {
User user = null;
try (Session session = factory.openSession()) {
Transaction transaction = session.beginTransaction();
Country country = (Country) session
.createQuery("FROM Country WHERE name = :country_name")
.setParameter("country_name", countryName).getSingleResult();
user = new User(username, password, name, email, gender, country);
for (String language : languages) {
Language lang = (Language) session
.createQuery("FROM Language WHERE language = :language")
.setParameter("language", language).getSingleResult();
user.addLanguage(lang);
}
session.persist(user);
transaction.commit();
}
return user==null ? -1 : user.getId();
}
Can someone please help me with the below points:
-
Am I using the correct strategy for generating the ids?
-
Am I using the correct cascade methodology (both in database and Hibernate)?
-
Is there a better way for the above procedure for addigin a user, reducing the total number of queries to the database / increasing the performance? I.e., instead of asking the database for existing Country, cannot I just create new Country object and Hibernate will be able to use the existing record in the database? Same for Language objects?
-
Should I make the Country association biderectional as well? Recommendations about uniderectional/bidirectional for both Country and Language classes.
-
Any other suggestion / improvement I should apply with Hibernate?
Many thanks in advance,
Javier