Google Code Prettify

2016年12月25日 星期日

JPA + Spring: many-to-many

上一篇整理的是 one-to-many (一對多) 的狀況,這一篇要整理的是 many-to-many (多對多) 的存取。

上一篇的狀況是一個使用者可以開多個飯局邀請其他使用者加入,是一對多,而一個使用者可以加入多個飯局,一個飯局也可以有多個使用者加入,形成了 USERS 和 APPOINTMENT 間的多對多關係。在關聯式資料庫中就會如上圖,多出第三方的 table,用來記錄多對多的關係。

在 JPA 中要怎麼表示這種多對多的關係? 修改 class Appointment 如下:
 1 @Entity
 2 @Table(
 3     name="APPOINTMENT",
 4     uniqueConstraints = {
 5         @UniqueConstraint(columnNames = "SYSID")
 6     })
 7 public class Appointment implements Serializable {
 8     private static final long serialVersionUID = -3781642274581820116L;
 9     
10     private String email;
11     private String name;
12     private Long restaurant_sysid;
13     private String description; 
14     private byte[] photo;
15     private Integer people;
16     private Integer willPay;
17     private String payKind;
18     private String cancel;
19     private Date htime;
20     private Date ctime;
21     
22     @ManyToOne(fetch = FetchType.LAZY)
23     @JoinColumn(name = "email", insertable = false, updatable = false)
24     private Users users;
25     
26     @ManyToMany(fetch = FetchType.LAZY)
27     @JoinTable(name = "PARTICIPANT",
28         joinColumns = { @JoinColumn(name = "APPOINTMENT_SYSID") },
29         inverseJoinColumns = { @JoinColumn (name = "EMAIL") })
30     private List<Users> particpants;
31 
32     //省略
33 
34     public Users getUsers() {
35         return users;
36     }
37     
38     public void setUsers(Users users) {
39         this.users = users;
40     }
41     
42     public List<Users> getParticpants() {
43         return particpants;
44     }
45     public void setParticpants(List<Users> particpants) {
46         this.particpants = particpants;
47     }
48 }
  1. 第24行: 這個 users 是指開飯局的人,只會有一個,是多對一的關係。
  2. 第30行: 這是參與飯局的人,可以有多個,是多對多的關係,在 JPA 中使用 @ManyToMany 來設定,JoinTable 中的 name 屬性為第三方的 table,joinColumns 屬性是第三方 table 與 table APPOINTMENT 間的對應欄位,inverseJoinColumns 則是第三方 table 與多方的另一邊 (table USERS) 的對應欄位。
測試程式如下:
要特別注意第1行一定要加 @Transactional,因為上面程式第26行是設定為 FetchType.LAZY,在執行下面程式第4行時只會讀取 table APPOINTMENT 的資料,要到第 6 行真的使用到 Users 的資料時,才會再到資料庫將 table USERS 中的相關資料取出來,在 JPA 的 CRUD 中,都要有 transaction 才能讀取,所以測試程式要加 @Transactional 才會有動作,不加雖然不會產生錯誤,但實際上沒有到資料庫讀資料。
1     @Transactional
2     @Test
3     public void testFindFromAppointment() {
4         Appointment a = daoApp.find("hi.steven@gmail.com");
5         
6         for(Users u:a.getParticpants()) {
7             System.out.println(u.toString());
8         }
9     }






