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