Google Code Prettify

顯示具有 Database 標籤的文章。 顯示所有文章
顯示具有 Database 標籤的文章。 顯示所有文章

2017年1月25日 星期三

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


在看過之前一對多多對多的例子後,現在把兩個合在一起,現在要把 Appointment 類別完全依它與其它 Entity 類別的關係如上圖的建立好,Appointment 修改如下:
 1 @Entity
 2 @Table(
 3     name="APPOINTMENT",
 4     uniqueConstraints = {
 5         @UniqueConstraint(columnNames = "SYSID")
 6     })
 7 @Data
 8 public class Appointment implements Serializable {
 9     private static final long serialVersionUID = -3781642274581820116L;
10     
11     @Id
12     @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQ_APPOINTMENT")
13     @SequenceGenerator(name="SEQ_APPOINTMENT", sequenceName="SEQ_APPOINTMENT", initialValue=1, allocationSize=1)
14     private Long sysid;
15     
16     @ManyToOne(fetch = FetchType.LAZY)
17     @JoinColumn(name = "EMAIL", insertable = false, updatable = false)
18     private Users users;
19     
20     @Column(name = "NAME", length = 30)
21     private String name;
22     
23     @ManyToOne(fetch = FetchType.LAZY)
24     @JoinColumn(name = "RESTAURANT_SYSID", insertable = false, updatable = false)
25     private Restaurant restaurant;
26     
27     @Column(name = "DESCRIPTION", length = 500)
28     private String description;
29     
30     @Column(name = "PHOTO")
31     private byte[] photo;
32     
33     @Column(name = "PEOPLE")
34     private Integer people;
35     
36     @Column(name = "WILLPAY")
37     private Integer willPay;
38     
39     @Column(name = "PAYKIND", length = 2)
40     private String payKind;
41     
42     @Column(name = "CANCEL", length = 1)
43     private String cancel;
44     
45     @Column(name = "HTIME")
46     private Date htime;
47     
48     @Column(name = "CTIME")
49     private Date ctime;
50 
51     @Override
52     public String toString() {
53         return "Appointment [sysid=" + sysid + ", name=" + name + ", description=" + description + ", photo="
54                 + Arrays.toString(photo) + ", people=" + people + ", willPay=" + willPay + ", payKind=" + payKind
55                 + ", cancel=" + cancel + ", htime=" + htime + ", ctime=" + ctime + "]";
56     }
57     
58     @ManyToMany(fetch = FetchType.LAZY)
59     @JoinTable(name = "PARTICIPANT",
60         joinColumns = { @JoinColumn(name = "APPOINTMENT_SYSID") },
61         inverseJoinColumns = { @JoinColumn (name = "EMAIL") })
62     private List<Users> particpants;  
63 }
  • 第 16~18 行,定義 Appointment 和 Users 的多對一關係。
  • 第 23~25 行,定義 Appointment 和 Restaurant 的多對一關係,特別注意第 24 行的 RESTAURANT_SYSID,這是 table APPOINTMENT 中關係到 table RESTAURANT 的欄位。
  • 第 58~62 行,定義 Appointment - Talking - Users 即參與者的多對多關係。
一個 Entity 即使定義了這麼複雜的關係,還是可以執行,不會有問題的,但是,後面會說明這樣定義的一些限制,先來看一下怎麼在 table APPOINTMENT 中新增一筆資料。
    @Test
    public void testCreate() throws IOException {
        //讀檔
        Path path = Paths.get("E:/55047183.jpg");
        byte[] photo = Files.readAllBytes(path);

        //寫入DB
        Appointment appointment = new Appointment();
        
        Users user = daoUsers.findOne("hi.steven@gmail.com");
        
        Restaurant restaurant = daoRestaurant.find(2L);
        
        appointment.setUsers(user);
        appointment.setName("除夕圍爐");
        appointment.setRestaurant(restaurant);
        appointment.setDescription("除夕找人聊天");
        appointment.setPhoto(photo);
        appointment.setPeople(2);
        appointment.setWillPay(500);
        appointment.setPayKind("A");
        appointment.setCancel("N");
        appointment.setHtime(Calendar.getInstance().getTime());
        appointment.setCtime(Calendar.getInstance().getTime());
        
        int count = daoApp.create(appointment);
        assertEquals(1, count);
    }