【日劇 - 夏目漱石之妻】
尾野真千子主演只有四集的連續劇,改編自夏目漱石老婆 (中根鏡子 -> 夏目鏡子) 所著的「漱石的回憶」。從這部戲可以看到,19世紀末,那個嚴重男尊女卑的社會,女人選擇婚姻,就像賭博一樣,而且是把全部的身家都拿出來賭,是一場貫穿後半輩子的豪賭。
片中夏目漱石 (長谷川博己 飾) 是個精神耗弱且會家暴的大男人,這種家暴在當時的日本社會可能是很平常的事吧? 他老婆看來沒有太不能接受。陪著一個這麼不懂體貼又會家暴的男人幾十年,不知道她覺得自己是否賭對了? 從影片最後一幕的劇情來看,是要說她賭對了吧?! 雙方最後還是挺恩愛的,不過,以21世紀的價值觀來看,這樣的婚姻恐怕是沒那麼幸福。
即使到了20世紀末,年輕男女到了適婚年齡都會面臨強大的社會、家庭壓力,壓迫著這些未婚男女走入婚姻,還把「白頭偕老」當成重大成就,而實際上很多人同住一個屋簷下,彼此間並沒有太多感情,就算白頭偕老,也沒有幸福感。還好,至少現在的台灣社會這樣的困擾不再那麼嚴重,想單身、想在一起卻不結婚 … 都可以,來自社會、家庭的壓力比以前小多了,可以更自由的選擇自己的人生。


2016年12月24日 星期六

JPA + Spring: one-to-many (I)

上一篇「JPA + Hibernate + Spring + JUnit (annotation)」已經說明了一些環境的基本設定,接下來要整理的是以 JPA annotation 進行 one-to-many 的存取。

如上圖所示,一個使用者可以開很多飯局邀請朋友加入,所以 USERS 對 APPOINTMENT 是一對多的關係,所以,先修改 class Users 如下:
 1 @Entity
 2 @Table(
 3     name="USERS",
 4     uniqueConstraints = {
 5         @UniqueConstraint(columnNames = "EMAIL")
 6     })
 7 public class Users implements Serializable {
 8     private static final long serialVersionUID = 3965333920543479036L;
 9     
10     private String email;
11     private String password;
12     private String phone1;
13     private String phone2;
14     private String nickname;
15     private String city;
16     private String gender;
17     private String birthday;
18     private String industry;
19     private String occupation;
20     private String description;
21     private Date vtime;
22     private Date ctime;
23     private Date lastLogin;
24     
25     private Set<Appointment> appointments;
26 
27     //省略    
28 
29     @OneToMany(mappedBy="users", cascade=CascadeType.ALL, fetch=FetchType.EAGER, orphanRemoval=true)
30     public Set<Appointment> getAppointments() {
31         return appointments;
32     }
33 
34     public void setAppointments(Set<Appointment> appointments) {
35         this.appointments = appointments;
36     }    
37 }
  1.  第25行: 一個人使用者可以開多個飯局,所以在 class Users 中包含一個 Set<Appointment> 或 List<Appointment> 的變數,用來指向 class Appointment,以存取 table APPOINTMENT 中的資料。
  2. 第29行: 在 getter method 前加入 @OneToMany,mappedBy 指出是要對應到那一個類別; cascade 設定兩者的關聯性,詳細會另篇說明,這裡設定為 ALL,就是所有刪除、新增等都會有關聯; fetch 有兩個值 EAGER 及 LAZY,EAGER 是在存取 Users 時,就會把相關聯的 Appintment 也抓出來,LAZY 則會等到真的要用時才到資料庫中將值取出。
測試程式會由讀取 table USERS 後,透過關聯取得該使用者的所有飯局,所以我們先為 Users 的 DAO 加一個 method - find,如下,這裡是用 EntityManager 提供的 find method 抓出資料,find method 只能以 primary key 找出資料,後面還會說明其它方式。
 1 @Repository
 2 public class UsersDAOImpl implements UsersDAO {
 3     @PersistenceContext
 4     private EntityManager manager;
 5 
 6     @Transactional
 7     public int create(Users user) {
 8         manager.persist(user);
 9         return 1;
10     }
11 
12     @Override
13     public Users find(String email) {
14         return manager.find(Users.class, email);
15     }
16 }
接下來看一下多方 (Appointment) 的類別怎麼定義。
 1 @Entity
 2 @Table(
 3     name="APPOINTMENT",
 4     uniqueConstraints = {
 5         @UniqueConstraint(columnNames = "SYSID")
 6     })
 7 public class Appointment implements Serializable {
 8     private static final long serialVersionUID = -3781642274581820116L;
 9     
11     private String name;
12     private Long restaurant_sysid;
13     private String description; 
14     private byte[] photo;
15     private Integer people;
16     private Integer willPay;
17     private String payKind;
18     private String cancel;
19     private Date htime;
20     private Date ctime;
21     
22     @ManyToOne(fetch = FetchType.LAZY)
23     @JoinColumn(name="EMAIL", insertable=false, updatable =false)
24     private Users users;
25 
26     //省略
27 
28     public Users getUsers() {
29         return users;
30     }
31     
32     public void setUsers(Users users) {
33         this.users = users;
34     }
35 }
  1. 第23行: name="EMAIL",這是指 Appointment 用那一個欄位和 Users 的 PK 有關聯。
  2. 第14行: 資料庫裡的欄位屬性是 BLOB,是檔案資料,這裡就用 byte[] 來存取。
