HyperSQL调研学习文档(四)

最近调研HyperSQL,把整理的资料记录一下,并分享给大家,由于时间略紧,内容肯定有遗漏和谬误的地方,欢迎大家指正。本人也会持续的修改更新。

5. 核心概念及数据结构

5.1 database, catalog, schema与schema对象

 

schema代表了一组schema对象及相应权限的概念,是在catalog之下又进行的一层逻辑划分。而schema对象,根据文档定义,是指用来包含数据或者管理数据或者对数据执行操作的database对象。例如:表、视图、等等。直观的说,schema对象就是用于描述数据库的各个概念及这些概念之间逻辑关系的元数据。每一个schema对象属于一个特定的schema。schema对象可以根据他们的特征被分为不同的组。
一个SQL环境的基本持久化元素是database对象。database包含了目录(catalogs)及其授权(authorizations)。一个目录(catalog)包含了一个特殊的叫做INFORMATION_SCHEMA的schema。这个schema是只读的,包括了一些视图(views)和其他的schema对象。
这些视图包含了存在于该目录(catalog)中的所有的database对象,加上所有的授权。每一个database对象都有一个名字。一个名字是一个标识符,并在它的命名空间内是唯一的。
在HyperSQL中,每一个database中只有一个目录(catalog)。该目录的名字是PUBLIC。你可以使用ALTER CATALOG RENAME TO语句来重命名目录(catalog)。所有的schemas都属于这个目录。目录名与database的文件名没有关系。
一些类型的schema对象可以独立于其他schema对象而生存。另一些类型则只能作为其他schema对象的元素而存在。当父对象被删除时,这些不独立的对象也会自动被销毁。

 

对于不同类型的schema对象,存在着隔离的命名空间。一些命名空间被两种相似类型的schema对象所分享。

SchemaManager是用来统一管理数据库中各元素(表、视图等等)结构定义元数据的。任何DDL都需要通过SchemaManager来修改SchemaObject来达到更改数据库结构定义的目的。

各种各样的schema对象可以相互依赖,例如一个schema对象可以包含对另一个schema对象的引用。这些引用可以跨越schema的界线。schema对象间的相互引用和跨边界引用在某些环境下是被允许的,在另一些环境下是不允许的。
可以使用DROP语句来销毁schema对象。如果非独立的schema对象存在,一个DROP语句只有在包含了CASCADE分句时才能执行成功。在这种情况下,非独立对象绝大多数时候会被销毁。
在一些情况下,例如删除DOMAIN对象,非独立对象不是被销毁,而是会删除相关依赖。
一个新的HyperSQL目录(catalog)包含了一个叫做PUBLIC的空schema。默认的,当一个session开始的时候,该schema就是初始schema。
在PUBLIC schema中,新的schema对象可以被定义和使用,这同样也适用于用户创建的任何新的schema。你可以对PUBLIC schema进行重命名。
HyperSQL允许删除所有的schemas,除了在新session开始时默认初始化的schema(默认为PUBLIC)。对于这个默认的schema,DROP SCHEMA ... CASCADE语句会执行成功,但是会导致清空该schema,而不是删除。

 

5.2 表内容存储、维护与查询的数据结构

涉及的类:Table、PersistentStore、Index、Row、NodeAVL
在CREATE TABLE创建表的时候,并未给Table分配实际存储仓储PersistentStore,也就是尚未关联PersistentStore。在创建表时构建其约束、索引、对应的Schema对象结构等。实际的PersistentStore的创建与关联会在第一次insert的时候触发。
一个表可能存在好多个索引,但一定会存在一个主索引(primary Index)。
表创建的时候,默认会将主键创建为primary Index索引(如果有的话),否则引擎会自动为表生成一个TEMP类型的无名索引作为主索引。另外也会为唯一键、外建等自动创建索引。
表的索引结构在Table创建的时候在Table中构建出来,保存在一个indexList的列表中(最少有一个primary Index, 可以有多个Index)。
而当Table对应的PersistentStore被创建并关联时,PersistentStore会将Table的indexList保存在自己内部,并根据indexList生成一个相同长度的accessorList,其中保存的是NodeAVL的列表,每一个NodeAVL都代表了全表所有Rows对应的节点的平衡二叉树的根节点。也就是说在PersistentStore中,与每个Index对应的都存在一棵平衡查找二叉树,其上的每个节点都代表一条表记录(Row)。
实际实现时我们不可能为每一个索引的存储二叉树都新生成一条数据,这样既浪费资源又不便于修改维护。HyperSQL在实现时Row对象只有一个,Row对象内部维护着与Index个数相同的NodeAVL节点,相当于为每一个Index都提供了钩子,在插入这条数据(Row)时,针对每一个Index,将钩子勾在其对应的平衡二叉树上,然后调整这棵树重新达到平衡。

 

 

插入时在查找某节点应该挂在树上的哪个位置时候,由对应的Index来确定比较的逻辑。

 

5.3 HyperSQL的undo日志:session.rowActionList

在Session中包含了名为rowActionList的属性,该属性是一个RowAction的列表,用于表示在事务及语句执行过程中执行的所有动作。RowAction代表了针对某一行数据的一个insert/delete/rollback/commit的动作链,其中保存了行数据备份row,所在表table,对应执行的session,执行的动作类型,以及执行的时间戳等等信息。

在每一次事务回滚或提交确认时都会清空rowActionList中执行该事务的所有actions,也就是说同一个session中的每一次事务中的所有动作都会独占rowActionList,即同一个session只能同时启动一个事务。而在执行每一条语句时候会在session.actionIndex中记录下当前rowActionList的长度,用以指示本条语句所产生所有动作的起始下标,在回滚单条语句执行的时候用到。

在执行插入、删除及更新语句的时候,结合了事务管理器的操作逻辑如下:

 

  • 开启事务,设置session.transactionTimestamp时间戳
  • 对每一条语句的执行,记录下当前的rowActionList的长度,保存到session.actionIndex中,并开启语句执行动作,记录session.actionStartTimestamp时间戳
  • 执行实际的插入或删除动作,然后记undo日志(也就是对每一个动作在rowActionList中追加对应的RowAction)。
  • 视语句执行结果执行回滚语句还是完成语句,如果执行结果错误,那么需要回滚语句执行,此时使用session.actionIndex和session.actionStartTimestamp就可以确定本语句执行期间创建的所有rowActions,对其逐条执行反操作,就可以实现回滚了。而完成语句时,只是做了释放锁等工作,并不对undo日志做任何操作。
  • 最终,在事务提交或回滚的时候,使用0和session.transactionTimestamp就可以确定本次事务所执行的所有rowActions,通过对其逐条执行反操作或者对其逐条记操作日志(commit log)就可以实现整个事务的回滚或确认提交。

5.4 RangeVariable及相关

 