在 Entity Appointment 中,並沒有定義 table APPOINTMENT 的 EMAIL、RESTAURANT_SYSID 兩個欄位,代替的是 Users 及 Restaurant 兩個 Entity,所以在新增資料時,就可以看到如上面紅色的程式碼,設定到 appointment 的是這兩個 Entity 的物件。
  • DAO
public List<Appointment> find(Users user) {
        TypedQuery<Appointment> query = manager.createQuery("select a from Appointment a where a.users = :user ", Appointment.class);
        query.setParameter("user", user);
        return query.getResultList();
    }
  • Unit Test
    @Transactional
    @Test
    public void testFindUser() throws IOException {
        Users user = daoUsers.findOne("hi.steven@gmail.com");
        
        List<Appointment> app = daoApp.find(user);

        for(Appointment a:app) {
            System.out.println(a.getParticpants().toString());
        }
    }
在 Entity Appointment 中沒有 email 欄位,要搜尋是那個使用者起的飯局,就要用該使用者的 Users 物件,如上 DAO 的 JPQL 所示。第二段的 Unit Test 程式也可以看到傳入 DAO 的是事先查詢出來的 Users 物件。從這裡可以看到,在設計 Entity 時,可能不一定要完全按照資料庫的關聯來建立,還要考慮到使用 DAO 時的方便性,以這個例子來說,或許可以 email 變數來代表 EMAIL 欄位,而不要用 Entity Users,雖然會無法由 Entity 中看出資料庫 table 間的關係,但是使用上比較方便的話,也是無妨。





【萬年五K的圍棋 - 星位-小馬步掛】
不管持黑子或白子,我佈局習慣下星位,這時候就會常遇到對手用小馬步掛來搶地盤,通常我就如下圖所示的下: (星位-小馬步掛-一間跳)

這樣下很簡單的黑白雙方各據一方,而且白子下 (4) 之後,黑子又可以有先手到別的戰場上去。問題是,有時候因為週邊情勢的關係,不希望白可以 (4) 佔有邊上領地,就會使用一間夾,如下: (星位-小馬步掛-一間夾)

這是其中一種最常遇到的變化,(7) 衝一手很重要,因為可以讓白棋留下缺陷。

2017年1月21日 星期六

JPA + Spring: 資料庫連線設定 (JNDI@Tomcat)

Tomcat 應該是 Java 世界使用最廣的 AP server? 即使正式營運主機上安裝的是商業的 AP server (IBM WebSphere、BEA WebLogic ...),程式員在開發程式時,也多半是在本機安裝 Tomcat 用來測試,這裡整理一下兩種建立資料庫連線的方式,一個是使用 Tomcat JNDI 的方式,另一個是使用 apache commons dbcp2,用這兩個方式都可以有 connection pool,在大部份的情況下,在正式營運環境會採用 JNDI,在本機開發則可能採用 dbcp2。
  • JDBC driver
將資料庫的 JDBC driver 放到 $Tomcat$/lib 目錄下,我使用的資料庫是 oracle,所以我將 ojdbc7.jar 放到 $Tomcat$/lib 目錄下。
  • 設定 server.xml


在 eclipse 中加入 Server 後,可以選擇要將那些專案加入 server 中,這裡選擇一個命名為 Party 的專案,按【Finish】後,在 server.xml 中就會產生如下內容:
<Context docBase="Party" path="/Party" reloadable="true" source="org.eclipse.jst.jee.server:Party" />
接著在裡面加入我們要的 JNDI 設定,如下:
<Context docBase="Party" path="/Party" reloadable="true" source="org.eclipse.jst.jee.server:Party">
    <Resource auth="Container" driverClassName="oracle.jdbc.OracleDriver" maxIdle="10" maxTotal="20" maxWaitMillis="-1" name="jdbc/DemoDB" password="passw0rd" type="javax.sql.DataSource" url="jdbc:oracle:thin:@192.168.0.102:1521:DemoDB" username="steven"/>