接著看一下 Appointment 的 DAO,裡面的 find 使用了另一種讀取資料的方式,就是寫 JPQL,雖然語法和 SQL 是很類似,但還是略有不同,要注意 from 後面的 Appointment 是 class Appointment,所以大小寫要正確。
 1 @Repository
 2 public class AppointmentDAOImpl implements AppointmentDAO {
 3     @PersistenceContext
 4     private EntityManager manager;
 5 
 6     @Transactional
 7     @Override
 8     public int create(Appointment appointment) {
 9         manager.persist(appointment);
10         return 1;
11     }
12 
13     @Override
14     public Appointment find(String email) {
15         return manager.createQuery("select a from Appointment a where a.email = :email", Appointment.class).setParameter("email", email).getSingleResult();
16     }
17 }
最後當然要來測試一下啦~ 如上一篇使用 JUnit,兩個 method 都可以存取到 table APPOINTMENT 裡的資料。
 1     @Test
 2     public void testFind() throws IOException {
 3         Appointment a = daoApp.find("hi.steven@gmail.com");
 4         
 5         Path pathTo = Paths.get("E:/testFind.jpg");
 6         Files.write(pathTo, a.getPhoto(), new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE});
 7     }
 8     
 9     @Test
10     public void testRelation() throws IOException {
11         Users u = daoUsers.find("hi.steven@gmail.com");
12         for(Appointment a:u.getAppointments()) {
13             System.out.println(a.toString());
14         }
15     }






【日劇 - 月薪嬌妻 (逃避雖可恥但有用)】
這是新垣結衣的新作,播出以來收視長紅,收視率由第一集逐步走高,到最後一集最高。劇中女主角森山美栗因為求職不順利而"逃入婚姻",引起許多不同角度的解讀,我看這部戲時想到的是從事家務也有其價值,而且這價值長期被低估,這部戲在探討的就是這個價值。當然啦~ 不管如何解讀,最重要的是看新垣結衣搞笑 ...
新垣結衣曾在 Legal High 中飾演思想左傾的年輕律師,在這部戲裡思想也略微左傾,不知道這是巧合? 還是她本人就是如此?

2016年10月22日 星期六

JPA + Hibernate + Spring + JUnit (annotation)

ORM 在 JPA 出現後,逐漸要被整合起來,而不是像之前,每個不同的 framework 有自己的一套 xml 及 annotation。因為 xml 設定也正逐漸被 annotation 取代,底下這個例子將僅使用 annotation,並且在可能的情況下儘量用 JPA 的 annotation,只有當 JPA 不敷使用時,才使用 framework 提供的 annotation。

在資料庫 (Oracle) 中有一個 table Users,這裡將使用 JUnit 建立一筆資料。
  • JPA config
 1 package idv.steven.invitation.database;
 2 
 3 import javax.sql.DataSource;
 4 
 5 import org.apache.commons.dbcp.BasicDataSource;
 6 import org.springframework.context.annotation.Bean;
 7 import org.springframework.context.annotation.Configuration;
 8 import org.springframework.orm.jpa.JpaTransactionManager;
 9 import org.springframework.orm.jpa.JpaVendorAdapter;
