MyBatis項目實戰 快速將MySQL轉換成Oracle語句

一、前言

因項目需求,小編要將項目從mysql遷移到oracle中 ~

之前已經完成 數據遷移 (https://zhengqing.blog.csdn.net/article/details/103694901)

現在將完成 基於MyBatis-Plus將項目中的MySQL語句全部轉換成Oracle語句

大概實現步驟:


  1. 將項目改成支持雙庫配置(因項目基於mysql數據庫已經全部完成,也不想直接替換掉,於是新增oracle庫,讓整個項目可支持多個數據庫,這裡不是多數據源哦!)
  2. Oracle中創建常用函數
  3. 遍歷項目中的xxxMapper.xml文件,找到mysql與oracle語句的區別,然後替換絕大部分SQL
  4. 最後將一些特殊的mysql語句手動修改為oracle語句

二、MyBatis-Plus 支持雙庫配置 【mysql,oracle】


1、application.yml中配置mybatis-plus的database-id


<code># mybatis-plus配置
mybatis-plus:
configuration:
jdbc-type-for-null: 'null' # 解決oracle更新數據為null時無法轉換報錯

database-id: oracle # 支持多庫配置 mysql,oracle/<code>


2、MybatisPlus核心配置文件 -> 根據不同的數據庫廠商執行不同的SQL


<code>@Configuration
@MapperScan("com.zhengqing.demo.modules.**.mapper*")
public class MybatisPlusConfig {

/**
* `xxxMapper.xml`文件中的`databaseId`會自動識別使用的數據庫類型與這裡相對應
* 注: 如果沒有指定`databaseId`則該SQL語句適用於所有數據庫哦~
*
* databaseIdProvider:支持多數據庫廠商
* VendorDatabaseIdProvider: 得到數據庫廠商的標識(驅動getDatabaseProductName()),mybatis就能根據數據庫廠商標識來執行不同的sql;
* MySQL,Oracle,SQL Server,xxxx
*/
@Bean
public DatabaseIdProvider getDatabaseIdProvider(){
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
// 為不同的數據庫廠商起別名
properties.setProperty("MySQL","mysql");
properties.setProperty("Oracle","oracle");
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}

}/<code>


3、xxxMapper.xml中通過databaseId指定數據庫類型


<code><select>
SELECT * FROM 表名 LIMIT 1
/<select>

<select>
SELECT * FROM 表名 WHERE ROWNUM <= 1
/<select>/<code>


三、Oracle中創建常用函數


這裡根據個人項目情況去實際應用即可~

1、ORACLE_TO_UNIX

Oracle時間 Date類型轉換為Unix時間戳,等同於mysql中的UNIX_TIMESTAMP

<code>create or replace function ORACLE_TO_UNIX(in_date IN DATE) return number is  
begin
return( ROUND( (in_date -TO_DATE('19700101','yyyymmdd'))*86400 - TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone),1,3))*3600, 0) );
end ORACLE_TO_UNIX;/<code>


2、FIND_IN_SET


<code>CREATE OR REPLACE FUNCTION FIND_IN_SET(piv_str1 varchar2, piv_str2 varchar2, p_sep varchar2 := ',')
RETURN NUMBER IS
l_idx number:=0; -- 用於計算piv_str2中分隔符的位置

str varchar2(500); -- 根據分隔符截取的子字符串
piv_str varchar2(500) := piv_str2; -- 將piv_str2賦值給piv_str
res number:=0; -- 返回結果
loopIndex number:=0;
BEGIN
-- 如果piv_str中沒有分割符,直接判斷piv_str1和piv_str是否相等,相等 res=1
IF instr(piv_str, p_sep, 1) = 0 THEN
IF piv_str = piv_str1 THEN
res:= 1;
END IF;
ELSE
-- 循環按分隔符截取piv_str
LOOP
l_idx := instr(piv_str,p_sep);
loopIndex:=loopIndex+1;
-- 當piv_str中還有分隔符時
IF l_idx > 0 THEN
-- 截取第一個分隔符前的字段str
str:= substr(piv_str,1,l_idx-1);
-- 判斷 str 和piv_str1 是否相等,相等 res=1 並結束循環判斷
IF str = piv_str1 THEN
res:= loopIndex;
EXIT;
END IF;
piv_str := substr(piv_str,l_idx+length(p_sep));
ELSE
-- 當截取後的piv_str 中不存在分割符時,判斷piv_str和piv_str1是否相等,相等 res=1
IF piv_str = piv_str1 THEN
res:= loopIndex;
END IF;
-- 無論最後是否相等,都跳出循環
EXIT;
END IF;
END LOOP;
-- 結束循環
END IF;
-- 返回res