</Context>
這樣設定完,啟動 Tomcat,Tomcat 就會自動建立起資料庫的連線,接下要看看程式要怎麼使用。
  • web.xml
<resource-ref>
     <description>oracleDB</description>
     <res-ref-name>jdbc/DemoDB</res-ref-name>
     <res-type>javax.sql.DataSource</res-type>
     <res-auth>Container</res-auth>
</resource-ref>
在專案的 web.xml 中,加入如上內容,裡面的 res-ref-name 指出了要使用的 JNDI 資源名稱,這裡當然就是上一步驟設定好的資料庫連線。
  • WebInitializer
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MvcConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}
在 spring MVC 的專案中,會以一個繼承 AbstractAnnotationConfigDispatcherServletInitializer 的類別為起始入口,就像一般在寫 application 會以 main 為起始入口一樣,這裡有兩個重要設定,在 getRootConfigClasses 中指出程式啟動後,要優先執行的設定類別,在 getServletConfigClasses 中則指出處理 servlet 的類別。雖然上面的設定都只有一個類別,實際上那是陣列,也就是說可以有多個。這一篇的重點不在 spring MVC,所以後續只會說明 RootConfig。
  • 在啟始類別中建立資料庫連線
 1 @Configuration
 2 @ComponentScan(
 3         basePackages = {"idv.steven.invitation"},
 4         includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class),
 5         excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
 6     )
 7 @Log4j
 8 public class RootConfig {
 9     
10     @Bean
11     @Profile("PROD")
12     public JndiObjectFactoryBean jndiObjectFactoryBean() {
13         JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
14         jndiObjectFB.setJndiName("jdbc/DemoDB");
15         jndiObjectFB.setResourceRef(true);
16         jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
17         
18         return jndiObjectFB;
19     }
20     
21     @Bean
22     @Profile("TEST")
23     public DataSource dataSource() {
24         BasicDataSource ds = new BasicDataSource();
25         ds.setDriverClassName("oracle.jdbc.OracleDriver");
26         ds.setUrl("jdbc:oracle:thin:@192.168.0.102:1521:DemoDB");
27         ds.setUsername("steven");
28         ds.setPassword("passw0rd");
29         ds.setInitialSize(5);
30         
31         return ds;
32     }
33 }
  1. 如文章一開頭說的,在正式營運環境和開發環境有可能使用不同的資料庫連線方式,這裡就是這樣,建立了兩個不同的連線方式,一個是使用 JndiObjectFactoryBean,另一個使用 DataSource。
  2. 第 14 行設定要使用那個 JNDI 資源,當第 15 行的 setResourceRef 設定為 true 時,就寫成如上面的方式,如果設定為 false,就得寫成 java:comp/env/jdbc/DemoDB。
  3. 第 11、22行指出這兩個 bean 會在什麼環境裡啟動,至於在什麼環境要怎麼判斷? 這是在 web.xml 中設定的,如下,裡面設定了 spring.profiles.active 的值為 PROD,這表示程式啟動時,只有 JndiObjectFactoryBean 會被建立,DataSource 不會被建立,反之亦然。
<context-param>
      <param-name>spring.profiles.active</param-name>
      <param-value>PROD</param-value>
