close

本篇要來闡述一下關於所謂的"資料庫遷移"

一般你搜尋關鍵字的話,可以使用RoomData migrate等相關字眼搜尋

而資料庫遷移是什麼?又是什麼時候應用呢?

我這邊給個案例,你大概就會懂了(-‿◦)

 

所謂的資料庫遷移...應該用"資料庫升級"可能會比較好懂一點(因為單字Migrate的意思是遷徙)

Google官方在說明此項技術時的用詞也是用Migrate這個詞,所以中翻大家都直接講遷移

 

那回歸正題,資料庫遷移的意思其實就是修改資料庫內的結構,非內行的可以當個長知識看就好

譬如你今天想要做民調,調查個年齡層喜歡的政黨

那麼你會怎麼設計問卷?按照常聽到的大概就是

1. 年齡

2. 學歷

3. 哪裡人

4. 喜歡的政黨

這四項吧?

 

Okay,那麼這些資料總是要建檔在電腦裡面,於是這時資料庫工程師就會照著你的表單輸入這四項

但若有一天你還想要在表單上加入一個選項叫做行業別

那麼資料庫的內容就要修正為

1. 年齡

2. 學歷

3. 哪裡人

4. 喜歡的政黨

5. 行業別

以上五項

這時候軟體工程師那邊就需要針對你的要求去修改他所建立的表單

而這"修改"的行為我們在軟體工程上就是稱呼他為"遷移"

 

...所以我覺得就是個文字遊戲而已...

 

好啦,那麼就來開始今天的內容

 

⚠️請注意⚠️

本篇的內容將延續以前寫的一篇文章

->碼農日常-『Android studio』使用Room資料庫以及資料庫監視工具Stetho

 

這邊建議如果是不會Room DataBase設置方式的朋友

建議可以從這篇看起

當然,最後我也會統整一下資料庫升級需要注意哪些事項

如果是熟悉使用Room的朋友也不至於還要從新適應我的Coding Style

 

跟上回輸入內容比較一下

1587276594-3837497855_n Screenshot_1616222512

 

主要就是多了一個欄位(・ω・)b

 

再來是Github,這邊基本上是沿用之前上傳的哦

->https://github.com/thumbb13555/RoomDataBaseExample

 


 

1. 資料結構

 

來吧,首先講一下關於文件的擺放

截圖 2021-03-20 下午3.35.14

專注在RoomDataBase資料夾裡面的內容就好

共有三個檔案,分別為

 

1. DataBase.java(抽象類)

2. DataUao.java(interface類)

3. MyData.java(實體類)

 

這三即是組成Room資料庫的基礎

再來看一下至上次為止的資料庫內容

截圖 2021-03-20 下午1.19.02

 

這時候的資料內容為

1. ID

2. Name

3. phone

4. hobby

5. elseinfo

以上五個欄位

 

那麼今天的目標就是要新增一個欄位叫做"age"年齡的欄位

而最後的結果將會如下

截圖 2021-03-20 下午1.18.36

 

變成6個欄位

1. ID

2. Name

3. phone

4. hobby

5. elseinfo

6. age

 

Okay, 那以下開始(・ωー)~☆


 

2. 開始修改

 

本文從這邊開始修改,修改順序為

1. 介面

2. MyData.java

3. DataBase.java

4. DataUao.java

5. MainActivity.java

當然啦,也不一定要按照我的順序

不過由於Android studio在你輸入的過程中就會逐步編譯

所以有些東西就是一開始寫上去會是紅字,看了不舒服罷了

那麼,開始吧

2-1. 修改介面

首先當然從最好改的開始XD

就結果論就是要像下圖所示

1587276594-3837497855_n Screenshot_1616222512