RangeVariable代表了一个带条件(Condition)的数据源的概念。这个数据源可能是Table, TableDerived以及Column或Variable等。
例如select * from table t where t.id>3; 解析成StatementQuery以后,其rangeVariables数组中就会有一个RangeVariable对象,代表了包含t.id>3这个condition的t表。
当我们在查询的时候,查询语句会被解析成StatementQuery,在StatementQuery中包含了分层查询的概念。其属性包括查询表达式queryExpression,rangeVariables[]以及subqueries[]:

 

  •     其中rangeVariables[]代表了本查询语句所涉及到的各个层次查询中的所有范围变量。
  •     queryExpression代表了本层的查询表达式,其中包括了字段表达式(exprColumns[])、查询条件(queryCondition)以及若干范围变量(rangeVariables),这里的范围变量仅限于本层所需使用的范围变量。
  •     subqueries[]代表了本查询语句所依赖的所有子查询,subquery最常见的就是一个TableDerived。在执行查询时候,如果存在subqueries就先执行各subquery的materialize方法对其进行填充,而各层的查询会以subqueries的填充结果作为数据来源(也可能直接通过Table等其他来源),这些数据来源会封装为RangeVariable,维护在各层查询(queryExpression)的rangeVariables[]中。

一个TableDerived代表通过查询得到的一个表,并没有实际存储,因此其内部维护着它自己的queryExpression;该TableDerived也可能查询另外的TableDevired来获得结果,以此类推。因此TableDerived可以是嵌套获取的,对应着SQL语句的嵌套查询。
由前面介绍可知,queryExpression包含了字段表达式(exprColumns[])、查询条件(queryCondition)以及若干范围变量(rangeVariables)。
如果TableDerived的查询源仍是TableDerived的话,那么其queryExpression中的rangeVariables里仍会包含TableDerived类型的RangeVariable,这就组成了嵌套查询的概念。
当一个StatementQuery执行时,首先会处理其涉及到的所有subqueries,将上面提到的所有RangeVariable中的TableDerived进行查询填充,填充过程是按层次深度反向进行的,比如subquery1的depth为1,它依赖了depth为2的subquery2,那么会先去执行subquery2,再基于subquery2的结果来执行subquery1。
在数据源统统准备好以后,开始执行本层的queryExpression。
如此便可将一个复杂的查询分层简单化处理,在每一层都只处理自己的业务逻辑,不用关心下一层子查询的业务逻辑。

 

6. SQL执行流程

 

6.1 connect

1. 当客户端调用DriverManager.getConnection(url, user, password)来获得一个Connection时,客户端最终会通过:

 

    Result login = Result.newConnectionAttemptRequest(user, password,
    database, zoneString, timeZoneSeconds);

来组装出一个尝试连接的ResultConstants.CONNECT类型的请求并发送给服务端。

2. 服务端收到socket连接后新建一个ServerConnection来处理该请求,首先执行init函数,在完成握手(shakehand)以后得到客户端传递来的请求,并对其进行处理。
3. 对于传递来的尝试连接的请求,其type==ResultConstants.CONNECT,因此会执行setDatabase(resultIn)函数,其中委托DatabaseManager来创建一个新的Session:

 

    session = DatabaseManager.newSession(dbID, user,
                                 resultIn.getSubString(),
                                 resultIn.getZoneString(),
                                 resultIn.getUpdateCount());

其内部委托dbID对应的database.connect来处理,最终委托到database对应的sessionManager来实际执行创建session的动作:

 

    Session session = sessionManager.newSession(this, user,
            	databaseReadOnly, true, zoneString, timeZoneSeconds);

其中新创建一个session,初始化以后放入sessionMap中进行统一维护,并将sessionIdCount加1。

4. 如果创建成功,则构建一个Result类型的结果并返回

    return Result.newConnectionAcknowledgeResponse(session);

5. 如有异常,则构建一个Result类型的异常结果并返回

 

    Result.newErrorResult(e);

6. 客户端接收到服务端返回的结果,并做处理:

 

	Result resultIn = execute(login);
        if (resultIn.isError()) {
            throw Error.error(resultIn);
        }
        sessionID              = resultIn.getSessionId();
        databaseID             = resultIn.getDatabaseId();
        databaseUniqueName     = resultIn.getDatabaseName();
        clientPropertiesString = resultIn.getMainString();
        randomID               = resultIn.getSessionRandomID();Result resultIn = execute(login);
        if (resultIn.isError()) {
            throw Error.error(resultIn);
        }
        sessionID              = resultIn.getSessionId();
        databaseID             = resultIn.getDatabaseId();
        databaseUniqueName     = resultIn.getDatabaseName();
        clientPropertiesString = resultIn.getMainString();
        randomID               = resultIn.getSessionRandomID();

若有异常,则抛出异常。否则根据返回结果设置本ClientConnection的属性值,注意sessionID和databaseID。
至此,一个客户端到服务端的Connection就建立起来了,实质上对应着一次与database引擎的会话(Session)。

 

 

6.2 drop table

1. 当客户端使用:

 

 

	Statement stmnt = connection.createStatement();
	stmnt.execute("DROP TABLE T123 IF EXISTS;");

来执行drop table动作。首先在使用createStatement函数创建Statement时,初始化JDBCStatement就会使用:

 

	Result.newExecuteDirectRequest();

为其创建一个ResultConstants.EXECDIRECT类型的resultOut。
然后在执行stmnt.execute时,根据sql语句以及stmnt的一系列属性设置resultOut:

 

	resultOut.setPrepareOrExecuteProperties(sql, maxRows, fetchSize,
                statementRetType, queryTimeout, rsProperties, generatedKeys,
                generatedIndexes, generatedNames);

最终调用JDBCConnection中的Session代理实现类来发送请求给服务端:

 

        	resultIn = connection.sessionProxy.execute(resultOut);

2. 如前所述,服务端收到一个ResultConstants.EXECDIRECT类型的请求,将其对应的sql语句解析并执行。对于"drop table ..."语句,会解析为一个StatementSchema类型的对象cs,并执行其cs.execute(session)。        

 

3. 其中,首先获得schemaManager:

 

SchemaManager schemaManager = session.database.schemaManager;

因为StatementSchema就是DDL解析而成的,而DDL定义或改变数据库的结构,数据类型,表之间的链接和约束等作用主要就是在SchemaManager中进行。然后判断解析后语句的类型type,此处为StatementTypes.DROP_TABLE,因此会执行删表相应的逻辑:

 

    首先获得删除表语句中的参数:name,objectType,cascade,ifExists。其中name是要删除表的表名,objectType为StatementTypes.DROP_TABLE,cascade代表是否级联删除,ifExists代表了IF EXISTS从句。

 

    如果ifExists为true,并且删除表所属的schema不存在,那么直接返回。如果ifExists为false,并且删除表所属的schema不存在,则报异常。否则继续

    检查当前session是否有操作指定对象的权限:schema是否相符并且拥有操作其中指定对象的权限。

    如果ifExists为true,并且要删除的表不存在,那么直接返回。如果ifExists为false,并且要删除的表不存在,则报异常。否则继续

    如果cascade不为true,说明不是级联删除,那就调用schemaManager.checkObjectIsReferenced(name);来检查是否有其他对象关联参数指定的schema对象,若存在需要级联删除的情况,则报错。
    最后根据语句类型调用到dropTable(session, name, cascade)函数,其中首先委托schemaManager得到由schemaName和tableName确定的表对象,然后调用其dropTableOrView方法来实际执行删除动作:

 

 	session.database.schemaManager.dropTableOrView(session, table,
                	cascade);