10 import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
11 import org.springframework.orm.jpa.vendor.Database;
12 import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
13 import org.springframework.transaction.PlatformTransactionManager;
14 import org.springframework.transaction.annotation.EnableTransactionManagement;
15 
16 @Configuration
17 @EnableTransactionManagement
18 public class JpaConfig {
19     @Bean
20     public DataSource dataSource() {
21         BasicDataSource ds = new BasicDataSource();
22         ds.setDriverClassName("oracle.jdbc.OracleDriver");
23         ds.setUrl("jdbc:oracle:thin:@steven-nb:1521:DemoDB");
24         ds.setUsername("steven");
25         ds.setPassword("P@ssw0rd");
26         ds.setInitialSize(5);
27         ds.setMaxActive(10);
28         
29         return ds;
30     }
31     
32     @Bean
33     public JpaVendorAdapter jpaVendorAdapter() {
34         HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
35         
36         adapter.setDatabase(Database.ORACLE);
37         adapter.setShowSql(true);
38         adapter.setGenerateDdl(false);
39         adapter.setDatabasePlatform("org.hibernate.dialect.Oracle10gDialect");
40         
41         return adapter;
42     }
43     
44     @Bean
45     public PlatformTransactionManager jpaTransactionManager() {
46         JpaTransactionManager tm = new JpaTransactionManager();
47         tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
48         
49         return tm;
50     }
51         
52     @Bean
53     public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
54         LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
55         emf.setDataSource(this.dataSource());
56         emf.setPackagesToScan(new String[] {"idv.steven.invitation.database.dao", "idv.steven.invitation.database.entity" });
57         emf.setJpaVendorAdapter(this.jpaVendorAdapter());
58         
59         return emf;
60     }
61 }
  1. 任何標識有 @Configuration 的類別,即為 spring 的設定類別 (取代了之前的 xml 設定檔)。
  2. @EnableTransactionManagement 表示,將由 spring 來管理 transaction。
  3. 這個類別就相當於採用 xml 設定時的 persistence.xml (參考: Hibernate + Spring + JPA (configure))。
  4. 第 34 行,在 spring 的 docs 中有這段說明「JpaVendorAdapter implementation for Hibernate EntityManager. Developed and tested against Hibernate 3.6, 4.2/4.3 as well as 5.x. Hibernate 4.2+ is strongly recommended for use with Spring 4.0+.」。
  5. 第 56 行,掃描 JPA 有關套件,將相關的類別載入。
  • 系統設定
 1 package idv.steven.invitation;
 2 
 3 import org.springframework.context.annotation.ComponentScan;
 4 import org.springframework.context.annotation.Configuration;
 5 
 6 @Configuration
 7 @ComponentScan(basePackages = {"idv.steven.invitation.database"})
 8 public class SystemConfig  {
 9 
10 }
  1. 這是個系統設定類別,當然類別裡一般是可以使用 @Bean 載入許多類別,因為我們的例子很簡單,這裡只是用第 7 行的方式掃描指定的套件,以載入上面的 JpaConfig 類別。
  • Entity
 1 package idv.steven.invitation.database.entity;
 2 
 3 import java.io.Serializable;
 4 
 5 import javax.persistence.Column;
 6 import javax.persistence.Entity;
 7 import javax.persistence.Id;
 8 import javax.persistence.Table;
 9 import javax.persistence.UniqueConstraint;
10 
11 @Entity
12 @Table(
13     name="USERS",
14     uniqueConstraints = {
15         @UniqueConstraint(columnNames = "EMAIL")
16     })
17 public class Users implements Serializable {
18     private static final long serialVersionUID = 3965333920543479036L;
19     @Id
20     private String email;
21     private String password;
22     private String phone1;
23     private String phone2;
24     private String nickname;
25     private String city;
26     private String gender;
27     private String birthday;
28     private String industry;
29     private String occupation;
30     private String description;
31     
32     … getter & setter method
33     
34 }
  1. ORM 中 entity 與實體 table 的對映,getter & setter method 我在這裡就省略不佔篇幅,第 14~16 行指出 EMail 欄位為 unique (這個 table 以 email 欄位為 primary key)。
  • DAO