直接幫你標注改動的部分即可

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="32dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button_Modify" />

    <Button
        android:id="@+id/button_Create"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="32dp"
        android:text="新增"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout5" />

    <Button
        android:id="@+id/button_Modify"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="8dp"
        android:text="修改"
        app:layout_constraintEnd_toStartOf="@+id/button_Clear"
        app:layout_constraintStart_toEndOf="@+id/button_Create"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout5" />

    <LinearLayout
        android:id="@+id/linearLayout5"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:orientation="vertical"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">



        <LinearLayout
            android:id="@+id/linearLayout2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:orientation="horizontal">

            <TextView

                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="姓名:"
                android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

            <EditText

                android:id="@+id/editText_Name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="3"
                android:ems="10"
                android:inputType="textPersonName" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:orientation="horizontal">

            <TextView

                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="電話:"
                android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

            <EditText

                android:id="@+id/editText_Phone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="3"
                android:ems="10"
                android:inputType="textPersonName" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:orientation="horizontal">

            <TextView

                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="興趣:"
                android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

            <EditText

                android:id="@+id/editText_Hobby"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="3"
                android:ems="10"
                android:inputType="textPersonName" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:orientation="horizontal">

            <TextView

                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="其他:"
                android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

            <EditText

                android:id="@+id/editText_else"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="3"
                android:ems="10"
                android:inputType="textPersonName" />
        </LinearLayout>
        <LinearLayout
            android:id="@+id/linearLayout6"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="20dp"
            android:orientation="horizontal">

            <TextView

                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="年齡:"
                android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

            <EditText

                android:id="@+id/editText_age"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="3"
                android:ems="10"
                android:inputType="textPersonName" />
        </LinearLayout>
    </LinearLayout>

    <Button
        android:id="@+id/button_Clear"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="16dp"
        android:text="清空顯示"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout5" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

2-2. 修改實體類MyData.java

再來是實體類,加入一個age的欄位

MyData.java

@Entity(tableName = "MyTable")
public class MyData {


    @PrimaryKey(autoGenerate = true)//設置是否使ID自動累加
    private int id;
    private String name;
    private String phone;
    private String hobby;
    private String elseInfo;
    private int age;

    public MyData(String name, String phone, String hobby, String elseInfo,int age) {
        this.name = name;
        this.phone = phone;
        this.hobby = hobby;
        this.elseInfo = elseInfo;
        this.age = age;
    }
    @Ignore//如果要使用多形的建構子,必須加入@Ignore
    public MyData(int id,String name, String phone, String hobby, String elseInfo,int age) {
        this.id = id;
        this.name = name;
        this.phone = phone;
        this.hobby = hobby;
        this.elseInfo = elseInfo;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }

    public String getElseInfo() {
        return elseInfo;
    }

    public void setElseInfo(String elseInfo) {
        this.elseInfo = elseInfo;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

 

2-3. 修改抽象類別DataBase.java, 加入Migrate程式

然後修改關於抽象類別那個部分

要注意到,這個地方改的內容要特別特別注意我標註的地方,否則不會執行

 

DataBase.java

(修改前)

@Database(entities = {MyData.class},version = 1,exportSchema = true)
public abstract class DataBase extends RoomDatabase {
    public static final String DB_NAME = "RecordData.db";
    private static volatile DataBase instance;

    public static synchronized DataBase getInstance(Context context){
        if(instance == null){
            instance = create(context);//創立新的資料庫
        }
        return instance;
    }

    private static DataBase create(final Context context){
        return Room.databaseBuilder(context,DataBase.class,DB_NAME).build();
    }
    public abstract DataUao getDataUao();//設置對外接口
}

 

(修改後)

@Database(entities = {MyData.class},version = 2,exportSchema = true)
public abstract class DataBase extends RoomDatabase {
    public static final String DB_NAME = "RecordData.db";
    private static volatile DataBase instance;

    public static synchronized DataBase getInstance(Context context){
//        if(instance == null){
//            instance = create(context);//創立新的資料庫
//        }
        if (instance == null){
            instance = Room.databaseBuilder(
                    context.getApplicationContext(),
                    DataBase.class,DB_NAME).addMigrations(MARGIN_1to2).build();
        }
        return instance;
    }

    private static DataBase create(final Context context){
        return Room.databaseBuilder(context,DataBase.class,DB_NAME).build();
    }

    public static Migration MARGIN_1to2 = new Migration(1,2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE \"MyTable\"  ADD COLUMN age INTEGER NOT NULL DEFAULT 18");


        }
    };

    public abstract DataUao getDataUao();//設置對外接口
}

把兩個比較一下,就能知道重點在哪裡囉:D

特別注意一下這裡

database.execSQL("ALTER TABLE \"MyTable\"  ADD COLUMN age INTEGER NOT NULL DEFAULT 18");

實際在編輯器上,他是長這樣的

截圖 2021-03-20 下午4.12.34

這行是關鍵中的關鍵,這個SQL指令就是告訴資料表要新增一個欄位的意思

ADD COLUMN 是指欄位名稱,以及欄位屬性(INTEGER為數字,要一般文字請輸入TEXT)

後面的NOT NULL DEFAULT 則意思是"欄位不可空,預設值為.."

所以在後面,我統一設置為18歲

而如果是TEXT欄位的話,請輸入'18'即可←連同單引號都要加入

 

2-4. 修改Interface DataUao.java

再來是修改其相關SQL語法,因為DataUao中本來就是放置SQL語法的地方

如下

DataUao.java

@Dao
public interface DataUao {

    String tableName = "MyTable";
    /**=======================================================================================*/
    /**簡易新增所有資料的方法*/
    @Insert(onConflict = OnConflictStrategy.REPLACE)//預設萬一執行出錯怎麼辦,REPLACE為覆蓋
    void insertData(MyData myData);

    /**複雜(?)新增所有資料的方法*/
    @Query("INSERT INTO "+tableName+"(name,phone,hobby,elseInfo,age) VALUES(:name,:phone,:hobby,:elseData,:age)")
    void insertData(String name,String phone,String hobby,String elseData,int age);

    /**=======================================================================================*/
    /**撈取全部資料*/
    @Query("SELECT * FROM " + tableName)
    List<MyData> displayAll();

    /**撈取某個名字的相關資料*/
    @Query("SELECT * FROM " + tableName +" WHERE name = :name")
    List<MyData> findDataByName(String name);

    /**=======================================================================================*/
    /**簡易更新資料的方法*/
    @Update
    void updateData(MyData myData);

    /**複雜(?)更新資料的方法*/
    @Query("UPDATE "+tableName+" SET name = :name,phone=:phone,hobby=:hobby,elseInfo = :elseInfo,age= :age WHERE id = :id" )
    void updateData(int id,String name,String phone,String hobby,String elseInfo,int age);

    /**=======================================================================================*/
    /**簡單刪除資料的方法*/
    @Delete
    void deleteData(MyData myData);

    /**複雜(?)刪除資料的方法*/
    @Query("DELETE  FROM " + tableName + " WHERE id = :id")
    void deleteData(int id);

}

修改這個地方,就是要細心再細心👍🏻

不要出錯了

 

2-5. 修改MainActivity.java內容

最後是外部接口,這部分就是如果你以上步驟都有改好,編輯器就會以紅字提醒了

 

MainActivity.java

(因為有200行,所以沒有修改的部份省略,全貌請參考Github)

public class MainActivity extends AppCompatActivity {
    MyAdapter myAdapter;
    MyData nowSelectedData;//取得在畫面上顯示中的資料內容

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Stetho.initializeWithDefaults(this);//設置資料庫監視
        
        (略)
        EditText edAge = findViewById(R.id.editText_age);
        (略)

        /**=======================================================================================*/
        /**設置修改資料的事件*/
        btModify.setOnClickListener((v) -> {
            new Thread(() -> {
                if(nowSelectedData ==null) return;//如果目前沒前台沒有資料,則以下程序不執行
                (略)
                int age = Integer.parseInt(edAge.getText().toString());/*遷移後新增*/
                MyData data = new MyData(
                        nowSelectedData.getId(), name, phone, hobby, elseInfo,age/*遷移後新增*/);
                DataBase.getInstance(this).getDataUao().updateData(data);
                runOnUiThread(() -> {
                   (略)
                    edAge.setText("");/*遷移後新增*/
                    nowSelectedData = null;
                    myAdapter.refreshView();
                    Toast.makeText(this, "已更新資訊!", Toast.LENGTH_LONG).show();
                });
            }).start();

        });
        /**=======================================================================================*/
        /**清空資料*/
        btClear.setOnClickListener((v -> {
            (略)
            edAge.setText("");/*遷移後新增*/
            nowSelectedData = null;
        }));
        /**=======================================================================================*/
        /**新增資料*/
        btCreate.setOnClickListener((v -> {
            new Thread(() -> {
                (略)
                int age = Integer.parseInt(edAge.getText().toString());/*遷移後新增*/
                if (name.length() == 0) return;//如果名字欄沒填入任何東西,則不執行下面的程序
                MyData data = new MyData(name, phone, hobby, elseInfo,age/*遷移後新增*/);
                DataBase.getInstance(this).getDataUao().insertData(data);
                runOnUiThread(() -> {
                    myAdapter.refreshView();
                    edName.setText("");
                    edPhone.setText("");
                    edHobby.setText("");
                    edElseInfo.setText("");
                    edAge.setText("");/*遷移後新增*/
                });
            }).start();
        }));
        /**=======================================================================================*/
        /**初始化RecyclerView*/
        new Thread(() -> {
            List<MyData> data = DataBase.getInstance(this).getDataUao().displayAll();
            myAdapter = new MyAdapter(this, data);
            runOnUiThread(() -> {
                recyclerView.setAdapter(myAdapter);
                /**===============================================================================*/
               (略)
                /**===============================================================================*/
                /**取得被選中的資料,並顯示於畫面*/
                myAdapter.setOnItemClickListener((myData)-> {//匿名函式(原貌在上方)
                    nowSelectedData = myData;
                    (略)
                    edAge.setText(String.valueOf(myData.getAge()));/*遷移後新增*/
                });
                /**===============================================================================*/
            });
        }).start();
        /**=======================================================================================*/

    }

    private static class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
       (略)
    }

    /**設置RecyclerView的左滑刪除行為*/
    private void setRecyclerFunction(RecyclerView recyclerView){
        I(略)
    }
}

 

這時候執行並打開Steoth(不知道這是什麼的罰你回去上一篇看),看到有新欄位就代表成功遷移囉!

截圖 2021-03-20 下午1.22.58

 


 

3. 整理

看我上面寫了那麼多,你一定覺得重點太多了有點霧煞煞( ´_ゝ`)

這邊幫你整理一下我大概做了什麼

1. 去實體類新增欄位

2. 撰寫資料庫升級之程式

3. 到Uao調整SQL語法

4. 修正輸入的內容

 

花了這麼大的篇幅,便是以上這些重點

 


 

4. 補充

 

對了,由於這次的範例的Github是有再次Commit的,所以或許你想看一看在前一版的程式碼

你可以如以下操作

 

1.到我這篇的Gi,並點擊紅色方塊部分

截圖 2021-03-20 下午4.46.02

 

2. 找到想要看的版本,點擊<>符號

截圖 2021-03-20 下午4.47.57

 

3.這時候網頁會跳轉到原介面,但是這時的內容已經變更到上一版的狀態囉

截圖 2021-03-20 下午4.49.43

 


 

今天的這個主題也是我很早之前就想寫的內容

不過我一直很猶豫到底要重寫一個專案還是拿之前做的東西來改

後來想了很久後還是決定拿之前的範例Code來改,因為感覺這樣比較符合平常會遇到的情況XD

那麼,這篇到此

如果文章對你有幫助的話...

TK

arrow
arrow

    碼農日常 發表在 痞客邦 留言(27) 人氣()