4. 在dropTableOrView方法中,首先判断要删除的是一个Table还是一个View,执行相应的函数dropTable/dropView。

 

由于我们删除的是一个表,所以执行dropTable,其动作是删除所有的外部约束(如果cascade为false,若被外部主键关联会报异常),外部关联等,然后删除表,详细步骤如下:

 

 

  •     遍历本表的所有约束,将外建约束与MAIN型约束添加到externalConstraints递归判断关联到本表的对象,并将其统统放在externalReferences中。
  •     在设置为非级联删除的情况下,判断是否存在级联删除的情况,如果存在就报异常
  •     处理外部约束externalConstraints(包括了MAIN和FOREIGN_KEY约束),将其关联不同于本表的主表或引用表,主约束名和引用约束名,以及引用约束对应的索引,分别添加到tableSet、constraintNameSet和indexNameSet中
  •     遍历本表的所有约束,将唯一键约束与主键约束添加到uniqueConstraintNames
  •     将本表封装进工具类TableWorks中,该工具类中的方法用于更改一个存在的表的结构,可能会导致创建一个新的表对象
  •     借助包装类TableWorks的方法删除tableSet中包含的所有表中的dropConstraintSet和dropIndexSet指定的所有外建约束/MAIN约束及索引(外建约束导致的索引),返回的table集合中的table对象很可能是新生成的table对象
  •     借助包装类TableWorks的方法将tableSet指定的所有tables,重新设置进schemaManager中,因为这些tables可能已经是新生成的对象了
  •     借助包装类TableWorks的方法更新tableSet中每一个表的外建约束(由于表对象很可能重新创建了,与原来不是同一个对象)中对于该表的引用
  •     删除所有关联依赖本对象的对象:对于cascade为true的情况,可能存在级联删除的其他对象;而当cascade为false时,只能是本对象的子对象
  •     得到本表中所有对其他对象有关联关系的字段,所有关联到其他表的外建约束,以及所有定义在本表的trigger。删除所有这些CONSTRAINT、TRIGGER及COLUMN对象对其他对象的引用关系(只是删除引用关系,不是删除引用的对象)
  •     删除其他对象对本表唯一键约束或主键约束的引用关系
  •     删除其他对象对本表的引用关系及本表对于其他对象的引用关系
  •     在所属schema中删除对于本表的schema对象、本表涉及到的索引schema对象、本表涉及到的约束schema对象及本表涉及到的trigger schema对象
  •     实际执行删除一个表对象,包括删除所有针对该表的权限,删除表的triggers以及本表使用的持久化仓储(PersistentStore)
  •     最终对于tableSet中的所有tables,找到所有关联依赖它们的对象,对于其中指定类型(有重新编译必要)的对象执行其compile函数重新编译    

5. 最终执行完毕后,返回一个Result.updateZeroResult给客户端。

 

6.3 create table

1. 客户端执行动作同上,只是sql语句变为了"create table ..."。
2. 服务端收到一个ResultConstants.EXECDIRECT类型的请求,将其对应的sql语句解析并执行。对于"create table ..."语句,会解析为一个StatementSchema类型的对象cs,并执行其cs.execute(session)。

 

 

3. 其中,首先获得schemaManager:

 

SchemaManager schemaManager = session.database.schemaManager;

 

因为StatementSchema就是DDL解析而成的,而DDL定义或改变数据库的结构,数据类型,表之间的链接和约束等作用主要就是在SchemaManager中进行。
然后判断解析后语句的类型type,此处为StatementTypes.CREATE_TABLE,因此会执行创建表相应的逻辑:

 

  • 首先获得语句解析后的参数列表:table, tempConstraints, tempIndexs, statement, ifNotExist等。其中table对应着要创建的表的对象,设置了表名、scope、type、字段定义等信息。tempConstraints代表了语句定义中要加于表上的约束,包括了主键约束(PRIMARY_KEY)、外建约束(FOREIGN_KEY)、唯一键约束(UNIQUE)、其他表的外建关联了本表主键的约束(MAIN)、检查约束(CHECK)、以及临时约束(TEMP)。其中非空约束是检查约束的一种情况。另外对于定义了主键的表,约束列表的第一项一定是主键约束;对于未定义主键约束的表,约束列表的第一项会是系统自动创建的临时约束。约束列表一定不会为空,其中第一项总是会被拿来创建主索引(primaryIndex),表的行数据一定会被插入到主索引中,当然也可能会存在其他非主索引。tempIndexs代表了语句中定义的索引。statement代表了在CREATE TABLE AS ...语句中的AS后面的查询部分,如果statement不为null,则会将statement执行的查询结果插入到新建的表中。
  • 接着执行setOrCheckObjectName(session, null, table.getName(), true) 设置表名对应的schemaObject所属的schema,并检查确保当前session对schema有操作的权限,以及检查schema中表名所属类型的命名空间中没有与表名重名的schemaObject。
  • 若参数中的tempConstraints不为空,则执行ParserTable.addTableConstraintDefinitions(session, table, tempConstraints, foreignConstraints, true)。其中首先获得tempConstraints中的第一个Constraint c(其代表了主索引约束,若存在主键,则第一个c代表了主键约束,否则系统会默认生成一个tempConstraint临时约束),使用该约束创建表的主索引(primaryIndex),若该索引是由主键约束生成的,那么在对应的字段定义中设置主键标志,并将主键约束添加到表中。
  • 处理完第一个constraint以后,遍历从第二个到最后一个constraint,视其constraintType类型执行不同的操作:UNIQUE, FOREIGN_KEY及CHECK,其中会对UNIQUE和FOREIGN_KEY类型的约束创建索引。    
  • 接着执行 table.compile(session, null),逐个执行表中各字段的compile,用于处理存在GENERATED字段的情况
  • 然后执行 SchemaManager.addSchemaObject(table) 使用SchemaManager在table对应的schema中的Table类型命名空间中添加table对象,并在referenceMap中添加由table到其他对象的关联关系,另外还会额外添加table的字段到其他对象的引用关系,若有的话。
  • 若tempIndexs不为空,处理tempIndex逻辑(待补充)
  • 若statement不为空,处理statement逻辑。此处的statment是在创建表时的查询语句,若有,则执行该语句,并将查询结果插入到表中。
  • 若table有大对象字段(Blob, Clob),则执行大对象字段的逻辑(略)
  • 最终返回:Result.updateZeroResult

