Spring + JPA(Hibernate)OneToMany示例

时间:2020-01-09 10:44:35  来源:igfitidea点击:

在这篇文章中,我们将看到一个与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