hibernate基於註解的維護權反轉:@OneToMany(mappedBy=)
背景說明:首先是SSH環境下,物件基於註解的方式對映到資料庫;
昨天遇到一個比較糾結的問題,@OneToMany(mappedBy="xxx"), mappedBy屬性有什麼用,然後是寫在哪一邊?
還有一個問題是:@JoinColumn(name="xxxxx"),JoinColumn有什麼用?
先貼出最初的程式碼:一些基本的註解,在一對多的關係上沒有使用JoinColumn和mappedBy屬性
部門類:主要是第33、34行
1 package com.lizhou.entity.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import javax.persistence.Column; 7 import javax.persistence.Entity; 8 import javax.persistence.GeneratedValue; 9 import javax.persistence.GenerationType; 10 import javax.persistence.Id; 11 import javax.persistence.OneToMany; 12 import javax.persistence.Table; 13 14 import org.hibernate.annotations.GenericGenerator; 15 16 /** 17 * 部門:與員工一對多關係 18 * @author bojiangzhou 19 * 20 */ 21 @Entity 22 @Table(name="department") 23 public class Department { 24 25 @Id 26 @GeneratedValue(generator="_native") 27 @GenericGenerator(name="_native", strategy="native") 28 private int id; //ID 29 30 @Column(length=20) 31 private String dname; //部門名稱 32 33 @OneToMany 34 private List<Employee> employeeList = new ArrayList<>(); //部門下的員工集合 35 36 // get/set方法59 60 }
員工類:主要是第32、33行
1 package com.lizhou.entity.test; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.Id; 7 import javax.persistence.ManyToOne; 8 import javax.persistence.Table; 9 10 import org.hibernate.annotations.GenericGenerator; 11 12 /** 13 * 員工:與部門多對一關係 14 * @author bojiangzhou 15 * 16 */ 17 @Entity 18 @Table(name="employee") 19 public class Employee { 20 21 @Id 22 @GeneratedValue(generator="_native") 23 @GenericGenerator(name="_native", strategy="native") 24 private int id; //ID 25 26 @Column(length=20) 27 private String ename; //員工姓名 28 29 @Column(length=20) 30 private String phone; //電話 31 32 @ManyToOne 33 private Department department; //所屬部門 34 35 36 //get/set方法67 68 }
最初的註解配置裡,在一對多的關係上,即employeeList和department沒有使用JoinColumn。
看下圖,employee表會自動新增一個外來鍵列department_id,雖然關係對映上是正確了,但是有一個問題,資料庫裡多了一張表出來,這不是想要的結果。
解決方法:在employeeList和department欄位上加上@JoinColumn註解
1 @OneToMany 2 @JoinColumn(name="departmentId") 3 private List<Employee> employeeList = new ArrayList<>(); //部門下的員工集合
1 @ManyToOne// 2 @JoinColumn(name="departmentId")// 3 private Department department; //所屬部門
這樣一來的話就只有兩張表了,所以在一對多或者一對一的關係下,需要加上@JoinColumn來指定外來鍵列,避免生成一張中間表。
而且經試驗,多的一方(Employee)裡的department必須加上@JoinColumn,Department裡不加不會影響表的結構,不知道會不會有其它影響;
但是如果Employee屬於多的一方,如果沒有指定外來鍵列,還是會自動生成一個department_id外來鍵列。
接下來討論mappedBy屬性:mappedBy屬性主要是針對外來鍵而言。與之相對應的是xml中的inverse屬性。
如下是測試類程式碼:此時還沒有設定mappedBy屬性,對映時,預設是都由自身維護關聯關係。
1 package com.lizhou.action.test; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 import org.junit.Test; 8 import org.springframework.context.ApplicationContext; 9 import org.springframework.context.support.ClassPathXmlApplicationContext; 10 11 import com.lizhou.entity.test.Department; 12 import com.lizhou.entity.test.Employee; 13 14 /** 15 * 測試類 16 * @author bojiangzhou 17 * 18 */ 19 20 public class TestAction { 21 22 private static SessionFactory sessionFactory = null; 23 24 static { 25 //讀取classpath中applicationContext.xml配置檔案 26 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); 27 //獲取session中配置的sessionFactory物件 28 sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory"); 29 } 30 31 @Test 32 public void testSave(){ 33 //建立一個部門物件 34 Department d1 = new Department(); 35 d1.setDname("研發部"); 36 37 //建立兩個員工物件 38 Employee e1 = new Employee(); 39 e1.setEname("張三"); 40 e1.setPhone("13111111111"); 41 Employee e2 = new Employee(); 42 e2.setEname("李四"); 43 e2.setPhone("18523222222"); 44 45 //設定物件關聯 46 d1.getEmployeeList().add(e1); 47 d1.getEmployeeList().add(e2); 48 e1.setDepartment(d1); 49 e2.setDepartment(d1); 50 51 //獲取Session 52 Session session = sessionFactory.openSession(); 53 //開始事務 54 Transaction t = session.beginTransaction(); 55 try { 56 //新增資料 57 session.save(d1); 58 session.save(e1); 59 session.save(e2); 60 //提交事務 61 t.commit(); 62 } catch (RuntimeException e) { 63 //有異常則回滾事務 64 t.rollback(); 65 e.printStackTrace(); 66 } finally { 67 //關閉session 68 session.close(); 69 } 70 } 71 72 73 }
執行testSave後,控制檯列印如下語句:
1 Hibernate: insert into department (dname) values (?) 2 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?) 3 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?) 4 Hibernate: update employee set departmentId=? where id=? 5 Hibernate: update employee set departmentId=? where id=?
可以看到多了兩條update語句,這是因為兩邊都維護關係,先插入的部門,再插入員工,插入員工時,已經設定好外來鍵了,但部門方也維護關係,會再執行一次更新操作,為員工設定外來鍵,這樣就導致多出了兩條update語句,這裡是有效能損耗的。
一種解決辦法是:將第46、47行去掉,即物件上部門不關聯員工
1 package com.lizhou.action.test; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 import org.junit.Test; 8 import org.springframework.context.ApplicationContext; 9 import org.springframework.context.support.ClassPathXmlApplicationContext; 10 11 import com.lizhou.entity.test.Department; 12 import com.lizhou.entity.test.Employee; 13 14 /** 15 * 測試類 16 * @author bojiangzhou 17 * 18 */ 19 20 public class TestAction { 21 22 private static SessionFactory sessionFactory = null; 23 24 static { 25 //讀取classpath中applicationContext.xml配置檔案 26 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); 27 //獲取session中配置的sessionFactory物件 28 sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory"); 29 } 30 31 @Test 32 public void testSave(){ 33 //建立一個部門物件 34 Department d1 = new Department(); 35 d1.setDname("研發部"); 36 37 //建立兩個員工物件 38 Employee e1 = new Employee(); 39 e1.setEname("張三"); 40 e1.setPhone("13111111111"); 41 Employee e2 = new Employee(); 42 e2.setEname("李四"); 43 e2.setPhone("18523222222"); 44 45 //設定物件關聯 46 // d1.getEmployeeList().add(e1); 47 // d1.getEmployeeList().add(e2); 48 e1.setDepartment(d1); 49 e2.setDepartment(d1); 50 51 //獲取Session 52 Session session = sessionFactory.openSession(); 53 //開始事務 54 Transaction t = session.beginTransaction(); 55 try { 56 //新增資料 57 session.save(d1); 58 session.save(e1); 59 session.save(e2); 60 //提交事務 61 t.commit(); 62 } catch (RuntimeException e) { 63 //有異常則回滾事務 64 t.rollback(); 65 e.printStackTrace(); 66 } finally { 67 //關閉session 68 session.close(); 69 } 70 } 71 72 73 }
1 Hibernate: insert into department (dname) values (?) 2 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?) 3 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?)
這樣部門方就不會去維護外來鍵關係了。但是有一個問題,物件上就沒有關聯了,我們要做的是物件上要互相關聯,資料庫方面只讓一方去維護關係即可。
物件上如果不關聯,因為部門和員工新增到資料庫後,是持久化狀態,存在於session快取中,那session操作快取中這幾個物件時,部門就沒有關聯員工了,那麼就還得再查詢一次資料庫,這不是想要的結果。
這時就要用到mappedBy屬性了。
在一的一方配置@OneToMany(mappedBy="department"),將維護權交由多的一方來維護;
那為什麼不讓多的一方交出維護權,讓一的一方來維護呢?上面的實驗也表明瞭如果讓一的一方來維護,始終都會多出兩條update語句,因為外來鍵是在多的這一方的,所以維護權應該交由多的一方。
部門類的配置:第36行和第37行的配置,部門部門交出維護權利,讓對方來維護
1 package com.lizhou.entity.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.FetchType; 10 import javax.persistence.GeneratedValue; 11 import javax.persistence.GenerationType; 12 import javax.persistence.Id; 13 import javax.persistence.JoinColumn; 14 import javax.persistence.OneToMany; 15 import javax.persistence.Table; 16 17 import org.hibernate.annotations.GenericGenerator; 18 19 /** 20 * 部門:與員工一對多關係 21 * @author bojiangzhou 22 * 23 */ 24 @Entity 25 @Table(name="department") 26 public class Department { 27 28 @Id 29 @GeneratedValue(generator="_native") 30 @GenericGenerator(name="_native", strategy="native") 31 private int id; //ID 32 33 @Column(length=20) 34 private String dname; //部門名稱 35 36 @OneToMany(mappedBy="department") 37 private List<Employee> employeeList = new ArrayList<>(); //部門下的員工集合 38 39 // get/set方法62 63 }
員工類的配置不變。
呼叫testSave時,部門和員工再物件上依然是關聯的:第46-49行
1 package com.lizhou.action.test; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 import org.junit.Test; 8 import org.springframework.context.ApplicationContext; 9 import org.springframework.context.support.ClassPathXmlApplicationContext; 10 11 import com.lizhou.entity.test.Department; 12 import com.lizhou.entity.test.Employee; 13 14 /** 15 * 測試類 16 * @author bojiangzhou 17 * 18 */ 19 20 public class TestAction { 21 22 private static SessionFactory sessionFactory = null; 23 24 static { 25 //讀取classpath中applicationContext.xml配置檔案 26 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); 27 //獲取session中配置的sessionFactory物件 28 sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory"); 29 } 30 31 @Test 32 public void testSave(){ 33 //建立一個部門物件 34 Department d1 = new Department(); 35 d1.setDname("研發部"); 36 37 //建立兩個員工物件 38 Employee e1 = new Employee(); 39 e1.setEname("張三"); 40 e1.setPhone("13111111111"); 41 Employee e2 = new Employee(); 42 e2.setEname("李四"); 43 e2.setPhone("18523222222"); 44 45 //設定物件關聯 46 d1.getEmployeeList().add(e1); 47 d1.getEmployeeList().add(e2); 48 e1.setDepartment(d1); 49 e2.setDepartment(d1); 50 51 //獲取Session 52 Session session = sessionFactory.openSession(); 53 //開始事務 54 Transaction t = session.beginTransaction(); 55 try { 56 //新增資料 57 session.save(d1); 58 session.save(e1); 59 session.save(e2); 60 //提交事務 61 t.commit(); 62 } catch (RuntimeException e) { 63 //有異常則回滾事務 64 t.rollback(); 65 e.printStackTrace(); 66 } finally { 67 //關閉session 68 session.close(); 69 } 70 } 71 72 73 }
控制檯列印的語句:只有三條插入語句,沒有更新語句了
1 Hibernate: insert into department (dname) values (?) 2 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?) 3 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?)
這裡遇到一個問題:如果配置mappedBy屬性的同時加上@JoinColumn會丟擲異常,所以不能同時使用@JoinColumn和mappedBy;因為@JoinColumn本身就是自己來維護外來鍵,和mappedBy衝突了。--->>>不知道這樣理解正確否!!^_^
1 package com.lizhou.entity.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.FetchType; 10 import javax.persistence.GeneratedValue; 11 import javax.persistence.GenerationType; 12 import javax.persistence.Id; 13 import javax.persistence.JoinColumn; 14 import javax.persistence.OneToMany; 15 import javax.persistence.Table; 16 17 import org.hibernate.annotations.GenericGenerator; 18 19 /** 20 * 部門:與員工一對多關係 21 * @author bojiangzhou 22 * 23 */ 24 @Entity 25 @Table(name="department") 26 public class Department { 27 28 @Id 29 @GeneratedValue(generator="_native") 30 @GenericGenerator(name="_native", strategy="native") 31 private int id; //ID 32 33 @Column(length=20) 34 private String dname; //部門名稱 35 36 @OneToMany(mappedBy="department") 37 @JoinColumn(name="departmentId") 38 private List<Employee> employeeList = new ArrayList<>(); //部門下的員工集合 39 40 // set/get 方法63 64 }
丟擲如下異常:
1 java.lang.ExceptionInInitializerError 2 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 3 at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) 4 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) 5 at java.lang.reflect.Constructor.newInstance(Unknown Source) 6 at org.junit.runners.BlockJUnit4ClassRunner.createTest(BlockJUnit4ClassRunner.java:217) 7 at org.junit.runners.BlockJUnit4ClassRunner$1.runReflectiveCall(BlockJUnit4ClassRunner.java:266) 8 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 9 at org.junit.runners.BlockJUnit4ClassRunner.methodBlock(BlockJUnit4ClassRunner.java:263) 10 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) 11 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) 12 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) 13 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) 14 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) 15 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) 16 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) 17 at org.junit.runners.ParentRunner.run(ParentRunner.java:363) 18 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) 19 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) 20 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) 21 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675) 22 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) 23 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192) 24 Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory' defined in class path resource [applicationContext.xml]: Invocation of init method failed; nested exception is org.hibernate.AnnotationException: Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: com.lizhou.entity.test.Department.employeeList 25 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1553) 26 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539) 27 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475) 28 at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:302) 29 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) 30 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:298) 31 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:193) 32 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:684) 33 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760) 34 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482) 35 at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139) 36 at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83) 37 at com.lizhou.action.test.TestAction.<clinit>(TestAction.java:26) 38 ... 22 more 39 Caused by: org.hibernate.AnnotationException: Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: com.lizhou.entity.test.Department.employeeList 40 at org.hibernate.cfg.annotations.CollectionBinder.bind(CollectionBinder.java:493) 41 at org.hibernate.cfg.AnnotationBinder.processElementAnnotations(AnnotationBinder.java:2156) 42 at org.hibernate.cfg.AnnotationBinder.processIdPropertiesIfNotAlready(AnnotationBinder.java:963) 43 at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:796) 44 at org.hibernate.cfg.Configuration$MetadataSourceQueue.processAnnotatedClassesQueue(Configuration.java:3788) 45 at org.hibernate.cfg.Configuration$MetadataSourceQueue.processMetadata(Configuration.java:3742) 46 at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1410) 47 at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1844) 48 at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1928) 49 at org.springframework.orm.hibernate4.LocalSessionFactoryBuilder.buildSessionFactory(LocalSessionFactoryBuilder.java:343) 50 at org.springframework.orm.hibernate4.LocalSessionFactoryBean.buildSessionFactory(LocalSessionFactoryBean.java:431) 51 at org.springframework.orm.hibernate4.LocalSessionFactoryBean.afterPropertiesSet(LocalSessionFactoryBean.java:416) 52 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1612) 53 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1549) 54 ... 34 more
還有一點說明下:
如果將第57行程式碼移到第59行後面,即先儲存員工,再儲存部門,會多出四條update語句
1 package com.lizhou.action.test; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 import org.junit.Test; 8 import org.springframework.context.ApplicationContext; 9 import org.springframework.context.support.ClassPathXmlApplicationContext; 10 11 import com.lizhou.entity.test.Department; 12 import com.lizhou.entity.test.Employee; 13 14 /** 15 * 測試類 16 * @author bojiangzhou 17 * 18 */ 19 20 public class TestAction { 21 22 private static SessionFactory sessionFactory = null; 23 24 static { 25 //讀取classpath中applicationContext.xml配置檔案 26 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); 27 //獲取session中配置的sessionFactory物件 28 sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory"); 29 } 30 31 @Test 32 public void testSave(){ 33 //建立一個部門物件 34 Department d1 = new Department(); 35 d1.setDname("研發部"); 36 37 //建立兩個員工物件 38 Employee e1 = new Employee(); 39 e1.setEname("張三"); 40 e1.setPhone("13111111111"); 41 Employee e2 = new Employee(); 42 e2.setEname("李四"); 43 e2.setPhone("18523222222"); 44 45 //設定物件關聯 46 d1.getEmployeeList().add(e1); 47 d1.getEmployeeList().add(e2); 48 e1.setDepartment(d1); 49 e2.setDepartment(d1); 50 51 //獲取Session 52 Session session = sessionFactory.openSession(); 53 //開始事務 54 Transaction t = session.beginTransaction(); 55 try { 56 //新增資料 57 session.save(e1); 58 session.save(e2); 59 session.save(d1); 60 //提交事務 61 t.commit(); 62 } catch (RuntimeException e) { 63 //有異常則回滾事務 64 t.rollback(); 65 e.printStackTrace(); 66 } finally { 67 //關閉session 68 session.close(); 69 } 70 } 71 72 73 }
1 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?) 2 Hibernate: insert into employee (departmentId, ename, phone) values (?, ?, ?) 3 Hibernate: insert into department (dname) values (?) 4 Hibernate: update employee set departmentId=?, ename=?, phone=? where id=? 5 Hibernate: update employee set departmentId=?, ename=?, phone=? where id=? 6 Hibernate: update employee set departmentId=? where id=? 7 Hibernate: update employee set departmentId=? where id=?
很明顯,在插入員工時,還沒有部門的資訊,等插入部門的時候,員工方會維護外來鍵關係,更新外來鍵;而部門方也會維護一次,所以多了四條語句。所以在新增資料的時候先儲存一的一方,再儲存多的一方。
總結:mappedBy屬性跟xml配置檔案裡的inverse一樣。在一對多或一對一的關係對映中,如果不表明mappedBy屬性,預設是由本方維護外來鍵。但如果兩方都由本方來維護的話,會多出一些update語句,效能有一定的損耗。
解決的辦法就是在一的一方配置上mappedBy屬性,將維護權交給多的一方來維護,就不會有update語句了。
至於為何要將維護權交給多的一方,可以這樣考慮:要想一個國家的領導人記住所有人民的名字是不可能的,但可以讓所有人民記住領導人的名字!
注意,配了mappedBy屬性後,不要再有@JoinColumn,會衝突!
OK!!!