</context-param>
  • JPA config
 1 @Configuration
 2 @EnableTransactionManagement
 3 @EnableJpaRepositories (
 4     basePackages = { "idv.steven.invitation.database.dao" },
 5     entityManagerFactoryRef = "entityManagerFactory",
 6     transactionManagerRef = "jpaTransactionManager"
 7 )
 8 public class JpaConfig {
 9     @Autowired(required=false)
10     private DataSource dataSource;
11     
12     @Autowired(required=false)
13     private JndiObjectFactoryBean jndiObjectFB;
14     
15     @Bean
16     public JpaVendorAdapter jpaVendorAdapter() {
17         HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
18         
19         adapter.setDatabase(Database.ORACLE);
20         adapter.setShowSql(true);
21         adapter.setGenerateDdl(false);
22         adapter.setDatabasePlatform("org.hibernate.dialect.Oracle10gDialect");
23         
24         return adapter;
25     }
26     
27     @Bean
28     public PlatformTransactionManager jpaTransactionManager() {
29         JpaTransactionManager tm = new JpaTransactionManager();
30         tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
31         
32         return tm;
33     }
34         
35     @Bean
36     public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
37         LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
38         if (dataSource == null)
39             emf.setDataSource((DataSource) jndiObjectFB.getObject());
40         else
41             emf.setDataSource(dataSource);
42         emf.setPackagesToScan(new String[] { "idv.steven.invitation.database.entity" });
43         emf.setJpaVendorAdapter(this.jpaVendorAdapter());
44         
45         return emf;
46     }
47 }
這個類別在「JPA + Hibernate + Spring + JUnit (annotation)」中已有說明,這裡只針對粗體的部份補充說明,第 9、12 行一定要有 required=false 的設定,因為兩個只會有一個被建立,如果沒有設定,預設是 required=true,那麼一定會有一個因為在 RootConfig 中沒有建立,在這裡無法被注入。第 38~41 行在設定 DataSource 時,當然要判斷一下現在那個有建立就用那一個。到這裡就搞定了 ...




Forza Gesù -- 一個小女孩成長的故事】
義大利教會合唱團的這首歌在網路上被廣傳,但是最受歡迎的是影片中影片那個版本,是主唱 2010 年 4 歲第一次領唱時的影片,到了 2016 年 10 歲時小女孩長大了,失去了 4 歲時那份混然天成的童真,就沒辦法感動那麼多人了 … 還是來看一下 2010 年的版本吧 ~



2017年1月2日 星期一

JPA + Spring: 繼承 (@MappedSuperclass)


在資料庫的設計中,有時會遇到如上的情況,兩個 table 屬性相同,都是儲放可登入系統的人,所需要的欄位也有許多重複,但是因為"管理者"和"使用者"是由不同介面登入,進入系統後可使用的功能也非常不同,因此被分別放在兩個 table。當發生這種狀況時,透過 ORM,在程式中可以將相同的欄位提取出來成為 super class,先來看一下 Administrator 怎麼寫。
 1 @Entity
 2 @Table(
 3         name="ADMINISTRATOR",
 4         uniqueConstraints = {
 5             @UniqueConstraint(columnNames = "EMAIL")
 6         })
 7 public class Administrator extends AbstractUser implements Serializable {
 8     private static final long serialVersionUID = 4001989953339069879L;
 9     
10     private String department;
11     private String roleId;
12     private String status;
13     
14     public String getDepartment() {
15         return department;
16     }
17     public void setDepartment(String department) {
18         this.department = department;
19     }
20     
21     public String getRoleId() {
22         return roleId;
23     }
24     public void setRoleId(String roleId) {
25         this.roleId = roleId;
26     }
27     
28     public String getStatus() {
29         return status;
30     }
31     public void setStatus(String status) {
32         this.status = status;
33     }
34     
35     @Override
36     public String toString() {
37         return "Administrotor [department=" + department + ", roleId=" + roleId + ", status=" + status + ", toString()="
38                 + super.toString() + "]";
39     }
40 }
兩個 table 共有的欄位被提取出來放到 AbstractUser 類別,其餘的欄位寫法和一般的 Entity 並沒有不同,再來看一下 AbstractUser 的寫法。
 1 @MappedSuperclass
 2 public class AbstractUser {
 3     private String email;
 4     private String password;
 5     private String occupation;
 6     private Date ctime;
 7     private Date lastLogin;
 8     
 9     @Id
10     public String getEmail() {
11         return email;
12     }
13     public void setEmail(String email) {
14         this.email = email;
15     }
16     public String getPassword() {
17         return password;
18     }
19     public void setPassword(String password) {
20         this.password = password;
21     }
22     public String getOccupation() {
23         return occupation;
24     }
25     public void setOccupation(String occupation) {
26         this.occupation = occupation;
27     }
28     public Date getCtime() {
29         return ctime;
30     }
31     public void setCtime(Date ctime) {
32         this.ctime = ctime;
33     }
34     public Date getLastLogin() {
35         return lastLogin;
36     }
37     public void setLastLogin(Date lastLogin) {
38         this.lastLogin = lastLogin;
39     }
40     
41     @Override
42     public String toString() {
43         return "AbstractUser [email=" + email + ", password=" + password + ", occupation=" + occupation + ", ctime="
44                 + ctime + ", lastLogin=" + lastLogin + "]";
45     }
46 }
如上,被提取出來成為上層類別,要在 class 前加上 @MappedSuperclass,這個上層類別也和 entity 一樣寫法,甚至 primary key 欄位也可以放在這 (第 9 行)。




