## 数据库路由

数据库访问统一通过**DB接口**，使用**ksql**语法。



苍穹系统支持每个应用使用独立的数据库，SQL在哪个库上执行，通过DBRoute来路由。

DB屏蔽了物理数据库信息，用DBRoute对应具体的物理库，由mc的数据中心管理进行配置。

DBRoute内置了常用的routeKey，也可以通过 `DBRoute.of("routeKey")` 来构造。

表单的routeKey在设计时已自动指定，在同一个应用下的表单routeKey相同。



##  快速上手

用法基本与jdbc同，DB更为简单，不必自行处理数据连接、结果集的关闭等。

- 新增用户：


```java
String sql = "insert into t_sec_user(fid,fnumber) values(?,?)";
Object[] params = { ID.genLongId(), "Test-Number" };

DB.execute(DBRoute.basedata, sql, params);
```

- 查询HR简历遍历结果集：


```java
String algoKey = getClass().getName() + ".query_resume";
String sql = "select fid, fnumber from t_hr_resume where fstatus=?";
Object[] params = { 'C' };

try (DataSet ds = DB.queryDataSet(algoKey, DBRoute.of("hr"), sql, params)) {
	while (ds.hasNext()) {
		Row row = ds.next();
		System.out.println("fid=" + row.get(0));
		System.out.println("fnumber=" + row.get(1));
	}
}
```

- 查询，使用自定义`ResultSetHandler`对结果集`ResultSet`进行遍历：


```java
String sql = "select id from t_sec_user where fphone like ?";
Object[] params = { "135%" };

List<Long> idList = DB.query(DBRoute.basedata, sql, params, rs -> {
	List<Long> ret = new ArrayList<>();
	while (rs.next()) {
		ret.add(rs.getLong(1));
	}
	return ret;
});
```



## algoKey说明

参数algoKey用于跟踪性能，当前执行的时间将被记录到指标系统。

这个值，应能有效定位到代码, 如=类名+方法，至少含类名。



## DB接口

### 增删改查

- 查询：query、queryDataSet
- 更新：update
- 新增、删除：execute、executeBatch

````java
public static <T> T query(DBRoute dbRoute, String sql, ResultSetHandler<T> rh)
    
public static <T> T query(DBRoute dbRoute, String sql, Object[] params, ResultSetHandler<T> rh)
    
public static DataSet queryDataSet(String algoKey, DBRoute dbRoute, String sql)

public static DataSet queryDataSet(String algoKey, DBRoute dbRoute, String sql, Object[] params)

public static int update(DBRoute dbRoute, String sql)
    
public static int update(DBRoute dbRoute, String sql, Object[] params)
    
public static boolean execute(DBRoute dbRoute, String sql)
    
public static boolean execute(DBRoute dbRoute, String sql, Object[] params)
    
public static int[] executeBatch(DBRoute dbRoute, String sql, List<Object[]> paramsList)
````

query方法用自定义ResultSetHandler处理jdbc结果集ResultSet。

```java
public interface ResultSetHandler<T> {
	T handle(ResultSet rs) throws Exception;
}
```

###  数据库元信息

```java

public static DBType getDBType(DBRoute dbRoute)

public static String getDatabaseName(DBRoute dbRoute)
    
public static boolean exitsTable(DBRoute dbRoute, String tableName)
    
public static boolean exitsIndex(DBRoute dbRoute, String tableName, String indexName)
    
public static List<String> getPrimaryKeys(DBRoute dbRoute, String table)  
    
```

### 指定查询超时时间

每次执行超时时间默认为5分钟：配置参数`db.query.timeout=300`(单位秒)

执行超过query time，则执行中断并抛出`SQLException`。

如需要自定义，如10分钟超时：

```java
try(QueryTimeout timeout = DB.timeout(600)){
	// 在这里，每次DB的调用的超时时间为10分钟。
	DB.update...
	DB.query...
}
```

### 优化in参数转临时表
- in优化参数配置：


```properties
# 优化阀值：默认1000
orm.opt.in.threshold=1000

# in允许的最大个数：默认50w
orm.opt.in.maxsize=500000
```

当启用in优化，in的参数个数>=优化阀值时，将对过滤条件信息优化。

in参数个数超过orm.opt.in.maxsize，则系统抛异常，异常信息形如：

> in query params length over the maximum：510000>500000

- 
  接口：

```java
/**
 * @param dbRoute
 * @param sb 
 * @return
 */
public static boolean execute(DBRoute dbRoute, SqlBuilder sb)   

/**
 * 查询
 * @param dbRoute
 * @param sb 
 * @param rh
 * @return T
 */
public static <T> T query(DBRoute dbRoute, SqlBuilder sb, ResultSetHandler<T> rh)
    
/**
 * DataSet查询
 * @param algoKey
 * @param dbRoute
 * @param sb 
 * @return
 */
public static DataSet queryDataSet(String algoKey, DBRoute dbRoute, SqlBuilder sb)
```

