首先你需要明白Evolution的作用是什麼?它可以讓你通過幾個腳本文件,輕鬆完成數據庫的管理工作。你只負責編寫腳本,腳本和數據庫之間的同步工作,Evolution幫你搞定。
一、如何開啓Evolution插件?
play默認是啓用Evolution插件的,如果想禁用Evolution插件,在conf/application.conf中添加配置項evolutionplugin=disabled,或者設置通過設置系統屬性的方式-Devolutionplugin=disabled。禁用Evolution插件相當於切斷了play與數據庫間的同步手段,實體類的任意變動都不會影響到數據庫的表結構,這在項目發佈時非常有用。
二、Evolution腳本存放位置
Evolution腳本在項目中的路徑爲conf/evolutions/{database name},例如對於默認的default數據庫,路徑爲conf/evolutions/default。Evolution腳本可以有很多個,腳本名爲連續的數字,從1開始。例如,第1個腳本1.sql,第2個腳本2.sql,如此類推...。
三、Evolution腳本格式
Evolution腳本包含三個部分:註釋,up腳本和down腳本。
1. 註釋
在標記# --- !Ups以上的都是註釋部分,標記中的---不是必須的,只要包含#!Ups就可以了,即標記模式要滿足^#.*!Ups.*$,同樣地,標記# --- !Downs也是類似的。註釋部分沒有格式限制,可以隨意書寫。
2. up腳本
在標記# --- !Ups和# --- !Downs之間的部分是up腳本,up腳本是一段用來初始化或更新數據庫的sql腳本,每一條sql語句必須以分號;結尾,如果sql語句中含有分號,需要使用;;進行轉義。註釋方法遵循標準sql,單行註釋使用--,多行註釋使用/* ... */。
3. down腳本
標記# --- !Downs之後的部分是down腳本,down腳本是一段撤銷腳本,類似於數據庫中的事務回滾,將數據庫恢復到up腳本執行之前的狀態。書寫規則同up腳本。
四、Evolution配置表PLAY_EVOLUTIONS
Evolution插件使用表PLAY_EVOLUTIONS管理同步腳本。在項目第一次啓動時,Evolution插件會在數據庫中創建PLAY_EVOLUTIONS表,比較可惜的是,Evolution插件並沒有根據不同的數據庫類型生成不同的建表語句,而是硬編碼了下面的建表語句:
1
2
3
4
5
6
7
8
9
|
create
table
play_evolutions ( id
int
not
null
primary
key ,
hash
varchar (255)
not
null ,
applied_at
timestamp
not
null ,
apply_script
text, revert_script
text, state
varchar (255),
last_problem
text ) |
PLAY_EVOLUTIONS表包含7個字段,解釋如下:
- id: 唯一對應一個腳本文件名,也成爲revision,值從1開始
- hash:apply_script和revert_script兩個字段內容拼接後的sha1哈希值,用來檢測腳本內容是否發生變化
- applied_at:記錄up或down腳本執行時間
- apply_script:存放腳本文件中的up腳本
- revert_script:存放腳本文件中的up腳本
- state:保存當前的執行狀態,值可以爲:applied/applying_up/applying_down
- last_problem: 存放腳本執行時錯誤信息
每個數據庫的Evolution腳本文件數和相應PLAY_EVOLUTIONS表中記錄條數相同,並且是一一對應關係,對應關係爲文件名和id相同。
五、Evolution插件執行過程分析
針對conf/application.conf配置的每個數據源依次執行:
1. 在conf/evolutions/{database name}目錄下,依次尋找1.sql,2.sql,...,只至發現某個文件不存在爲止,例如目錄下有:0.sql,1.sql,2.sql,4.sql,則最終只會找到1.sql, 2.sql兩個文件,最後按文件名降序排列得到一個列表;
2. 查詢PLAY_EVOLUTIONS中所有記錄,按id降序排列得到一個列表;
3. 比較前兩步得到的兩個列表:
1)如果有腳本文件在數據庫中不存在,則向PLAY_EVOLUTIONS插入一條記錄,並執行該腳本文件的up腳本;
2)如果PLAY_EVOLUTIONS表中有記錄,但是該腳本文件卻不存在,則執行該條記錄的down腳本,並且刪除該條記錄
3)如果腳本文件存在,並且PLAY_EVOLUTIONS表中也有相應記錄,則比較腳本文件的sha1(up腳本+down腳本)與表中記錄的hash值是否相等,如果相等,則不做任何處理;如果不等,則先執行表中記錄的down腳本,刪除該條記錄,重新插入一條與腳本文件對應的新記錄,執行up腳本。考慮到一個應用可能在多臺服務器上同時部署,在執行up/down腳本時,會先將表中相應記錄的state改爲applying_up/applying_down狀態,如果執行出錯,則更新last_problem字段,存入錯誤描述,狀態保持不變,如果執行成功,則將狀態更新成applied。
六、常見問題解決方法
1. 瀏覽器老是提示"Database xxx needs evolution!", 則在conf/application.conf中添加配置applyEvolutions.{database name}=true,即可解決。
2. up/down腳本執行出錯後,啓動項目瀏覽器總是提示"Database xxx is in inconsistent state!", 如果有腳本執行失敗,則Evolution插件不會再嘗試執行出錯的腳本,而是直接在瀏覽器中報錯,此時的解決辦法是手工在數據庫中執行出錯腳本,然後再單擊頁面上的"Mark it resolved"按鈕。
3. Ebean每次都會重新生成1.sql文件,如何手工修改1.sql,而不是用Ebean的自動生成腳本?
刪除1.sql文件的頭兩行註釋:
七、不同運行模式下的差異
1. 測試模式下(Mode.Test),無視配置參數,任意的Evolution操作都會被直接執行。
2. 開發模式下(Mode.Dev),如果配置了applyEvolutions.{database name}=true,則自動執行本次Evolution操作,否則會在瀏覽器中提示"Database xxx needs evolution!"。
3. 產品模式下(Mode.Prod)情況比較複雜,根據配置參數分三種情況:
1)如果本次Evolution操作不涉及down腳本,並且配置了applyEvolutions.{database name}=true,則自動執行本次Evolution操作;
2)如果本次Evolution操作涉及down腳本,並且配置了applyEvolutions.{database
name}=true和applyDownEvolutions.{database
name}=true,則自動執行本次Evolution操作;
3)如果本次Evolution操作涉及down腳本,並且沒有同時配置applyEvolutions.{database
name}=true和applyDownEvolutions.{database
name}=true兩個參數,則直接拋出InvalidDatabaseRevision異常。
八、Evolution with Oracle
在play第一次連接數據庫時,Evolution插件會嘗試創建PLAY_EVOLUTIONS表,上文曾提到過,Evolution插件以硬編碼形式提供的建表語句無法在Oracle中執行,原因是Oracle中沒有text類型,所以在將play的數據源切換至Oracle時,我們需要手工在Oracle上創建PLAY_EVOLUTIONS表,建表語句如下:
1
2
3
4
5
6
7
8
9
|
create
table
play_evolutions ( id
number not
null
primary
key ,
hash
varchar2(255) not
null , applied_at
timestamp
not
null , apply_script
clob, revert_script
clob, state
varchar2(255), last_problem
clob ) |
這裏會有個問題,apply_script和revert_script存放的是up和down腳本,有時腳本會很大,而很多數據庫都會限制text類型必須小於64kb,就算選擇Oracle的clob類型也必須小於4000kb,較通用的解決辦法是將大的腳本文件分成幾個較小的腳本文件。
另外需要注意的是,Oracle中字段名不能超過30個字符,不要使用實體映射的默認表名,例如User/Role,最好使用@Table註解生成另外一個名稱:
1
2
3
4
5
6
7
8
9
10
|
@Entity @Table (name= "r_user" ) public
class
User extends
Model { @Id public
Long id; @Constraints .Required public
String name; public
static
Finder<Long,User> find = new
Finder<Long,User>(Long. class ,
User. class );
} |
九、小結
Evolution插件總體還是令人比較滿意的,遺憾的是在連接Oracle數據源時需要手工干預。希望在以後版本中,Evolution插件能夠自動判斷數據庫類型,儘量減少人爲的手工干預。