【日劇 - 戀仲】
福士蒼汰和本田翼主演的純愛日劇,從青梅竹馬到步入禮堂,雖然歷經波折,雖然到最後一集編劇仍試著要製造點懸疑,但是,這類的戲大致上94%會是圓滿的結局,所以那個懸疑的效果真的太多餘了 ...
劇中福士蒼汰主演的三浦葵不斷的出現這種狂奔的畫面,我只能說,年輕真好! 要個中年人這麼演的話肯定吃不消!
上圖右邊是劇中男主角的競爭對手蒼井翔太 (野村周平 飾),左邊是三位主要人物之外最重要的配角金澤公平 (太賀 飾),是個對朋友非常熱心的人,當然,我知道編劇和導演想要塑造的是正面的形象,但是,說實在的,他的熱心已經過頭了! 往往過度的介入別人的人生,這我就不太能欣賞了!
本田翼不用特別形容她的漂亮,是男人看了都知道! 她演過不少日劇、日影了,一直到這兩三年戲份才多了起來,在本劇中她的演技真的很生硬,連走路的樣子都很生硬 … Orz ... 純愛戲嘛~ 女主角長得好看比演的好重要,不然叫觀眾要怎麼投入劇情裡呢?!
這是第一集一開頭演高中生時期,她幫男主角三浦葵加油的畫面。她真的蠻漂亮的,再放了一張 … XD
過一年,也就是2016年,她在「校閱女王」中演重要的配角,這時候她的演技倒是進步不少,人是會成長的啊 ~ (下圖)
到這邊介紹了好幾個人了,再介紹最後一位,是主角三浦葵的前女友冴木瑠衣子 (市川由衣 飾),以外表來說是不如本田翼亮麗,但也相當漂亮,劇中她是個溫柔的女人,工作上的表現又相當不錯,這應該也是個相當有女性媚力的吧?! 這年頭有能力的女人也是相當吸引人的!
結尾了,看一下重逢後一個重要轉折,也是在這一刻結局就確定了!
一般來說,這種男人第一次偷偷親女人,女人是知道的,如果女人喜歡這個男人,就會繼續裝睡,算是一種默許吧 ~

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世紀末,年輕男女到了適婚年齡都會面臨強大的社會、家庭壓力,壓迫著這些未婚男女走入婚姻,還把「白頭偕老」當成重大成就,而實際上很多人同住一個屋簷下,彼此間並沒有太多感情,就算白頭偕老,也沒有幸福感。還好,至少現在的台灣社會這樣的困擾不再那麼嚴重,想單身、想在一起卻不結婚 … 都可以,來自社會、家庭的壓力比以前小多了,可以更自由的選擇自己的人生。