AstroZero脚本开发规则及约束限制
脚本性能检查规则
在AstroZero脚本编辑器中编写脚本代码时,请仔细阅读如下内容,了解脚本性能的检查规则。在AstroZero标准页面和高级页面中,对页面性能进行分析时,会分析页面所引用脚本的性能。如果脚本存在性能问题,界面会通过弹框提示您,方便您直观的发现脚本中的问题。如何在标准页面和高级页面进行性能分析,请参见查看AstroZero标准页面性能、查看AstroZero高级页面性能。
- 静态检查规则
- 规则1:SELECT语句需要带上where条件
SELECT语句中缺少查询条件,存在由于查询结果数量过大,导致脚本性能不佳的风险。请确认查询结果,如果查询结果数量过大,建议增加限制条件或采用分页查询,分页查询请见下方推荐代码示例。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select object_name from object_demo order by createddate desc")
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select object_name from object_demo where id = ? order by createddate desc")
推荐代码示例:import * as db from 'db'; let se = db.sql() let sqlResult = [] for(let i=0; i <1000; i++){ let query_sql = se.exec("select object_name from object_demo limit ${5000*i},5000") sqlResult.push(query_sql) if(query_sql.length<5000){ break } }
- 规则2:SELECT语句中单次limit查询数量需要低于平台最高查询个数5000的限制
SELECT语句中单次limit查询数量大于平台限制个数,存在由于查询结果数量过大,导致脚本性能不佳的风险。如果查询结果过大,请判断是否需要增加限制条件或采用分页查询,分页查询请见下方推荐代码示例。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select object_name from object_demo where object_type = 'Test' limit 100000")
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select object_name from object_demo where object_type = 'Test' limit 3000")
推荐代码示例:import * as db from 'db'; let se = db.sql() let sqlResult = [] for(let i=0; i <1000; i++){ let query_sql = se.exec(`select object_name from object_demo limit ${5000*i},5000`) sqlResult.push(query_sql) if(query_sql.length<5000){ break } }
- 规则3:SELECT语句中谨慎使用区间查询条件
SELECT语句中使用区间查询(“<>”、“<”、“>”、“>=”、“<=”),存在由于查询结果数量过大,导致脚本性能不佳的风险。如果必须采用区间查询,建议增加limit限制条件,以免影响查询效率。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select object_name from object_demo where object_num > 50")
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select object_name from object_demo where object_num > 50 limit 3000")
- 规则4:SELECT语句中查询字段不在表的索引库中
如果SELECT语句where条件中,查询字段并未创建索引,请判断该字段是否需要创建索引,以提高代码查询效率。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select object_name from object_demo where object_id = ?")
表“object_demo”中的“object_id”并没有创建索引。
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select object_name from object_demo where id = ?")
表“object_demo”中的“id”创建了索引。
- 规则5:SELECT语句中尽量避免同时从大于等于4张表中取数据
SELECT语句中,进行多表关联查询时,尽量不要同时从大于或等于4张表中获取数据。如果必须要查询大于或等于4张表的数据时,建议先通过关联少于4张表进行查询,然后根据查询结果再做关联查询,保证每次关联查询的表数量少于4,以提高查询效率。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select a.item as item1, b.item as item2, c.item as item3 ,d.item as item4, e.item as itmem5 from object_demo1 as a object_demo2 as b, object_demo3 as c ,object_demo4 as d, object_demo5 as e where a.id=b.objectid and b.name = c.objectid and c.name = d.objectid and d.name = e.objectid and e.name ='Test'")
正确代码示例:import * as db from 'db'; let sqlResult1 = db.sql().exec("select c.item as item3, d.item as item4, e.item as itmem5 from object_demo3 as c ,object_demo4 as d, object_demo5 as e where c.name = d.objectid and d.name = e.objectid and e.name ='Test'") let sqlResult = [] let item3s = "'"+sqlResult1.map(item=>{item.item3}).join("','")+"'" let sqlResult2 = db.sql().exec('select a.item as item1, b.item as item2, c.item as item3 from object_demo1 as a object_demo2 as b, object_demo3 as c where a.id=b.objectid and b.name = c.objectid and c.item in ('+item3s+')') for(let i in sqlResult1){ for(let j in sqlResult2){ if i.item3 == j.item3: sqlResult.append({ "item1":j.item1, "item2":j.item2, "item3":i.item3, "item4":i.item4, "item5":i.item5 }) } }
- 规则6:SELECT语句中聚集函数必须增加别名
SELECT语句中,聚合函数必须使用别名方式存储查询结果,以免因聚合函数返回的结果不一致,导致存在兼容性问题。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select count(*) from object_demo where object_name = 'Test'")
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select count(*) as count from object_demo where object_name = 'Test'")
推荐代码示例:***聚合函数示例 **** select count(*) as count_res, select max(*) as max_res, select min(*) as min_res, select avg(*) as avg_res, select sum(*) as sum_res
- 规则7:SELECT语句中严禁使用“select from...”形式查询语句
严禁使用“select ...”形式查询语句,请指出select的具体字段。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select from object_demo where object_name = 'test'")
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select id, object_type from object_demo where object_name = 'test'")
- 规则8:SELECT语句中拼接的参数值请谨慎使用入参变量
SELECT语句中,拼接的参数值请谨慎使用入参变量,以免引起SQL注入的风险。
错误代码示例:import * as db from 'db'; let errorDemo = "select id,name from object_demo where id = "; errorDemo += input.parameter let errorDemoResult = db.sql().exec(errorDemo)
其中,“input.parameter”为脚本入参。
正确代码示例:import * as db from 'db'; let correctDemo = "select id,name from object_demo where id = ?"; let correctDemoResult = db.sql().exec(correctDemo, { params: [input.parameter] })
其中,“input.parameter”为脚本入参。
- 规则9:“for”循环中请谨慎使用SELECT语句
“for”循环中,请谨慎使用SELECT语句,以免后续进行结果赋值时,由于数据过多,导致内存溢出。
错误代码示例:import * as db from 'db'; for (let i = 0; i < input.para.length; i++) { db.sql().exec("select id,name from object_demo where id = Test'"); }
正确代码示例:import * as db from 'db'; db.sql().exec("select id,name from object_demo where id = 'Test'");
- 规则10:SELECT语句中谨慎使用order by
SELECT语句中,请谨慎使用order by。如果需要使用order by,请为排序字段增加索引,以提高查询效率。如果无法增加索引,需要关注是否存在查询性能低下的风险。
错误代码示例:import * as db from 'db'; let errorDemo = db.sql().exec("select object_name from object_demo where object_id = 'Test' Orde by createdDate")
表“object_demo”中的“createdDate”,并没有创建索引。
正确代码示例:import * as db from 'db'; let correctDemo = db.sql().exec("select object_name from object_demo where id = 'Test' Order by createdDate")
“object_demo”中的“createdDate”,创建了索引。
- 规则1:SELECT语句需要带上where条件
- 动态检查规则
脚本运行时,触发动态检查。动态检查规则以静态检查规则为基础,无法检查SQL注入以及for循环中的SQL项。
- 当存在脚本性能问题时,通过弹框界面提示给用户,方便用户直观发现脚本中的问题。
- 当不存在性能问题时,便不展示弹框页面,以免影响用户开发。
脚本开发规范及限制
在AstroZero脚本编辑器中编写脚本代码时,建议您先了解脚本编写的规范和限制,便于快速地完成脚本代码的编写。
- 命名限制
所有的名称定义要能体现其作用,避免使用缩写(专有名词除外)。
- 脚本采用小驼峰命名,例如createDeviceInstance。
- 结构体(struct)采用大驼峰命名,例如QueryPaymentResult。
- 结构体内的字段采用小驼峰命名,例如customerName 。
- 类、枚举值和接口采用大驼峰命名。
class Comments {...} interface Console {...} enum Direction { Up = 1, Down, Left, Right}
- 函数采用小驼峰命名。
addOfferingAssociatePriceAndStock(input: Input): Output {...}
- 属性或变量采用小驼峰命名。
class Comments { userId: String; content: String; } let oneComments = newComments();
变量为单数时,命名包含对象名称,如level1Catalog、level2Catalog。变量为复数时,命名包含集合名称,如attributeRelationRuleList。
- 脚本定义规则
- 每个字段的定义,均需要定义type、label、description、required和isCollection,有默认值的非必填。
- 当字段为集合类型时,需要定义成“[]”。
@action.param({ type: "Attribute", label: "Attribute", description: "attributeList", required:false isCollection: true }) attributeList: Attribute[];
- @action.object时,需要在脚本中详细定义清楚Object,不要引用其他脚本的Object。
- 如果是嵌套结构体,则从下到上粒度依次变小。
export class ProductObject { //ignore } export class StockObject { //ignore } export class ProductStock { @action.param({ type: 'Struct', label: "ProductObject", isCollection: false }) productInfomation: ProductObject; @action.param({ type: 'Struct', label: "StockObject", isCollection: false }) stock: StockObject; } @action.object({ type: "param" }) export class Input { @action.param({ type: 'String', label: "productId", isCollection: false }) productId: String; }
- 不需要定义多个Output对象,可以直接在方法使用定义的对象出参。
@action.object({ type: "param" }) export class Input { @action.param({ type: 'String', label: "productId", isCollection: false }) productId: String; } @action.object({ type: "param" }) export class getProductStock { @action.method({ input: "Input", output: "ProductStock", label: 'getProductStock' }) getProductStock(input: Input): ProductStock {
【不推荐】:如果出参对象包含从外部引入的对象,还是要按该方式定义。
@action.object({ type: "param" }) export class Input { @action.param({ type: 'String', label: "productId", isCollection: false }) productId: String; } @action.object({ type: "param" }) export class Output { @action.param({ type: 'Object', isCollection: false }) ProductStock: ProductStock; } @action.object({ type: "param" }) export class getProductStock { @action.method({ input: "Input", output: "Output", label: 'getProductStock' }) getProductStock(input: Input): Output {
- 除非业务有特殊要求,新增修改脚本不返回结果码和结果信息。
- 脚本注释的原则和写法
无用的代码不能以注释形式存在,能用代码说明的尽量不要添加注释,脚本注释尽可能简洁。建议注释统一用英文,出入参不必写注释说明。
- 在函数方法和结构体的元数据描述上,添加注释。
/** * 根据产品ID查询产品详情信息 */ @action.method({ input: "Input", output: "Output", label: 'queryProductDetailForCart' })
- 方法内关键业务语句前,必须添加注释。
- 方法内的单行注释以“//”开头,应放在相关代码的上方或右方,不可放于下方。如果放于上方,注释需与前面的代码间用空行隔开。
- 注释与代码的比例没有量化标准。在删掉所有的代码内容、仅保留代码层级结构和注释,如果通过注释,可以很容易理解方法内都做了哪些事情(类似于伪代码),则认为注释比例合理。
// 校验密码是否正确 let password = input.password; if (accountRecord["Password"] != password) { error.name = "CM-001003"; error.message = "Invalid loginId or password."; throw error; }
- 在函数方法和结构体的元数据描述上,添加注释。
- 脚本间相互引用规则
- 脚本中不要包含没用到的标准库或对象的引用。
如果实际上只用到了CA_PC_Offering对象,则示例语句中其他对象需要删除:
@useObject(['CA_PC_FeeItemDef', 'CA_PC_Offering', 'CA_PC_CatalogOffering', 'CA_PC_Catalog', 'CA_PC_OfferingAttribute'])
- 只引用使用到的对象,而不用import *。
import { OfferingObjectQuery } from './ca_pc__getOfferingObject'; import { setI18nError } from 'context'; import { sql } from 'db';
【不推荐】:
import * as queryOfferingObjectAction from './ca_pc__getOfferingObject'; import * as context from 'context'; import * as db from 'db';
- @action.method属于必须的方法注解,写在方法上面。
@action.method({ input: "Input", output: "Output", label: 'queryClassification' }) queryClassification(input: Input): Output { ... }
- 为方便页面直接调用脚本,仅在脚本最后统一导出需要的对象,而不是导出所有对象。如果不需要,可以不用导出。
例如,getOfferingObject脚本中最后一行导出OfferingObjectQuery对象:
export let theAction = new OfferingObjectQuery();
- 可以把公共的对象Object定义,按分类放在一些公共的脚本里(例如pc_XXX.ts、cm_XXX.ts),其他需要的脚本直接引用即可。
- 脚本中不要包含没用到的标准库或对象的引用。
- 语法规则
- 对象判空
if(object){ //ignore }
集合对象示例:
if(collection&&collection.length!=0){ //ignore }
- 数字类型统一定义成“Number”、日期类型定义成“Date”。
@action.param({ type: "Date", label: "effectiveTime", description: "Date." }) effectiveTime: Date; @action.param({ type: "Number", label: "salePrice", description: "salePrice." }) salePrice: number;
- 变量或数组定义时,要说明类型,集合需要加上泛型。
let isDone:Boolean = false; let decLiteral : number =6; let productList = new Array<productObject>();
- 遍历循环推荐用“forEach”,不推荐用“for…in”。
testArray.forEach((value, index, array )=>{ //ignore });
- 推荐用“let”变量声明,不推荐用“var”。
【推荐】:
let offeringId : String = “aaa”;
【不推荐】:
var offeringId = “aaa”;
- 前段代码有对象声明时,推荐使用“.fieldName”获取对象的字段,而不是用“[‘fieldName’]”。
【推荐】:
offeringStruct.id = result[i].base.offeringId;
【不推荐】:
offeringStruct.id = result[i][‘base’][‘offeringId’];
- 在需要默认值的情况下,使用“||”代替“if”判断。
function(value){ //使用||对value进行判空 value = value || “hello” }
- 创建一个已知对象时,推荐用“new”方法。
【推荐】:
let offeringIdRequest = new OfferingIdRequest(); offeringIdRequest.catalogList = catalogList; offeringIdRequest.classificationList:=classificationList; let getOfferingIdByConditionInput = { "OfferingIdRequest": offeringIdRequest }
【不推荐】:
let getOfferingIdByConditionInput = { "OfferingIdRequest": { "catalogList": catalogList, "classificationList": classificationList, "status": "", "stockCondition": "", "keyWord": "", "offset": "", "limit": "" } }
- 函数定义
let traitRec = function(xxxx,xxx) { //ignore }
只能在函数定义后的语句中,使用该函数。
- 没有初始化值的变量申明,使用“undefined”,不要使用“null”。
let object = undefined;
- 局部变量需要在“class”内定义,不要在全局命名空间内定义类型/值(即不要在“class”外定义变量),常量可以定义成全局。
let identityIdList = [];
- 使用“lambda”表达式代替匿名函数。
只有需要时,才把“arrow”函数的参数括起来。正确使用“arrow”的示例如下:
x => x + x (x,y) => x + y <T>(x: T, y: T) => x === y
- 对象判空
- 脚本SQL语句规则
- 不推荐用拼接SQL方法,避免注入风险。
let sql = "select id,name,ExternalCode,FeeType,FeeItemName,FeeItemDescription,Remark,Status from CA_PC_FeeItemDef where 1=1"; if (!InputParams.id && !InputParams.name && !InputParams.feeType && !InputParams.feeItemName && !InputParams.status) { context.setI18nError("ca_pc__001013"); return; } if (InputParams.id) { sql += "and id ='" + InputParams.id + "'" }
- 多表复杂查询建议用“sql.exec()”或“sql.excute()”方法,“excute()”方法比“exec()”多返回字段集和操作成功数。
let result = execsql.exec("select id,name,OfferingId,ParentId,SkuCode,ChannelId,Status,ProductLabel,PriceCode,DefaultChoose,PaymentType from CA_PC_Product where id in (" + str + ")", { params: productId });
多表复杂查询只能使用拼接SQL方法,但是有限制,例如示例中的“str”要求如下:
- str如果来源于入参,则入参在拼接SQL之前需要进行校验,以免引入SQL注入攻击。如果来源于内部数据,可以不进行校验。
- 不推荐直接在SQL中拼接入参,应该采用在SQL中拼接占位符,然后把入参放入参数的数组中,例如:
attriSql = "select AttrDef from DE_DeviceDefAttri where DeviceDef=? and ExternalCode=? and AttrType='DYNAMIC' and ValueType='1'"; attriRecords = db.sql().exec(attriSql, { params: [deviceDefId, defExternalCode] });
- 对于单表查询和增删改SQL,推荐使用Orm接口方法。
Options选项可以选择返回字段、排序、聚合运算和分页等功能。当value不存在时,默认为null类型。
let CA_PC_Stock = db.object('CA_PC_Stock'); let amountCount = 0; if (UpdateStock.amount) { let record = { Amount: UpdateStock.amount }; amountCount = CA_PC_Stock.updateByCondition({ "conjunction": "AND", "conditions": [{ "field": "SkuCode", "operator": "eq", "value": UpdateStock.skuCode }] }, record); }
- 避免在循环中调用方法和操作数据库,可以用“in”来查询在集合中的结果。
let productIdList = input.productId; let str = ""; let arr = []; for (let i = 0; i < productIdList.length; i++) { arr[i] = "?"; } str = arr.toString(); let result = execsql.exec("select id,name,OfferingId,ParentId,SkuCode,ChannelId,Status,ProductLabel,PriceCode,DefaultChoose,PaymentType from CA_PC_Product where id in (" + str + ")", { params: productIdList });
- 对sql进行优化时,尽量使用有索引的字段,避免使用没有索引的字段。
- 批量操作数据库时,尽量使用已封装好的批量操作接口。
例如,Orm.batchInsert、Orm.batchUpdate接口和Orm.deleteByCondition批量删除接口。如果批量创建父子对象记录,且批量创建的记录在一个完整的事务中,全部成功或全部失败,建议使用Orm.compositeInsert接口。
var s = db.object('Customer__CST'); var records = []; var record = { "name": "hello", "count__CST": 123, "Contacts": { "records": [ { "name": "hello_contact1" }, { "name": "hello_contact2" }, ] } }; records.push(record); var ids = s.compositeInsert(records); console.log("id list = ", ids); count = s.count(); console.log("record count = ", count);
- 匹配查询推荐用like,日期比较推荐用“<”、“>”。
【推荐】:
select id from t where name like ‘abc%’ select id from t where createdate>=’2005-11-30’ and createdate<’2005-12-1’
【不推荐】:
select id from t where substring(name,1,3)=’abc’ select id from t where datediff(day,createdate, ‘2005-11-30’)
- 使用exists替代in,使用not exists替代not in。
SELECT * FROM EMP (基础表) WHERE EMPNO > 0 AND EXISTS (SELECT ‘X’FROM DEPT WHERE DEPT.DEPTNO = EMP.DEPTNO AND LOC = ‘MELB’)
【不推荐】:
SELECT * FROM EMP (基础表) WHERE EMPNO > 0 AND DEPTNO IN(SELECT DEPTNO FROM DEPT WHERE LOC =’MELB’)
- 避免在索引列上使用is null和is not null,会造成索引失效。
SELECT … FROM DEPARTMENT WHERE DEPT_CODE >=0
【不推荐】:
SELECT … FROM DEPARTMENT WHERE DEPT_CODE IS NOT NULL
- 注意事项
- 尽量避免Select子句中,使用“*”。
- 尽量避免在where子句中,使用!=或<>操作符。
- 尽量避免在where子句中,对字段进行函数操作。
- 尽量避免在SQL中,使用 “!=” 、“||”、“+”符号。
- 尽量避免在where条件中,做数据筛选。
- 尽量避免查询中,按字段排序。
- 尽量避免多表关联查询和嵌套查询,不要使用超过2表的关联查询。
- 不要在循环内重复使用同一条件查询,应该在循环外处理。例如,公共数据仅在循环外查询一次。
- 不要在同一个脚本的多个方法内,使用同一条件多次查询,可以定义类的成员变量。
- 避免关联查询
- 避免频繁的数据库交互
例如,查询5000条数据,查询补充数据的时候,不要在循环内多次交互数据库,把可以合并的条件在循环外拼接并进行一次性查询,在循环内只需要从结果集中获取数据,可以极大提升查询性能。
- 尽量利用对象做临时缓存
例如查询到DeviceDef后,按照“id:Object”的方式存起来,后续查询时先判断缓存对象中是否已经存在,如果存在则直接获取不再查询。
- 不推荐用拼接SQL方法,避免注入风险。
- 脚本代码风格限制
- 每句话后面加分号,脚本写完右键选择“Format Document”统一格式。
- string类型赋值统一使用双引号,获取字段统一使用单引号。
- 相关代码写在一起,不相关逻辑最好以空行隔开。
- 总是使用“{}”把循环体和条件语句括起来。
- 开始的“{”总是在同一行。
- 小括号里开始不要有空白逗号,冒号、分号后要有一个空格。
for (var i = 0, n = str.length; i < 10; i++) { } if (x < 10) { } function f(x: number, y: string): void { }
- 每个变量声明语句只声明一个变量。
var x = 1; var y = 2;
而不是下面的方式
var x = 1, y = 2;
- else要在结束的“}”后,另起一行。
- 一个函数仅完成一件功能,即使是简单功能也应该编写单独的方法实现。
- 单个方法的方法体不要太长,建议控制在150行以内,保证代码可读性,也方便维护、测试。
- 脚本扩展名限制
因为脚本实际上是存在数据库中的,所以脚本没有路径的概念,扩展名也没有特别的意义。
导入模块时,尽量采用不带扩展的方式。如果必须要带扩展名,只允许“.ts”扩展名文件。
import * as circle from './circle';
- 脚本循环依赖限制
当循环调用模块时,一个模块可能在未完成执行时被返回。因此,需要仔细地规划模块间调用,以允许循环模块依赖在应用程序内正常工作。
例如,脚本a:
console.log('a 开始'); exports.done = false; import * as b from 'b'; console.log('在 a 中,b.done = ', b.done); exports.done = true; console.log('a 结束');
脚本b:
console.log('b 开始'); exports.done = false; import * as a from 'a'; console.log('在 b 中,a.done = ', a.done); exports.done = true; console.log('b 结束');
脚本main:
console.log('main 开始'); import * as a from 'a'; import * as b from 'b'; console.log('在 main 中,a.done = ', a.done ',b.done = ', b.done);
当main加载a时,a又加载b。 此时,b又会尝试去加载a。 为了防止无限的循环,会返回一个a的exports对象的未完成的副本给b模块。然后b完成加载,并将exports对象提供给a模块。当main加载这两个模块时,都已完成加载,因此该程序的输出会是:
main 开始 a 开始 b 开始 在 b 中,a.done = false b 结束 在 a 中,b.done = true a 结束 在 main 中,a.done=true,b.done=true