1 package idv.steven.invitation.database.dao;
2 
3 import idv.steven.invitation.database.entity.Users;
4 
5 public interface UsersDAO {
6     public int create(Users user);
7 }
  1. 定義 DAO 介面,這是可以省略,直接定義下面的類別也可以,這是為了設計上的彈性。 
 1 package idv.steven.invitation.database.dao;
 2 
 3 import javax.persistence.EntityManager;
 4 import javax.persistence.PersistenceContext;
 5 
 6 import org.springframework.stereotype.Repository;
 7 import org.springframework.transaction.annotation.Transactional;
 8 
 9 import idv.steven.invitation.database.entity.Users;
10 
11 @Repository
12 public class UsersDAOImpl implements UsersDAO {
13     @PersistenceContext
14     private EntityManager manager;
15 
16     @Transactional
17     public int create(Users user) {
18         manager.persist(user);
19         return 1;
20     }
21 }
  1. 第 13 行,由 JpaConfig 類別裡載入 EntityManager,這裡不能用 @Inject 或 @Autowired 而要用 @PersistenceContext,因為這裡不是直接載入一個 Bean,而是由 EntityManagerFactory bean 產生一個 EntityManager。
  2. 第 16 行指出這個 method 的 transaction 要交由 spring 控管,所以在這個 method 一開始 spring 會產生一個 transaction,在離開 method 前根據成功或失敗決定 commit 或 rollback。
  • 單元測試 JUnit
 1 package idv.steven.invitation.database.dao;
 2 
 3 import static org.junit.Assert.*;
 4 
 5 import javax.inject.Inject;
 6 
 7 import org.junit.After;
 8 import org.junit.Before;
 9 import org.junit.Test;
10 import org.junit.runner.RunWith;
11 import org.springframework.test.context.ContextConfiguration;
12 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
13 
14 import idv.steven.invitation.SystemConfig;
15 import idv.steven.invitation.database.JpaConfig;
16 import idv.steven.invitation.database.entity.Users;
17 
18 @RunWith(SpringJUnit4ClassRunner.class)
19 @ContextConfiguration(classes = { JpaConfig.class, SystemConfig.class })
20 //@Transactional
21 public class UserDAOTest {
22     @Inject
23     private UsersDAO dao;
24 
25     @Before
26     public void setUp() throws Exception {
27     }
28 
29     @After
30     public void tearDown() throws Exception {
31     }
32 
33     @Test
34     public void testCreate() {
35         Users user = new Users();
36         user.setEmail("hi.steven@gmail.com");
37         user.setNickname("Steven");
38         user.setBirthday("19750315");
39         user.setCity("台南市");
40         user.setGender("M");
41         user.setPassword("password");
42         
43         int n = dao.create(user);
44         assertEquals(1, n);
45     }
46 }
  1. 第 19 行,載入相關設定檔。
  2. 第 20 行,如果在單元測試的類別前加上 @Transactional,那麼在單元測試結束後,JUnit 會將資料 rollback。
  3. 第 35~41 將要加入資料庫的值設定在 Users 的物件。
  4. 第 43 行,呼叫 DAO 將資料 insert 到資料庫,Hibernate 會產生這樣的 sql --- insert into USERS (BIRTHDAY, CITY, DESCRIPTION, GENDER, INDUSTRY, NICKNAME, OCCUPATION, PASSWORD, PHONE1, PHONE2, EMAIL) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)。
  • see also
第一個 spring boot web 程式 (使用 embedded tomcat)




【日本電影 - 你的名字】
這部動畫電影無疑是今年日本最夯的電影,劇中以慧星撞地球及男女主角靈魂(身體)互換為背景,述說著兩人在茫茫人海中尋找生命中註定要在一起的人,確實是相當能吸引青年男女的劇情。下圖是慧星分裂後的瞬間,以拋物線來說,那個分裂出來的小塊應該會拋向遠方,在劇中則落在男女主角所在之處,這一點引來了一些批評,當然這個是個缺失,但無損整個故事的精彩。