Spring + JPA(Hibernate)OneToMany示例
在这篇文章中,我们将看到一个与JPA(Hibernate JPA实现)进行Spring集成的示例,所用的数据库是MySQL。对于该示例,使用具有双向关联的两个表。
如果我们想了解如何创建Maven项目,请查看此文章-在Eclipse中使用Maven创建Java项目
Maven依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.theitroad</groupId>
<artifactId>SpringProject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>SpringProject</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.1.8.RELEASE</spring.version>
<spring.data>2.1.10.RELEASE</spring.data>
<hibernate.jpa>5.4.3.Final</hibernate.jpa>
<mysql.version>8.0.17</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.jpa}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<release>10</release>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<warSourceDirectory>WebContent</warSourceDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
为Spring核心,Spring上下文和Spring ORM添加了相关性。
随着使用Hibernate JPA实现,添加了Hibernate的依赖关系(hibernate-entitymanager)。这个依赖hibernate-entitymanager也像hibernate-core一样获取所有依赖的jar。
MySQL连接器用于从Java应用程序连接到MySQL DB。
数据库表
有雇员和帐户两个表,其中一个雇员可能有多个帐户。对于该一对多关系,帐户表中有一个外键约束,其中雇员表(id)中的主键被添加为帐户中的外键。
CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `first_name` varchar(45) DEFAULT NULL, `last_name` varchar(45) DEFAULT NULL, `department` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; CREATE TABLE `account` ( `acct_id` int(11) NOT NULL AUTO_INCREMENT, `acct_no` varchar(45) NOT NULL, `emp_id` int(11) NOT NULL, PRIMARY KEY (`acct_id`), UNIQUE KEY `acct_no_UNIQUE` (`acct_no`), KEY `id_idx` (`emp_id`), CONSTRAINT `emp_fk` FOREIGN KEY (`emp_id`) REFERENCES `employee` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Spring JPA示例实体类
映射到数据库表的实体类。
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name="account")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="acct_id")
private int id;
@Column(name="acct_no", unique=true)
private String accountNumber;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emp_id", nullable = false)
private Employee employee;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
以下是帐户实体类的一些要点:
1员工可以使用字段上的@ManyToOne注释在JPA中将多重性映射到多个帐户(也可以在getter上完成)。
2.可以从字段的类型中自动推导目标实体(在这种情况下为雇员)。
3. @JoinColumn注释指定用于连接实体关联或者元素集合的列,在此情况下为外键列。
4.如果要将外键列设为NOT NULL,则需要将属性设置为nullable = false。如果我们正在使用Hibernate工具生成表,这将很有帮助。
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name="employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id", nullable = false)
private int id;
@Column(name="first_name")
private String firstName;
@Column(name="last_name")
private String lastName;
@Column(name="department")
private String dept;
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL)
private Set<Account> accounts;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public Set<Account> getAccounts() {
return accounts;
}
public void setAccounts(Set<Account> accounts) {
this.accounts = accounts;
}
@Override
public String toString() {
return "Id= " + getId() + " First Name= " +
getFirstName() + " Last Name= " + getLastName() +
" Dept= "+ getDept();
}
}
以下是有关Employee实体类的一些要点:
1.员工可以容纳许多帐户,以适应添加了可以存储帐户的Set引用。
2. @ OneToMany注释定义了一个多值关联。
3.如果关系是双向的,则必须使用mappedBy元素来指定作为关系所有者的实体的关系字段或者属性。
4.使用CascadeType,我们可以指定传播到关联实体的操作。 CascadeType.ALL级联所有操作(PERSIST,REMOVE,REFRESH,MERGE,DETACH)
双向协会
尽管在此示例中使用了双向关联,但它可能无法满足所有要求。
双向关联使获取关联集合变得很方便,而无需显式编写任何查询,但是与此同时,对象图可能非常庞大和复杂,而获取它可能会使整个应用程序变慢。
即使我们获取整个关联,由于延迟加载,Hibernate可能也不会获取子关联,在这种情况下,只有当我们尝试访问它们时才获取集合的内容。如果我们在会话已关闭时尝试访问集合元素,则可能导致LazyInitializationException。
因此,在很多情况下,最好使用单向关联(仅@ManyToOne端)。
DAO类
public interface EmployeeDAO {
public void addEmployee(Employee emp);
public List<Employee> findAllEmployees();
public Employee findEmployeeById(int id);
public void deleteEmployeeById(int id);
}
@Repository
public class EmployeeDAOImpl implements EmployeeDAO {
@PersistenceContext
private EntityManager em;
@Override
public void addEmployee(Employee emp) {
em.persist(emp);
}
@Override
public List<Employee> findAllEmployees() {
List<Employee> employees = em.createQuery("Select emp from Employee emp", Employee.class)
.getResultList();
return employees;
}
@Override
public Employee findEmployeeById(int id) {
//Employee emp = em.find(Employee.class, id);
Employee emp = em.createQuery("SELECT e FROM Employee e INNER JOIN e.accounts a where e.id = :id", Employee.class)
.setParameter("id", id)
.getSingleResult();
return emp;
}
@Override
public void deleteEmployeeById(int id) {
Employee emp = findEmployeeById(id);
em.remove(emp);
}
}
请注意,在EmployeeDAOImpl类上使用了@Repository注释,这使它成为组件,并且在完成组件扫描后有资格注册为Spring Bean。
服务等级
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.theitroad.springproject.dao.EmployeeDAO;
import com.theitroad.springproject.model.Account;
import com.theitroad.springproject.model.Employee;
@Service
public class EmployeeService {
@Autowired
private EmployeeDAO dao;
@Transactional
public Employee getEmployeeById(int id) {
Employee emp = dao.findEmployeeById(id);
emp.getAccounts();
System.out.println(emp.toString());
for(Account acct: emp.getAccounts()) {
System.out.println("Acct No- " + acct.getAccountNumber());
}
return emp;
}
@Transactional
public List<Employee> getAllEmployees(){
return (List<Employee>) dao.findAllEmployees();
}
@Transactional
public void addEmployee(Employee emp) {
dao.addEmployee(emp);
}
@Transactional
public void deleteEmployeeById(int id) {
dao.deleteEmployeeById(id);
}
}
EmployeeService对EmployeeDAO有依赖关系,可以使用@Autowired注释来满足它。从服务类中调用DAO中的方法。
配置类
在此Spring数据JPA示例中,使用Java配置,因此使用@Configuration注释对类进行注释。
为了从属性文件中读取DataSource DB属性,使用@PropertySource注释配置属性文件(config / db.properties)的路径。
@EnableTransactionManagement注释启用Spring的注释驱动的事务管理功能。
@ComponentScan注释使用作为基本包提供的路径启用组件扫描。
在此Java配置类中,我们设置了EntityManagerFactory并将Hibernate用作持久性提供程序。通过使用setPackagesToScan方法,可以提供实体类所在的包的路径,因为这样做不需要persistence.xml配置文件。
import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.theitroad.springproject")
@PropertySource("classpath:config/db.properties")
public class AppConfig {
@Autowired
private Environment env;
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
// Where Entity classes reside
factory.setPackagesToScan("com.theitroad.springproject.model");
factory.setDataSource(dataSource());
factory.setJpaProperties(hibernateProperties());
return factory;
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(env.getProperty("db.driverClassName"));
ds.setUrl(env.getProperty("db.url"));
ds.setUsername(env.getProperty("db.username"));
ds.setPassword(env.getProperty("db.password"));
return ds;
}
Properties hibernateProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", env.getProperty("hibernate.sqldialect"));
properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.showsql"));
return properties;
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory().getObject());
return txManager;
}
}
config / db.properties
db.driverClassName=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/theitroad db.username=root db.password=admin hibernate.sqldialect=org.hibernate.dialect.MySQLDialect hibernate.showsql=true
Spring JPA示例测试
为了运行我们的Spring ORM JPA Hibernate示例,我们可以使用以下测试程序来添加新员工和关联的帐户。
import java.util.HashSet;
import java.util.Set;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import com.theitroad.springproject.model.Account;
import com.theitroad.springproject.model.Employee;
import com.theitroad.springproject.service.EmployeeService;
public class App {
public static void main( String[] args ){
AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
EmployeeService empService = context.getBean("employeeService", EmployeeService.class);
Employee emp = new Employee();
emp.setFirstName("Hyman");
emp.setLastName("Cullinan");
emp.setDept("Finance");
Account acct1 = new Account();
acct1.setAccountNumber("123yur34");
acct1.setEmployee(emp);
Account acct2 = new Account();
acct2.setAccountNumber("123yur35");
acct2.setEmployee(emp);
Set<Account> accounts = new HashSet<Account>();
accounts.add(acct1);
accounts.add(acct2);
emp.setAccounts(accounts);
empService.addEmployee(emp);
//Employee employee = empService.getEmployeeById(9);
context.close();
}
}
为了保存实体,会触发以下Hibernate查询。
Hibernate: insert into employee (department, first_name, last_name) values (?, ?, ?) Hibernate: insert into account (acct_no, emp_id) values (?, ?) Hibernate: insert into account (acct_no, emp_id) values (?, ?)
用于通过ID获取员工。
public class App {
public static void main( String[] args ){
//EntityManager
AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
EmployeeService empService = context.getBean("employeeService", EmployeeService.class);
Employee employee = empService.getEmployeeById(10);
//empService.deleteEmployeeById(5);
context.close();
}
}
从日志中,我们可以看到,由于延迟加载,首次选择查询仅获取员工信息,而未获取关联的帐户。仅当访问帐户信息时,才会触发用于获取帐户的选择查询。
Hibernate: select employee0_.id as id1_1_, employee0_.department as departme2_1_, employee0_.first_name as first_na3_1_, employee0_.last_name as last_nam4_1_ from employee employee0_ inner join account accounts1_ on employee0_.id=accounts1_.emp_id where employee0_.id=? Id= 10 First Name= Hyman Last Name= Cullinan Dept= Finance Hibernate: select accounts0_.emp_id as emp_id3_0_0_, accounts0_.acct_id as acct_id1_0_0_, accounts0_.acct_id as acct_id1_0_1_, accounts0_.acct_no as acct_no2_0_1_, accounts0_.emp_id as emp_id3_0_1_ from account accounts0_ where accounts0_.emp_id=? Acct No- 123yur34 Acct No- 123yur35
另请注意,显示帐户信息的位置已放入交易结束的EmployeeService中。如果我们在会话结束后尝试访问帐户信息,则将收到LazyInitializationException。这是使用双向关联的缺点之一。
Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.theitroad.springproject.model.Employee.accounts, could not initialize proxy - no Session