执行session.database.schemaManager.setSchemaChangeTimeStamp() 将SchemaManager的SchemaChangeTimpStamp设置为全局最新的数字。
4. 最终执行完毕后,返回一个Result.updateZeroResult给客户端。

 

 

6.4 Query

1. 客户端执行动作同上,只是sql语句变为了"SELECT * FROM xx WHERE xxx"这样的。注意,其执行的方法是statement.executeQuery()。
2. 服务端收到一个ResultConstants.EXECDIRECT类型的请求,将其对应的sql语句解析并执行。对于"select ..."语句,会解析为一个StatementQuery类型的对象,其属性包括了查询表达式queryExpression,涉及到的范围变量rangeVariables[]以及子查询subqueries[],在StatementQuery中包含了分层查询的概念,详细说明请参照5.4节RangeVariable及相关。得到解析后的查询语句StatementQuery cs后执行cs.execute(session)。
3. 其中先处理子查询(如果有的话):

 

 

 

    if (subqueries.length > 0) {
           materializeSubQueries(session);
    }

在materializeSubQueryies方法中对subqueries中的每一个subquery(TableDerived类型)执行其materialise方法,该方法的作用是使用查询结果填充本TableDerived表。

说明:对于有嵌套查询的情况例如(此示例语句选取了不会被优化的情况):

SELECT t.I, t.B, m.A FROM T t, (SELECT * FROM MYTABLE WHERE O<1) m WHERE t.I = m.O

上述语句被解析后生成的StatementQuery的subqueries中就会包含一个subquery,代表了SELECT * FROM MYTABLE WHERE O<1这句查询应该生成的TableDerived。在此说明一下几个概念的相互关系:一条查询语句会被解析成StatementQuery,其中包括了子查询subqueries数组和queryExpression查询表达式。其中的每一个子查询subquery都是一个TableDerived类型的对象,代表了由子查询执行结果所创建的持久化范围为Statement的表,当其执行materialise方法时会完成实际数据的填充过程(在一条语句执行过程中,该子查询对应的TableDerived可能会反复填充,例如对于join中的右表,或者correlated的查询)。而queryExpression是一个QueryExpression/QuerySpecification类型对象,表示该StatementQuery对应的查询语句的整个查询逻辑,其中包含了rangeVariables数组,这些rangeVariables对象都是RangeVariable类型,字面意思是范围变量,代表了一个带条件的数据源的概念。例如示例中的MYTABLE WHERE O<1就是一个表类型的RangeVariable对象,其rangeTable为MYTABLE,条件为O<1。当RangeVariable的rangeTable是一个TableDerived类型时,就表示了嵌套查询的概念:QuerySpecification包含了若干的RangeVariable,RangeVariable的rangeTable又可能是TableDerived,TableDerived也可能包含QuerySpecification。当然,不可能无限制的重复下去,嵌套的终结点是RangeVariable中的rangeTable是Table类型,或TableDerived中包含的是dataExpression而非queryExpression。

因此,在整句的查询之前,对于每一个无需重复计算的subquery(例如非correlated),首先执行其materialise方法来执行查询并将结果填充到该表对应的PersistentStore中。subquery中也可能包含子查询deepersubquery,所以它的执行就依赖于deepersubquery的查询结果,因此查询语句是嵌套执行的,对应的TableDerived有depth属性,表明该子查询处于查询语句的哪个嵌套层次里。subqueries数组中的子查询列表是按一定顺序排列的,对于有依赖的子查询,层次更深的排在前面。执行子查询的填充过程时,最深层次的子查询先执行,后续子查询及最外层查询都可能会依赖上一层子查询的结果。

TableDerived中可能包含dataExpression和queryExpression其中之一:dataExpression对应于IN (0, 1, 2, 3, 4)这样语句中的"0, 1, 2, 3, 4",是直接的数据表达式;而queryExpression对应的一个查询语句,如上所述该查询语句也可以是嵌套的。在执行subquery.materialise(session)方法时,若该subquery是由queryExpression来确定的,那么会首先执行

result = queryExpression.getResult(session, 0)

然后从session对应的persistentStoreCollection相应的命名空间中取出本tableDerived对应的持久化仓储:

store=session.sessionData.getSubqueryRowStore(tableDerived) 

最后执行insertResult(session, store, result)将queryExpression查询所得的数据插入到tableDerived对应的persistentStore中,完成数据填充。

上述过程中QueryExpression.getResult(Session session, int maxrows)方法被其子类QuerySpecification重写过,该方法是整个查询的核心方法,其业务逻辑比较复杂,在下面章节中专门讲解。

4. 在subquery对应的tableDerived填充完毕后,执行整个语句最外层的查询:

 result = getResult(session);

其中执行:

Result result = queryExpression.getResult(session,
            session.getMaxRows());

该方法与前面执行子查询时最终调用的方法是同一个方法。这里的queryExpression代表了整个语句最外层查询的表达式,其执行会基于前面subqueries执行填充后的tableDerived表。该方法中调用到了:

Result r = getSingleResult(session, maxrows);

在getSingleResult方法中,首先得到语句中limit定义的offset和count(若有的话),然后调用:

Result              r         = buildResult(session, limits);

来实际执行查询动作,并将结果保存到r内部的数据导航器navigator中。该查询过程逻辑比较复杂,是整个查询的核心逻辑,将在下面详细描述。

随后对于查询结果进行处理,如果是select distinct语句,则将结果集中的数据去重navigator.removeDuplicates(session)。

如果有order by从句,则根据order by对结果集进行排序navigator.sortOrder(session)。

如果有limit <offset>, <count>从句,那么对结果集进行截取navigator.trim(offset, count)。

最终返回结果Result r。

6.4.1 QuerySpecification的buildResult过程

如前所述,无论是子查询还是最外层查询,最终都会调用到对应的查询表达式解析成的QuerySpecification对象的buildResult方法,方法签名如下:

private Result buildResult(Session session, int[] limits)

其执行过程如下所示:

a. 创建一个RowSetNavigator navigator用于接收符合条件的数据;然后创建一个ResultConstants.DATA类型的Result用于返回查询结果,将navigator设置进去:result = Result.newResult(navigator)。

b. 根据条件设置是否是成组结果(resultGrouped)。若存在limit从句,则设置skipCount和limitCount。

c. 如果是简单的count语句(isSimpleCount为true,不加任何条件的全表count),则直接使用table对应的持久化仓储的elementCount,如果elementCount小于0,则查询第一个索引得到本表的count数并设置进elementCount中。然后将结果设置进navigator中,并返回。

c. 根据本层的rangeVariables的长度,也就是本层查询需要使用的范围变量(rangeVariables)的个数,创建范围迭代器数组rangeIterators[],其中每一项都是执行对应rangeVariables[i].getIterator(session)方法生成的,其中会创建一个RangeIterator并将其设置到session.sessionContext中。会视本范围变量所处的位置是否是被right join,创建RangeIterator的具体实现类RangeIteratorMain或是RangeIteratorRight。其中RangeIteratorRight代表了right join右边的范围变量的迭代器,为了实现right join的逻辑需要进行一些额外的处理,详见下面。

d. 执行如下的循环结构体:

for(int currentIndex=0;;){  
    if(currentIndex<fullJoinIndex){如有有right join的情况,执行right join的额外逻辑}  
    RangeIterator it = rangeIterators[currentIndex];  
    if(it.next()){  
        if(currentIndex<rangeVariables.length-1){currentIndex++; continue;}  
    }else{  
        it.reset(); currentIndex--; continue;  
    }  
    //通过所有RangeIterator的当前值组合出结果data
    if(是单条结果或者是分组结果但结果集中尚不包含data){
        navigator.add(data);
    }else{
        navigator.update(groupData, data);
    }
}  

 

其中,rangeVariables代表了本层查询从几个数据源中获得结果,例如从两个子查询生成的TableDerived表中查询结果,就会有两个rangeVariable。
该循环结构体中,对rangeVariables对应的RangeIterator的处理从左到右依次执行,首先对最左边的RangeIterator,调用其next()找到符合条件Condition的一条数据。
当找到时,如果currentIndex<rangeVariables.length-1,说明还有来源尚未处理,则将currentIndex = currentIndex+1,然后取出对应的rangeIterator[currentIndex]继续循环处理,一直处理到最右侧的rangeIterator。当最右侧的rangeIterator通过next()找到一条记录时,说明一条符合整个查询的结果被找了出来,此时将该记录添加到第一步创建的结果集navigator中。
当右侧的rangeIterator无法找到下一条时,重置该rangeIterator并将currentIndex--,退回到其左侧的rangeIterator,执行其next()找到下一条数据并重复上述过程。

 

最终的结果是从左到右遍历了所有来源的每条记录的组合(当然对于有索引条件的rangeVariable不必从头到尾的扫描,具体rangeIterator.next()逻辑将在下面详细说明),从中过滤出最终符合条件的结果,并添加到结果集navigator中。

在整个从左到右扫描的过程中,对于任一范围变量扫描到的每一条数据,如果发现其符合要求,或者是由于其whereConditions不符合要求,则对其执行addFoundRow()方法,该方法在本范围变量处于right join的右侧时将当前扫描的行ID添加到本范围迭代器的lookup中,供后续right join的额外执行流程使用。

fullJoinIndex初始为0,代表做了right join动作的来源在rangeVariables中的位置下标,若为0则代表尚未做right join。该过程开始执行的判断条件为:

if (currentIndex < fullJoinIndex)

因此只有在currentIndex减到-1才会开始执行该过程,此时意味着查询的整个从左到右遍历查找结果的过程已经完成。其执行逻辑代码如下:

boolean end = true;

//从第二个rangeVariable开始,因为第一个不可能是isRightJoin类型。
for (int i = fullJoinIndex + 1; i < rangeVariables.length; i++) {
    if (rangeVariables[i].isRightJoin) {
        fullJoinIndex = i;
        currentIndex  = i;
        end           = false;

      //将当前的RangeIteratorRight设置为右侧outerRows查询模式 在从左向右查询完毕之后执行该函数,然后继续
        ((RangeIteratorRight) rangeIterators[i])
            .setOnOuterRows();	

        break;
    }
}

if (end) {
    break;
}

其中找到类型为isRightJoin的范围变量,将其对应的RangeIteratorRight设置为右侧outerRows查询模式,代码如下:

public void setOnOuterRows() {

            conditions         = rangeVar.whereConditions;    //对于right join的rangeVariable需要判断的where条件
            isOnRightOuterRows = true;			    //是否处于从左向右计算完毕后,右侧outerRows执行模式
            hasLeftOuterRow    = false;
            condIndex          = 0;

            initialiseIterator();	//此时不再需要考虑joinConditions了,只考虑whereConditions而重新填充该rangeVariable
    }

然后break,重新执行步骤d开始时的循环流程,只是在isOnRightOuterRows模式下,RangeIteratorRight的next()函数查找下一条符合要求的数据的逻辑会有所不同,此时会判断前面添加到lookup中的行数ID。将找到的符合要求的数据添加到结果集中(这些数据代表了right join中右侧符合条件的数据但是在左侧没有对应数据的情况,也属于符合要求的数据)。

e. 如果遍历到最右侧的RangeIterator的next()函数仍返回true,那说明找到一条符合要求的数据,此时使用QuerySpecification中的exprColumns[],根据当前session的sessionContext中的各个迭代器中当前行的值,计算出对应结果字段的值,并组装成一条符合条件的查询结果Object[] data。

f. 在根据各个范围变量当前数据封装成实际要求数据的时候,首先创建用于表示结果的Object[] data,接着计算其中无需考虑聚合的字段的值,然后根据skipCount执行忽略(若不为0的话)。

g. 若存在分组或聚合,则执行groupData = navigator.getGroupData(data); 以获得groupData。如果groupData为null则说明当前查到的是该组中的第一条数据;如果不为空,则说明该组中已经有数据了,而groupData代表了对各数据累积计算的结果,将groupData赋予data。由data来统一代表新生成的或者是累积计算的结果。

h. 对字段中所有的聚合字段执行计算更新:

data[i] = exprColumns[i].updateAggregatingValue(session,
                        data[i]);

exprColumns[i]是一个ExpressionAggregate类型,执行其updateAggregatingValue函数会将当前数据中的值累计计算进data[i]中。

i. 将计算完毕的data添加或更新到navigator中。并继续返回上面循环查找下一条符合要求的数据。

j. 当数据查找并添加到navigator的过程完毕之后,重置所有范围变量对应的范围迭代器。如果本次查询不是分组或聚合的情况,那么查询就完成了,直接返回result。

k. 否则,在存在聚合的情况下,如果不分组并且结果集为空的情况下,最终生成一条计算了聚合结果的语句并加入结果集(尽管计算的数据都是空或者0值)。

j. 最后判断having condition条件,若不符合要求的就从navigator中删除,并将navigator重置后返回result。      

6.4.2 RangeIterator.next()的执行过程

上述d步骤中,对于每一个范围变量,通过与其对应的范围迭代器RangeIterator的next()方法来查找下一条符合条件(conditions)的数据。RangeIterator的两个具体实现子类为RangeIteratorMain和RangeIteratorRight,其中RangeIteratorRight代表了处于right join右侧的范围变量,RangeIteratorMain代表其他所有类型的范围变量。RangeIteratorRight有两个工作模式:正常的从左到右扫描模式,以及右侧outerRows模式。在正常的从左到右扫描模式中,RangeIteratorRight的next()逻辑和RangeIteratorMain是一致的。右侧outerRows模式是在正常从左到右扫描完毕后开启的针对右表存在符合条件数据,但是左表没有对应数据的情况。

• 下面先看正常的next()逻辑:

 

对RangeIterator的conditions中的每一个RangeVariableCondition(在OR这种查询条件的每一个都可以使用索引的情况下,引擎会对其进行优化,将其分为多个RangeVariableCondition,分别对每一个进行查询,然后取其并集):
    1. 首先判断isBeforeFirst,如果为true,说明本范围迭代器还未被填充(其RowIterator it是空的),则首先填充本RangeIterator,初始化其it属性,it就是本RangeIterator用于遍历所有行的行迭代器。在填充过程中,如有必要,首先执行其对应rangeVariable的rangeTable的填充(如果是correlated类型的TableDerived,那么对于每一条外层查询得到的数据,都要重新计算填充)。然后,如果查询条件有索引相关的,则执行精确查询,否则需要执行全表扫描。

 

    2. 然后执行result=findNext():

    a. 遍历上面得到的行迭代器it中的每一行数据,依次对其执行检查:terminalCondition, indexEndCondition, joinConditions.nonIndexCondition, whereConditions.nonIndexCondition, excludeConditions,直到找到符合所有条件的数据,或者遍历完毕或者符合了终结条件直接终结。扫描过程中,如果发现数据符合要求,或者是由于其whereConditions而不符合要求,则对该数据执行addFoundRow()方法,该方法在本范围变量处于right join的右侧时将当前扫描的行ID添加到本范围迭代器的lookup中,供后续right join的额外执行流程使用。

 

    b. 如果遍历完毕或被终结又没找到符合要求的数据,那么进一步判断是不是处于left join的右侧,并且针对左侧的当前数据在本范围变量中尚未找出匹配条件的数据,并且本范围变量处于所有数据源的最右侧。如果都符合条件,那么如果不存在where条件或者符合where条件的要求,则仍会被视为找到了一条符合要求的数据。对应着left join中左侧数据符合要求,右侧没有匹配数据的情况。

 

    3. 如果步骤2找到符合要求的数据,则直接返回true。否则执行reset()将行迭代器it置空,并将isBeforeFirst标志置为true,继续循环执行下一个RangeVariableCondition(若有的话)。

所有RangeVariableCondition都执行完还没有合适的数据,则返回false。

 

• 接下来看处于右侧outerRows模式的RangeIteratorRight的next()逻辑:

由于在将RangeIteratorRight设置为右侧outerRows模式的方法里就执行了initialiseIterator填充方法:

	public void setOnOuterRows() {

            conditions         = rangeVar.whereConditions;
            isOnRightOuterRows = true;
            hasLeftOuterRow    = false;
            condIndex          = 0;

            initialiseIterator();
        }

因此直接执行result = findNextRight()方法:

遍历initialiseIterator()得到的行迭代器it中的每一行数据,依次对其执行检查:indexEndCondition、nonIndexCondition条件、并额外检查本行数据是否在lookup中存在,若存在则不符合要求,直到找到符合所有条件的数据,或者遍历完毕或者符合了indexEndCondition直接终结。若查到了符合条件的数据,则返回true并且此时it中的当前数据就是当前找到的符合条件的数据;否则返回false。

6.5 Insert

1. 客户端执行动作同上,只是sql语句变为了

INSERT INTO T(I, B, A) VALUES(NULL, 'get_column_name2', 'wangd2')

这样的,当然也可以是类似:

INSERT INTO MYTABLE(O, A, B) SELECT I, A, B FROM T WHERE I<22

 

2. 服务端收到一个ResultConstants.EXECDIRECT类型的请求,将其对应的sql语句解析并执行。对于"insert into table ..."语句,会解析为一个StatementInsert类型的对象cs,并执行其cs.execute(session)。
3. 其中先处理子查询(如果有的话)

 

	if (subqueries.length > 0) {
                materializeSubQueries(session);
        }

如上述第二个示例语句中所示,插入的数据也可以是通过查询获得的,而查询中也可能包括嵌套查询。所以也可能涉及到子查询,关于子查询的执行涉及到的数据结构及执行的详细步骤请参见6.4 Query的相关描述。

4. 然后执行具体的getResult动作:

 

首先获得要插入的表对应的持久化仓储,如果还不存在就创建一个:

 

    PersistentStore store = baseTable.getRowStore(session);

其中,通过database.persistentStoreCollection.getStore(table) 来获得或创建,而PersistentStoreCollectionDatabase是通过委托data.logger来具体分配新的持久化仓储的:在Logger为表创建一个新持久化仓储时会将table传递进去,此时,store会获得table的indexList,并创建与之一一对应的accessorList。indexList与accessorList的具体作用请参照5.2章节中的描述。
a. 对于普通的insert而言,其要插入的数据是在插入语句中定义的,并且是单条的情况(参见上面示例语句1),对这种情况首先获取要插入的数据:

 

	Object[] data = getInsertData(session, colTypes,
                                      insertExpression.nodes[0].nodes);

其中insertExpression.nodes[0].nodes中的每一个expression都是expressionValue类型,因此执行expression.getValue(session)获得的就是其明确定义的值。由于是简单insert,因此只会定义一行数据,也就是insertExpression.nodes中只会有一个node。

然后调用insertSingleRow(session, store, data)方法将data数据插入表中:有必要的话自动生成某些字段的值;执行实际插入及记录undo日志;校验数据插入后是否会有CHECK约束或外建约束冲突;并在插入前后触发相应的trigger。其中会调用到:

baseTable.insertSingleRow(session, store, data, null);

该函数是实际执行将数据插入到表中的过程,我们在下面详细说明。

b. 如果不是普通的insert,说明要么是在插入语句中定义了多行数据,此时要插入的数据表达式是定义在insertExpression.nodes中,定义了几行数据就有几个节点;要么是使用查询语句来定义要插入的语句,此时queryExpression存在。因此首先根据queryExpression是否为null执行getInsertValuesNavigator(session)或getInsertSelectNavigator(session)得到要插入的数据集。其中getInsertSelectNavigator会执行queryExpression,执行查询的过程请参见6.4 Query章节。

如果上一步得到的结果集不为空,则调用

insertRowSet(session, generatedNavigator, newDataNavigator)

该函数把newDataNavigator数据集中的数据插入到表中:有必要的话自动生成某些字段的值;执行实际插入及记录undo日志;校验数据插入后是否会有CHECK约束或外建约束冲突;并在插入前后触发相应trigger。其中对于集合中的每一条数据,同样会执行:

baseTable.insertSingleRow(session, store, data, null);

5. 最终返回保存了插入条数的结果:return new Result(ResultConstants.UPDATECOUNT, count);

 

6.5.1 Table.insertSingleRow过程

 

Table用于插入单条数据的函数签名为:

Row insertSingleRow(Session session, PersistentStore store, Object[] data,
                        int[] changedCols)

其执行过程如下:

1. 首先对数据进行自动生成及校验:generateAndCheckData(session, data); 其中:

 

  •     如果有generated字段,则给data中的generated字段设置值
  •     根据字段类型定义,对data进行字段类型的转换与校验
  •     对于存在Domain字段,或者是非空字段的情况进行约束校验

2. 然后委托对应的store(存储仓储)根据数据来创建一个Row:

Row row = (Row) store.getNewCachedObject(session, data, true);

说明:PersistentStore的实现类RowStoreAVL是一个抽象类,实现了持久化仓储的公共抽象部分,而将各不同具体持久化仓储所不同实现的地方开放为抽象函数,供各不同类型仓储自行实现。getNewCachedObject方法就是各不同类型仓储需要各自实现的方法。在此以MEMORY表所对应的RowStoreAVLMemory来介绍,执行创建row对象的过程如下:

  • 得到自增的row行号
  • 构建一个MEMORY类型表的row对象,将table, data以及行号设置进去,并根据表的索引个数构建相同个数的“钩子”:nPrimaryNode指向的一个NodeAVL链,这些NodeAVL统统指向本row对象,并会在真正插入后成为各个索引树中的节点
  • 如果是处在事务中,生成“undo”日志rowAction,挂在row对象上,关于undo日志及rowAction的说明,请参照5.3小节的说明。

3. 执行插入及记录“undo日志”,执行:

session.addInsertAction(this, store, row, changedCols);

该函数的作用是将行对象的插入动作添加到session的rowActionList中,并实际执行row的插入(目前还不写commit日志)。在后面commit或rollback时根据session的rowActionList记commit日志或是回滚。 其中,调用了

data.txManager.addInsertAction(session, table, store, row, changeColumns)

委托事务管理器来执行动作并记录rowAction作为undo日志。TransactionManager是一个接口,定义了HSQLDB事务管理器对外提供的公共方法,以及支持的事务并发管理模型。在HyperSQL 2中支持二阶段锁模型(LOCKS)、多版本并发控制模型(MVCC)以及它俩的混合模型(MVLOCKS)三种。事务管理器接口说明及并发管理模型的实现细节将在另一篇文章中专门介绍。在二阶段锁模型2PL中执行过程如下:

  • 首先调用store.indexRow(session, row) 对于store中indexList中的每一个Index,都将Row创建的相应的NodeAVL挂在该Index对应的二叉树上:indexAVL.insert(session, store, row)
  • 然后将前面创建的row.rowAction添加到session.rowActionList中,供后续提交确认或回滚使用,并将row.rowAction置为null。

至此,一条数据插入到表中的逻辑就执行完毕了。如果在插入过程中有任何异常,例如约束冲突等等,则结合事务管理器执行其rollback流程。

 

ps:在store中存有indexList和accessorList两个列表。其中indexList保存了本表建立的索引,accessorList是一个与indexList一一对应的NodeAVL的数组,代表了按照各个Index规定的顺序建立的本表所有行作为节点的一个平衡二叉树的根节点。

 

在插入数据时,会对indexList中的每一个Index,从store中得到其对应的accessorList中的二叉树跟节点,若根节点为null则说明目前表中还没有数据。此时通过:

        store.setAccessor(index, accessor(node)) ---> accessorList[index.getPosition()]=accessor

来为其设置根节点。

若根节点不为null,则通过Index指定的字段类型及是否倒序来查找这个二叉树,直到找到合适的位置,并将对应的NodeAVL插入到这棵二叉树中。

 

 

 

6.6 delete

 

1. 客户端执行动作同上,只是sql语句变为了

DELETE FROM MYTABLE WHERE B = 'woshiwangdong'

这样的,当然也可以是类似:

DELETE FROM MYTABLE WHERE O IN (SELECT I FROM T WHERE T.B LIKE '%wd*%')

 

2. 服务端收到一个ResultConstants.EXECDIRECT类型的请求,将其对应的sql语句解析并执行。对于"delete from <tablename> where ..."语句,会解析为一个StatementDML类型的对象cs,并执行其cs.execute(session)。
3. 其中先处理子查询(如果有的话)

 

	if (subqueries.length > 0) {
                materializeSubQueries(session);
        }

如上述第二个示例语句中所示,删除的数据也可以是通过查询获得的,而查询中也可能包括嵌套查询。所以也可能涉及到子查询,关于子查询的执行涉及到的数据结构及执行的详细步骤请参见6.4 Query的相关描述。

4. 然后执行StatementDML.getResult(session)函数,其中会根据本语句的类型执行相应的逻辑,由于本语句是delete from因此,解析后的类型是StatementTypes.DELETE_WHERE=19,所以执行的逻辑是:

 

case StatementTypes.DELETE_WHERE :
                if (isTruncate) {
                    result = executeDeleteTruncateStatement(session);
                } else {
                    result = executeDeleteStatement(session, limit);
                }
                break;

此处先不讨论truncate的情况。最终执行的方法就是:

 

Result executeDeleteStatement(Session session, int limit)

在该方法中,首先遍历代表了要删除数据集的targetRangeVariables,将要删除数据添加到结果集中。targetRangeVariables数组代表了FROM从句后面的部分,其中每一个rangeVariable代表了一个带条件的数据源,既包括where条件也包括join条件。并在next方法中通过一定的算法找到各数据源中同时满足所有条件的一组值,作为一条符合条件的结果。详细的查询过程请参照6.4.1小节。

然后如果要删除数据的结果集不为空,则执行:

count = delete(session, baseTable, rowset);

该方法是删除语句执行的核心方法,实际执行数据集的删除。其业务逻辑比较复杂,将在下面详细讲解。
最终返回包含删除条数的结果:

 

return new Result(ResultConstants.UPDATECOUNT, count)

6.6.1 StatementDML.delete过程详解

在上述执行过程中,如果查询到的要删除数据集不为空,则会执行StatementDML的delete函数,该函数签名如下:

 

int delete(Session session, Table table,
               RowSetNavigatorDataChange navigator)

其执行过程如下所示:

 

  • 首先记录要删除数据的条数到rowCount中
  • 如果存在设置了ON DELETE CASCADE属性的MAIN型约束,则递归查找对于navigator中每一条数据,需要级联删除的其他关联表中的数据,添加到结果集navigator中。
  • 对navigator中的每一条数据(包含了上一步添加的需要级联删除的数据),递归找到由于删除/更新它们需要执行级联更新的数据并添加到结果集navigator中。
  • 对于结果集navigator中每一条要删除/更新的数据(根据其changedData是否为null来判断),执行DELETE_BEFORE_ROW或UPDATE_BEFORE_ROW型的trigger
  • 对于结果集navigator中的每一条数据,执行删除并记undo日志(更新也是需要先删除再插入)
  • 对于结果集navigator中需要更新的数据(通过changedData不为null来判断),将更新数据插入表中
  • 如果存在级联更新的情况,对于关联表来说,需要先删除数据再插入数据,插入后未提交之前需要进行约束校验,主要校验CHECK约束及外建约束是否冲突。
  • 对于级联更新/删除的每一条数据,在其对应的表上触发针对本条数据的DELETE_AFTER_ROW/UPDATE_AFTER_ROW型trigger。
  • 执行最初删除数据的表targetTable的DELETE_AFTER型trigger。
  • 执行级联更新/删除数据所在表的UPDATE_AFTER/DELETE_AFTER型trigger。
  • 最终返回原本要删除数据的条数rowCount。

其中,在递归获得级联删除数据/递归获得级联更新数据时调用的方法均为:

static void performReferentialActions(Session session,
                                          RowSetNavigatorDataChange navigator,
                                          Row row, Object[] data,
                                          int[] changedCols, HashSet path,
                                          boolean deleteCascade) 

当其最后一个参数deleteCascade为true时,本方法就只处理删除过程中那些on delete cascade外建约束的情况,递归找出关联表中需要关联删除的数据,将其添加到需要删除数据集navigator中。

当其最后一个参数deleteCascade为false时,本方法就用于处理更新时的外建约束或是删除时非on delete cascade外建约束的情况,递归找到删除或修改本条数据时通过各外键约束关联到本数据的那些数据,并根据外建约束的定义(如on delete update,on delete default, on delete null等)设置需要更新的数据的取值,并将这些行对象以及更新后的值添加到更新结果集中。

6.7 update

 

 

1. 客户端执行动作同上,只是sql语句变为了

UPDATE MYTABLE SET A='bibuzhidaoba2' WHERE B='woshiwangdong'

这样的,当然也可以是类似:

UPDATE MYTABLE SET A='newdata' WHERE O IN (SELECT I FROM T WHERE T.B LIKE '%wd*%')

 

2. 服务端收到一个ResultConstants.EXECDIRECT类型的请求,将其对应的sql语句解析并执行。对于"update <tablename> set xxx where ..."语句,会解析为一个StatementDML类型的对象cs,并执行其cs.execute(session)。
3. 其中先处理子查询(如果有的话)

 

	if (subqueries.length > 0) {
                materializeSubQueries(session);
        }

如上述第二个示例语句中所示,删除的数据也可以是通过查询获得的,而查询中也可能包括嵌套查询。所以也可能涉及到子查询,关于子查询的执行涉及到的数据结构及执行的详细步骤请参见6.4 Query的相关描述。

4. 然后执行StatementDML.getResult(session)函数,其中会根据本语句的类型执行相应的逻辑,由于本语句是update where因此,解析后的类型是StatementTypes.UPDATE_WHERE=92,所以执行的逻辑是:

case StatementTypes.UPDATE_WHERE :
                result = executeUpdateStatement(session, limit);
                break;

最终执行的方法就是:

Result executeUpdateStatement(Session session, int limit)

与Delete语句类似,在该方法中,首先遍历代表了要删除数据集的targetRangeVariables,找到符合条件的待更新数据,并针对每一条待更新数据通过更新语句计算出本条数据的新值:

Object[] newData = getUpdatedData(session, targets, baseTable,
                                              updateColumnMap,
                                              updateExpressions, colTypes,
                                              data);

将通过updateExpressions计算得到的newData添加到结果集rowset中。targetRangeVariables数组代表了FROM从句后面的部分,其中每一个rangeVariable代表了一个带条件的数据源,既包括where条件也包括join条件。并在next方法中通过一定的算法找到各数据源中同时满足所有条件的一组值,作为一条符合条件的结果。详细的查询过程请参照6.4.1小节。

接着执行:

 

count = update(session, baseTable, rowset, generatedNavigator);

该方法是更新语句执行的核心方法,实际执行数据集rowset包含数据的更新,其业务逻辑比较复杂,将在下面详细讲解。

最终返回包含更新条数的结果:

return new Result(ResultConstants.UPDATECOUNT, count)

6.7.1 StatementDML.update过程详解

如上所述,在查询并计算好要更新数据的集合以后,会执行StatementDML的update函数,该函数签名如下:

int update(Session session, Table table,
               RowSetNavigatorDataChange navigator,
               RowSetNavigator generatedNavigator)

其执行过程如下所示:

 

  • 首先记录要删除数据的条数到rowCount中
  • 然后对于更新数据结果集rowset中的每一条数据,如有必要,设置其Identity字段以及Generated字段的值。
  • 对rowset中的每一条数据,递归找到由于更新它需要执行级联更新的数据并添加到结果集rowset中。
  • 对于rowset中的每一条数据(包括上一步添加的级联更新数据),执行UPDATE_BEFORE_ROW型的trigger。并在执行完毕后进行基于表定义所有约束的校验。
  • 对于rowset结果集中的每一条数据,执行删除并记undo日志
  • 对于rowset结果集中的每一条数据,执行插入并记undo日志(更新动作是由删除原来数据并插入新数据完成的)
  • 针对每一条新插入的数据,未提交之前需要进行约束校验,主要校验CHECK约束及外建约束是否冲突。
  • 对于rowset结果集中的每一条数据,在其对应表上触发针对本条数据的UPDATE_AFTER_ROW型trigger。
  • 执行最初更新数据的表targetTable的UPDATE_AFTER型trigger。
  • 执行级联更新数据所在的表的UPDATE_AFTER型trigger。
  • 最终返回原本要更新数据的条数rowCount

 

其中,在递归获得级联更新数据时调用的方法为:

[java]  view plain  copy
 
  1. static void performReferentialActions(Session session,  
  2.                                           RowSetNavigatorDataChange navigator,  
  3.                                           Row row, Object[] data,  
  4.                                           int[] changedCols, HashSet path,  
  5.                                           boolean deleteCascade)   

当其最后一个参数deleteCascade为true时,本方法就只处理删除过程中那些on delete cascade外建约束的情况,递归找出关联表中需要关联删除的情况。将其添加到需要删除数据集navigator中。

当其最后一个参数deleteCascade为false时,本方法就用于处理更新时的外建约束或是删除时非on delete cascade外建约束的情况,递归找到删除或修改本条数据时通过各外键约束关联到本数据的那些数据,并根据外建约束的定义(如on delete update,on delete default, on delete null等)设置需要更新的数据的取值,并将这些行对象以及更新后的值添加到更新结果集中。

 

 

 

 

相关文章:HyperSQL调研学习文档(一)

相关文章:HyperSQL调研学习文档(二)

相关文章:HyperSQL调研学习文档(三)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值