- 例子：

```java
	@Test
	public void testDBQuerySqlBuilder(){
		//设置启用阀值
		System.setProperty("orm.opt.in.threshold", "100");
		List<Object> pkIds = getPKs(100);
		//oraginal sql= SELECT FID FROM T_SEC_USER WHERE FID IN(?,?,?...) ORDER BY FID 		DESC
		SqlBuilder sqlBuilder = new SqlBuilder();
		sqlBuilder.append("SELECT FID FROM T_SEC_USER WHERE");
		sqlBuilder.appendIn("FID", pkIds);
		sqlBuilder.append("ORDER BY FID DESC");
		DataSet ds = DB.queryDataSet("test_sqlBuilder", DBRoute.basedata, sqlBuilder);
	}
	
	public List<Object> getPKs(int size){
		String sql = "SELECT TOP "+size+",0 FID FROM T_SEC_USER";
		DataSet ds = DB.queryDataSet("InQueryToTempTable_getPKS", DBRoute.base, sql);
		List<Object> list = new ArrayList<>();
		for(Row r:ds){
			Object o = r.get(0);
			list.add(o);
		}
		return list;
	}
```



## 结果集

DB对查询返回的ResultSet进行了统一的封装和转换。

###  空格和0长度串

数据库字段若为字符串类型，则不允许保存null和0长度的字符串，无值默认用" "。

用户仍正常使用，存取值由DB层处理转换：

> - 保存：参数传入""或null，则保存为" "。 必须通过参数设置才会自动转换。(ParameterSetter)
>
>
> -  查询：" "转为""。 通过DB结果集转换。(WrapCloseResultSet)

控制是否转换的参数：db.spaceAsEmptyString，默认值为true。

### bigint类型值

ksql bigint类型的字段，取值返回类型统一 为long。

### boolean类型参数

参数传boolean类型，则在jdbc参数设置时自动转为'0'或'1'。(对应的存储字段类型应为char(1))

> 注意：在ResultSet中取值仍为'0'或'1'，在ORM的DynamicObject中的值则为true或false。



## 禁止新建线程访问

若需要开启新线程，用ThreadPools启动。

自行new Thread被禁止使用DB访问数据库。



## 开发环境的SQL日志

日志配置参见 [Log](/log/)

在开发环境，如果info级别的日志被关闭，则可以通过以下代码，在控制台输出。

```java
// 启动前开启DB日志 (加在BosDebugInOne里)
System.setProperty("db.sql.out", "true");

// 运行期前开启DB日志(仅用于调试，不要在产品中使用，会导致日志配置失效)
DB.setSqlOut(true);
```



**SQL跟踪调试1**：在AbstractDBImpl.logSQL方法中设置断点。

**SQL跟踪调试2**：为方便对sql进行编码式过滤调试，提供了自定义SqlLogger。在BosDebugInOne start之前，增加InitListener，设置SqlLogger(见下面代码)，把SQL再次log出来过滤。不受db.sql.out参数的控制，只要设置了SqlLogger，日志必输出，不影响原有的日志输出。

```java
webInit.addInitListener(new InitListener() {
	@Override
	public void started() {
		DB.setSqlLogger(new SqlLogger() {
			@Override
			public void log(String sql, Object...params) {
				if (sql.toLowerCase().indexOf("t_sec_user") != -1) {
					// debug，在这里设置断点进行调试。
					System.err.println("### 用户表sql: " + sql);
				} else {
					System.out.println("### SQL: " + sql);
				}
			}
		});
	}
});

webInit.start(); 
```

> **上例输出**：
>
> \### 用户表sql: /\*ORM\*/ SELECT B.fdptid "entryentity.dpt.id"
> FROM t_SEC_User A
> LEFT JOIN t_SEC_UserPosition B ON B.FId=A.FId
> WHERE A.FId = ?



## 不常用接口

```java
// 是否启用了xdb引擎(水平分表)，配置参数：xdb.enable=false (默认值)
public static boolean isXDBEnable()

// 是否开启读写分离，配置参数：db.splittingread.enable=true (默认值)
public static boolean isSplittingReadEnable()

// 是否开启自动读写模式，配置参数：db.splittingread.autoreadwrite=false (默认值)
public static boolean isSplittingAutoReadWrite()
    
// 获取当前事务已写的库路由标识，无事务或尚未写，则返回null。
public static String getWrittenRouteKey()
```



## DB日志

数据库访问的日志名称为"DB"。

日志内容形如：

```sql
[scheduleTask-BIZJOB-1/traceId:6283b4735a8e0b8b/time:1573522750557] DB ----sql@basedata->basedata#con7----
UPDATE T_SCH_TASK SET FRUNAT = ?  WHERE FID = ? 
172.19.109.231,757682138324724736
```