RETURN res;
END FIND_IN_SET; /<code>


四、工具類(MySQL語句轉換Oracle語句)


替換步驟:


  1. 在 xxxMapper.xml 中將所有sql語句上加入 databaseId="mysql"
  2. 複製一份mysql的sql(即 將替換的oracle語句)
  3. 在複製的sql上加入databaseId="oracle"
  4. 找出mysql與oracle語句區別,然後替換sql

溫馨小提示: 這裡工具類只供參考,實際操作根據自己的項目做修改哦,操作前建議先備份自己的項目,以防操作不當丟失代碼哦!

<code>import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import java.io.*;
import java.util.*;

/**
*

mysql遷移oracle 測試工具類


*

* @description :
* @author : zhengqing
* @date : 2020/1/08 10:10
*/
public class MySQLToOracleTest {

private final static String ORACLE_SQL = " ";

@Test // 替換項目中的sql語句
public void testSQL() throws Exception {
String path = System.getProperty("user.dir") + "\\\\src\\\\main\\\\java\\\\com\\\\zhengqing\\\\\\xxx"; // TODO 這裡替換為自己的項目路徑
File file = new File(path);
HashMap<object> fileMap = new HashMap<>();
getAllFileByRecursion(fileMap, file);

fileMap.forEach((key, value) -> {
String fileXmlName = (String) key;
File fileXml = (File) value;
String filePath = fileXml.getPath();

if (fileXmlName.equals("Test.xml")) {
System.out.println(filePath);
try {
// 1、加入 databaseId="mysql"
addMysql(filePath);
// 2、複製一份oracle的sql
if (!checkHasOracle(filePath)) {
copyMysqlToOracle(filePath);
}
// 3、加入 databaseId="oracle"
addOracle(filePath);
// 4、替換mybatis `xxxMapper.xml` 中的sql語句
repalceSQL(filePath);
} catch (IOException e) {
e.printStackTrace();
}
}

});
System.out.println(fileMap);
}

/**
* 替換mybatis `xxxMapper.xml` 中的sql語句
*/

private static void repalceSQL(String path) throws IOException {
File file = new File(path);
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 內存流, 作為臨時流
CharArrayWriter tempStream = new CharArrayWriter();
// 替換
String line = null;
int row = 0;
int rowOracle = 0;
while ((line = bufIn.readLine()) != null) {
row++;
if (line.contains(ORACLE_SQL)) {
rowOracle = row;
}

if (rowOracle != 0 && row > rowOracle) {
// ① 替換 `LIMIT` -> `AND ROWNUM <= 1` TODO 【注: 部分包含`ORDER BY` 關鍵字,需單獨處理】
if (line.contains("limit") || line.contains("LIMIT")) {
System.out.println();
System.out.println(" ==============================↓↓↓↓↓↓ copy分頁所需 (" + row + ") ↓↓↓↓↓↓===================================== ");
System.out.println("SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM (");
System.out.println();
System.out.println(") TMP WHERE ROWNUM <=1) WHERE ROW_ID > 0");
System.out.println();
}
line = StringUtils.replace(line, "limit 1", "AND ROWNUM <= 1");
line = StringUtils.replace(line, "LIMIT 1", "AND ROWNUM <= 1");
line = StringUtils.replace(line, "limit 0,1", "AND ROWNUM <= 1");
line = StringUtils.replace(line, "LIMIT 0,1", "AND ROWNUM <= 1");

// ② oracle中不能使用“ ` ”符號
line = StringUtils.replace(line, "`", "");

// ③ CONCAT('%', #{name}, '%') -> '%'||#{name}||'%' (Oracle中concat函數只能放兩個參數)
if (line.contains("concat")) {
// String newLine = line.substring(line.indexOf("concat(") + 7, line.lastIndexOf("'%')") + 3);
line = line.replaceAll(",", " || ");
line = line.replaceAll("concat", "");
}
if (line.contains("CONCAT")) {
// String newLine = line.substring(line.indexOf("CONCAT(") + 7, line.lastIndexOf("'%')") + 3);
line = line.replaceAll(",", " || ");
line = line.replaceAll("CONCAT", "");
}


// ④ `UNIX_TIMESTAMP` -> `ORACLE_TO_UNIX` date類型時間轉10位時間戳
line = line.replaceAll("UNIX_TIMESTAMP", "ORACLE_TO_UNIX");

// ⑤ 部分關鍵字需加上雙引號 TODO 【注: 字段名大寫,映射的別名需保存原本小寫!】 `level -> "LEVEL"` `user -> "USER"` `number -> "NUMBER"` `desc -> "DESC"`
List<string> keywordList = new ArrayList<>(Arrays.asList("level", "user", "number"));
if (!line.contains("test=")) {
for (String e : keywordList) {
// StringUtils.swapCase(e) : 大小寫互換
line = line.replaceAll(" " + e + " ", " \"" + StringUtils.swapCase(e) + "\" ");
line = line.replaceAll("." + e + " ", "\\\\.\"" + StringUtils.swapCase(e) + "\" ");
if (line.endsWith(e) || line.endsWith(e + ",")) {
line = line.replaceAll(e, "\"" + StringUtils.swapCase(e) + "\"");
}
}
}
if (line.endsWith(" date") || line.endsWith(" date,") || line.endsWith(" 'date'") || line.endsWith(" 'DATE'") || line.endsWith("DATE")) {
line = line.replaceAll(" date", " \"date\"");
line = line.replaceAll(" date,", " \"date,\"");
line = line.replaceAll(" 'date'", " \"date\"");
line = line.replaceAll(" 'DATE'", " \"date\"");
line = line.replaceAll(" DATE", " \"date\"");
}
line = line.replaceAll(" date ", " \"date\" ");
line = line.replaceAll(" DATE ", " \"date\" ");

// ⑥ `IFNULL` -> `NVL`
line = line.replaceAll("IFNULL", "NVL");
line = line.replaceAll("ifnull", "NVL");

// ⑦ 時間 `str_to_date` -> `to_date` `date_format` -> `to_char`
// `%Y-%m-%d` -> `yyyy-MM-dd` `%Y-%m` -> `yyyy-MM`
line = line.replaceAll("str_to_date", "TO_DATE");
line = line.replaceAll("STR_TO_DATE", "TO_DATE");
line = line.replaceAll("date_format", "TO_CHAR");
line = line.replaceAll("DATE_FORMAT", "TO_CHAR");

// 這裡注意替換順序問題,最長的應該放最前面!!!
line = line.replaceAll("%Y-%m-%d %H:%i:%S", "yyyy-MM-dd HH24:mi:ss");
line = line.replaceAll("%Y-%m-%d %H:%i:%s", "yyyy-MM-dd HH24:mi:ss");
line = line.replaceAll("%Y-%m-%d %H:%i", "yyyy-MM-dd HH24:mi");
line = line.replaceAll("%Y-%m-%d %H", "yyyy-MM-dd HH24");
line = line.replaceAll("%Y-%m-%d %h", "yyyy-MM-dd HH");
line = line.replaceAll("%Y-%m-%d", "yyyy-MM-dd");

line = line.replaceAll("%Y-%m", "yyyy-MM");
line = line.replaceAll("%Y", "yyyy");
line = line.replaceAll("%H", "HH24");
line = line.replaceAll("%k", "HH24");

line = line.replaceAll("now\\\\(\\\\)", "(SELECT SYSDATE + 8/24 FROM DUAL)");
line = line.replaceAll("NOW\\\\(\\\\)", "(SELECT SYSDATE + 8/24 FROM DUAL)");

// ⑧ ...

// 需手動處理的SQL 【 group by | 批量插入 | ... 】
}
// 將該行寫入內存
tempStream.write(line);
// 添加換行符
tempStream.append(System.getProperty("line.separator"));
}
// 關閉 輸入流
bufIn.close();
// 將內存中的流 寫入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
}

/**
* 加入 databaseId="mysql"
*/
private static void addMysql(String path) throws IOException {
File file = new File(path);
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 內存流, 作為臨時流
CharArrayWriter tempStream = new CharArrayWriter();
// 替換
String line = null;
while ((line = bufIn.readLine()) != null) {
if ((line.contains("<select> if (line.endsWith(">")) {
line = line.replaceAll(">", " databaseId=\"mysql\">");
} else {
line = line + " databaseId=\"mysql\"";
}
}
// 將該行寫入內存
tempStream.write(line);

// 添加換行符
tempStream.append(System.getProperty("line.separator"));
}
// 關閉 輸入流
bufIn.close();
// 將內存中的流 寫入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
}

/**
* 加入 databaseId="oracle"
*/
private static void addOracle(String path) throws IOException {
File file = new File(path);
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 內存流, 作為臨時流
CharArrayWriter tempStream = new CharArrayWriter();
HashSet<string> lineSet = new HashSet<>();
// 替換
String line = null;
while ((line = bufIn.readLine()) != null) {
if (line.contains("databaseId=\"mysql\"")) {
if (lineSet.contains(line)) {
line = line.replaceAll("databaseId=\"mysql\"", "databaseId=\"oracle\"");
}
lineSet.add(line);
}
// 將該行寫入內存
tempStream.write(line);
// 添加換行符
tempStream.append(System.getProperty("line.separator"));
}
// 關閉 輸入流
bufIn.close();
// 將內存中的流 寫入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
}

/**
* 複製一份oracle的sql

*/
private static void copyMysqlToOracle(String path) throws IOException {
File file = new File(path);
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 內存流, 作為臨時流
CharArrayWriter tempStream = new CharArrayWriter();
// 替換
String line = null;
// 需要替換的行
List<string> lineList = new LinkedList<>();

int row = 0;
int firstRow = 0;

while ((line = bufIn.readLine()) != null) {
row++;
if (line.contains("<select> firstRow = row;
}

// 添加替換內容
if (firstRow != 0 && row >= firstRow && !line.contains("")) {
lineList.add(line);
}

// 查詢結束位置
if (line.contains("")) {
tempStream.append(System.getProperty("line.separator"));
tempStream.write(ORACLE_SQL);
tempStream.append(System.getProperty("line.separator"));
tempStream.append(System.getProperty("line.separator"));

lineList.forEach(lineValue -> {
// copy mysql 語句 轉為oracle
try {
tempStream.write(lineValue);
tempStream.append(System.getProperty("line.separator"));
} catch (IOException e) {
e.printStackTrace();
}
});

tempStream.append(System.getProperty("line.separator"));
}
// 將該行寫入內存
tempStream.write(line);
// 添加換行符

tempStream.append(System.getProperty("line.separator"));
}
// 關閉 輸入流
bufIn.close();
// 將內存中的流 寫入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
}

/**
* 檢查是否已經複製SQL
*/
private static boolean checkHasOracle(String path) throws IOException {
File file = new File(path);
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 內存流, 作為臨時流
CharArrayWriter tempStream = new CharArrayWriter();
// 替換
String line = null;
boolean result = false;
while ((line = bufIn.readLine()) != null) {
if (line.contains(ORACLE_SQL)) {
result = true;
}
// 將該行寫入內存
tempStream.write(line);
// 添加換行符
tempStream.append(System.getProperty("line.separator"));
}
// 關閉 輸入流
bufIn.close();
// 將內存中的流 寫入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
return result;
}

/**
* 遞歸文件夾 -> 找到所有xml文件
*/

private static void getAllFileByRecursion(HashMap<object> fileMap, File file) {
File[] fs = file.listFiles();
for (File f : fs) {
String fileName = f.getName();
if (f.isDirectory()) {
// 若是目錄則遞歸,否則打印該目錄下的文件
getAllFileByRecursion(fileMap, f);
}
if (f.isFile() && fileName.endsWith(".xml")) {
fileMap.put(fileName, f);
}
}
}

}/<object>/<select>/<string>/<string>/<select>/<string>/<object>/<code>


五、總結

這裡簡單說下MySQL和Oracle中的SQL區別,以及mysql語句轉換oracle語句示例

1、分頁


  1. mysql: LIMIT 0,1
  2. oracle: ROWNUM <= 1

情景①:mysql中不含ORDER BY


<code>-- mysql
SELECT * FROM 表名 LIMIT 1

-- oracle
SELECT * FROM 表名 WHERE ROWNUM <= 1/<code>


情景②:mysql中含ORDER BY


<code>-- mysql
SELECT * FROM 表名 ORDER BY 字段名 DESC LIMIT 1


-- oracle
SELECT * FROM (
SELECT TMP.*, ROWNUM ROW_ID FROM (
SELECT * FROM 表名 ORDER BY 字段名 DESC
) TMP WHERE ROWNUM <= 1 )
WHERE ROW_ID > 0;/<code>


溫馨小知識:SQL SELECT語句執行順序


  1. FROM子句組裝來自不同數據源的數據
  2. WHERE子句基於指定的條件對記錄進行篩選
  3. GROUP BY子句將數據劃分為多個分組
  4. 聚集函數進行計算
  5. HAVING子句篩選分組
  6. 計算所有表達式
  7. ORDER BY對結果進行排序

2、oracle中字段名不能使用符號 " ` " 包括


<code>-- mysql
SELECT `字段名` FROM 表名

-- oracle
SELECT 字段名 FROM 表名/<code>


3、字符串拼接


注: Oracle中CONCAT函數只能放兩個參數,因此改為 || 拼接

  1. mysql: CONCAT('%', 'xxx' , '%')
  2. oracle: '%' || 'xxx' || '%'
<code>-- mysql
SELECT 字段名 FROM 表名 WHERE 字段名 LIKE CONCAT('%','helloworld','%')

-- oracle
SELECT 字段名 FROM 表名 WHERE 字段名 LIKE ('%' || 'helloworld' || '%')/<code>


4、date類型時間轉10位時間戳


  1. mysql: UNIX_TIMESTAMP
  2. oracle: ORACLE_TO_UNIX (注:此函數為步驟三中手動創建的,並非oracle自帶哦!)

5、字段名為Oracle關鍵字需加上雙引號


溫馨小提示: 字段名需大寫,如果Java實體類對應字段為小寫,映射的別名注意需保持原本小寫與之對應 ~

例如:

  1. level -> "LEVEL"
  2. user -> "USER"
  3. number -> "NUMBER"
  4. desc -> "DESC"
  5. date -> DATE

6、判斷是否為 NULL: 如果x為NULL,則返回value,否則返回x值本身


  1. mysql: IFNULL(x, value)
  2. oracle: NVL(x, value)

7、日期時間互換


前mysql,後oracle

  1. 字符串類型轉時間類型: STR_TO_DATE -> TO_DATE
  2. 時間類型轉指定字符串類型: DATE_FORMAT -> TO_CHAR
  3. 獲取系統當前時間: NOW() -> SELECT SYSDATE FROM DUAL
<code>-- 時間類型轉指定字符串類型
SELECT DATE_FORMAT( NOW(),'%Y-%m-%d %H:%i:%s'); -- mysql
SELECT TO_CHAR( SYSDATE,'yyyy-MM-dd HH24:mi:ss') FROM DUAL; -- oracle

-- 字符串類型轉時間類型
SELECT STR_TO_DATE( NOW(), '%Y-%m-%d %H'); -- mysql
SELECT TO_DATE( '2020-01-09', 'yyyy-MM-dd') FROM DUAL; -- oracle 【 注:oracle中前者字符串時間的格式需與後者轉換格式相同哦~ 】

-- 獲取系統當前時間
SELECT NOW(); -- mysql
SELECT SYSDATE + 8/24 FROM DUAL; -- oralce 【注:如果服務器時間沒有相差8小時則無需加上`8/24`】

-- mysql
SELECT YEAR( NOW() ); -- 求年份
SELECT QUARTER( NOW() ); -- 求季度
SELECT MONTH( NOW() ); -- 求月份

-- oracle
SELECT TO_CHAR(SYSDATE, 'Q') FROM DUAL; -- 求季度/<code>

另外這裡給出小編所用到的時間標識符格式

<code>-- 前:mysql             後:oracle 

"%Y-%m-%d %H:%i:%S" "yyyy-MM-dd HH24:mi:ss"
"%Y-%m-%d %H:%i:%s" "yyyy-MM-dd HH24:mi:ss"
"%Y-%m-%d %H:%i" "yyyy-MM-dd HH24:mi"
"%Y-%m-%d %H" "yyyy-MM-dd HH24"
"%Y-%m-%d %h" "yyyy-MM-dd HH"
"%Y-%m-%d" "yyyy-MM-dd"
"%Y-%m" "yyyy-MM"
"%Y" "yyyy"
"%H" "HH24"
"%k" "HH24"/<code>


8、判斷時 左 右 字段類型必須相同


這裡注意是必須,可能在oracle版本不同的情況下,老版本不同類型也會查詢出來,但建議還是改為相同類型關聯,避免以後數據庫版本升級出現問題!!!

建議小轉大,比如:數字轉字符串;並使用CONCAT去修改類型,因為mysql和oracle都支持此函數,並且不會在特殊類型上出現問題 ~

<code>-- ex: `JOIN` 關聯表時 兩張表的關聯`字段類型`必須`相同`
SELECT a.*,b.*
FROM 表1 a
LEFT JOIN 表2 b on a.字符串類型字段 = CONCAT(b.數字類型字段, '')/<code>


9、批量插入


<code>-- mysql
<insert>
INSERT INTO 表名( `字段名1`, `字段名2`, `字段...`) VALUES
<foreach>
#{item.字段1},#{item.字段2},#{item中的每一個字段名...}
/<foreach>
/<insert>

-- oracle
<insert>
INSERT INTO 表名(字段名1,字段名2,xxx...)
SELECT A.*
FROM(
<foreach>
SELECT
#{item.字段1},#{item.字段2},#{item中的每一個字段名...}
FROM DUAL
/<foreach>
) A
/<insert>/<code>


10、分組 GROUP BY

oracle中GROUP BY分組後,查詢出來的所有字段(除分組字段)必須為聚合函數的字段,否則會報錯!

解決:

  1. 查詢字段改為聚合函數
  2. 使用如下分析函數 OVER (Partition BY ...) 及開窗函數
<code>-- mysql
SELECT 字段名,xxx... FROM 表名 GROUP BY 分組字段


-- oracle
SELECT
*
FROM (
SELECT tb.*, ROW_NUMBER ( ) OVER ( PARTITION BY tb.分組字段 ORDER BY tb.排序字段 DESC ) AS result
FROM (
SELECT 字段名,xxx... FROM 表名 -- 此處為查詢sql,去掉`GROUP BY`分組條件,將分組字段加到上面 【 注:此sql的查詢字段中要麼全是聚合函數字段,要麼都不是! 】
) tb
) WHERE result = 1/<code>


11、Oracle中表的別名不能用AS, 列的別名可以用AS

why ?:為了防止和Oracle存儲過程中的關鍵字AS衝突的問題

12、注意Oracle對數據類型要求很嚴!!!

MySQL與Oracle不同之處遠不止小編提及的如上幾點,更多的還需要大家根據在實際項目中做對比,然後去修改哦 ~

六、本文MySQL轉Oracle語句工具源碼


溫馨小提示:如果後期小編空閒會將上文中提供的測試替換工具類再加以修改,到時候會存放在如下github倉庫中...

https://github.com/zhengqingya/mysql-to-oracle


分享到:


相關文章: