数据库类连接
SmartEDB 提供了多种实现表之间关系连接的方法(可参阅类关系页),每种方法在内存开销和性能方面都有其独特的优势和成本。以下各节介绍了可供 C 开发人员使用的不同技术。
oid 引用
在“卫星收音机接收器” 示例中,采用对象标识符(oid)来关联两个类。当数据模型包含一组能够唯一标识主要对象的合理数量的数据时,这种方法可以达到最佳效果。
在此情境下,oid被定义为包含单个4字节整数的结构体ProgId。然而,唯一标识一个类的实际数据集可能相当复杂,包含多个不同类型的字段。SmartEDB运行时对oid 的内部结构没有内置知识。oid 以字节数组的形式表示,并通过该数组上的哈希索引进行访问。引用ProgramData类(例如TimeSlot.pId
)中的oid 的ref
字段与oid 具有相同的结构,并在基于oid 的查找操作中用作键。
需要注意的是,oid/ref关系将TimeSlot 对象链接到特定的ProgramData对象,但不包括任何级联操作或引用完整性验证。
实际上,这种“oid连接”是通过SmartEDB运行时根据oid/ref声明创建的内部哈希索引实现的。连接也可以显式地实现而不使用oid。以下将介绍三种此类技术。
索引连接
在 SQL 中,两个表Department(部门)和Employee(员工)之间简单的“一对多”关系可以这样实现:
select e.name, d.name from Employee e
inner join Department d d.dept_no = d.dept_no;
要实现类似“部门 > 员工”这样的多对一关系,我们可能会像下面这样定义一个数据库模式:
#define uint2 unsigned<2>
declare database index_join_db;
class Department
{
string name;
string code;
uint2 dept_no;
unique tree<name> Iname;
unique tree<code> Icode;
unique hash<dept_no> Idept_no[1000];
};
class Employee
{
string name;
uint2 dept_no;
unique tree<name> Iname;
unique tree<dept_no,name> Idept_name;
};
在此,唯一的哈希索引 Idept_no
在 Department 类中充当“主键”,而唯一的 B 树索引 Idept_name
在 Employee 类中充当“外键”。
需注意,Idept_name
是由 Employee 类中的两个字段值(dept_no
和 name
)构成的复合索引。“外键”未必一定是复合的。仅在 dept_no
字段上创建一个简单的 B 树索引就足以形成连接关系,但使用复合索引,我们还具备一个额外的优势,即能够依据 name
字段,按照字母顺序对具有相同 dept_no
值的所有实例进行排序。
为演示连接的实现方式,考虑以下代码片段(可参阅SDK示例 05_indexes/joins/index_join),其用于管理 Department(部门)和 Employee(员工)对象。首先,我们使用一些示例数据填充 Department 类:
// 与类“Department”相对应的结构体
struct department_data
{
char name[20];
char code[10];
uint2 dept_no;
};
struct department_data departments[] =
{
{ "Accounting", "Acct", 101 },
{ "Engineering", "Eng", 102 },
{ "Customer Service", "CS", 103 },
{ "Technical Support", "TS", 104 }
};
...
printf("\nCreate departments:\n");
for (i = 0; i < sizeof(departments) / sizeof(departments[0]) && MCO_S_OK == rc; ++i)
{
rc = mco_trans_start(db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &t);
if (MCO_S_OK == rc)
{
Department dept;
Department_new(t, &dept);
Department_name_put(&dept, departments[i].name, strlen(departments[i].name));
Department_code_put(&dept, departments[i].code, strlen(departments[i].code));
Department_dept_no_put(&dept, departments[i].dept_no);
printf("\n\t%d) %s, %s, dept_no=%d", i,
departments[i].code, departments[i].name,
departments[i].dept_no );
rc = mco_trans_commit(t);
}
}
然后我们插入 Employee 对象,并将它们与相应的 Department 对象关联起来:
// 定义与 Employee 类对应的结构体
struct employee_data
{
char name[20];
uint2 dept_no;
};
// 定义员工与部门之间的关系
struct employee_department
{
char name[20];
char dept_code[10];
};
struct employee_department employee_departments[] =
{
{ "John", "Acct" }, { "Samuel", "Acct" }, { "Thomas", "Acct" },
{ "David", "Eng" }, { "James", "Eng" }, { "Robert", "Eng" },
{ "William", "CS" }, { "Kevin", "CS" }, { "Alex", "CS" },
{ "Daniel", "CS" }, { "Diego", "CS" }, { "Brandon", "TS" }
};
...
printf("\n\nCreate employees and join each to a department:\n");
for (i = 0; i < sizeof(employee_departments) / sizeof(employee_departments[0])
&& MCO_S_OK == rc; ++i)
{
rc = mco_trans_start(db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &t);
if (MCO_S_OK == rc)
{
// 通过代码查找部门;提取部门编号;创建员工并分配姓名、部门编号
Department dept;
rc = Department_Icode_find(t, employee_departments[i].dept_code,
strlen(employee_departments[i].dept_code), &dept);
if (MCO_S_OK == rc)
{
uint2 dept_no = 0;
rc = Department_dept_no_get(&dept, &dept_no);
if (MCO_S_OK == rc)
{
Employee emp;
Employee_new(t, &emp);
Employee_name_put(&emp,
employee_departments[i].name, strlen(employee_departments[i].name));
Employee_dept_no_put(&emp, dept_no);
printf("\n\t%d) %s, dept_no=%d", i, employee_departments[i].name, dept_no);
rc = mco_trans_commit(t);
}
...
}
...
}
...
}
请注意,我们使用 Department_Icode
索引来查找具有指定代码的 Department 对象;然后提取 Department 的 dept_no
并将其存储在新的 Employee 对象中。这就在 Department 对象和 Employee 对象之间创建了关联(关系),在后续的查询中会基于此执行连接操作。现在,要导航(即“连接”)这两个类之间的关系,我们可能会使用如下代码来显示给定员工所在部门的所有同事:
// 1. 通过姓名查找“员工”对象并提取部门编号
rc = Employee_Iname_find(t, search_name, (uint2)strlen(search_name), &emp1);
if (MCO_S_OK == rc)
{
printf("\n\n%s's co-workers in ", search_name);
Employee_dept_no_get(&emp1, &dept_no1);
Employee_Idept_name_index_cursor(t, &csr);
// 2. 根据Department对象的dept_no查找Department对象并显示Department名称
rc = Department_Idept_no_find(t, dept_no1, &dept1);
if (MCO_S_OK == rc)
{
Department_name_get(&dept1, name, sizeof(name), &len);
printf("%s are:\n", name);
// 3. 将游标定位在Idept_name索引中,指向具有此dept_no的第一个对象
rc = Employee_Idept_name_search(t, &csr, MCO_GE, dept_no1, "", 0);
// Scroll forward through the cursor and display the names found...
while (MCO_S_OK == rc)
{
// 检查当前的Employee是否与找到的Employee相同
Employee_from_cursor(t, &csr, &emp2);
Employee_name_get(&emp2, name, sizeof(name), &len);
// 如果两个名称不相等,则显示名称(如果dept_no仍然相同)
if (0 != strcmp(name, search_name))
{
// 确认部门编号(dept_no)是否仍然相同,否则退出。
Employee_dept_no_get(&emp2, &dept_no2);
if (dept_no1 != dept_no2) break;
printf("\n\t%s", name);
}
rc = mco_cursor_next(t, &csr);
}
}
}
请注意,我们使用 Employee_Iname
索引来查找具有指定名称的 Employee 对象;然后提取该 Employee 的 dept_no
,在索引 Employee_Idept_name
上执行搜索,这会将游标定位到具有此 dept_no
的索引树中的第一个节点。然后,我们通过游标滚动,直到 dept_no
不同。对于每个索引节点,我们从游标获取 Employee 对象并提取其名称字段。如果这不是我们正在为其搜索同事的原始员工的名称,我们将显示该名称并调用 mco_cursor_next()
转到索引树中的下一个节点。
优点和缺点
这是许多开发人员熟悉的典型“关系型”风格的连接。虽然实现起来直观且直接,但管理多个索引会产生显著的开销。当提交插入事务时,每个 Department 对象会创建一个哈希索引和两个 B 树索引节点,每个 Employee 对象会创建两个 B 树索引节点。这除了数据库对象本身占用的空间外,还导致了内存消耗,并且 B 树索引结构需要“平衡”,这可能会消耗大量的处理器周期。因此,插入对象的整体性能不是最优的。
由于哈希索引 Idept_no
,通过 dept_no
查找 Department 对象是高效的。并且通过复合索引 Employee_Idept_name
在 Employee 对象上创建的游标有助于“滚动”通过索引树,按名称的字母顺序列出员工。
请注意,要显示名称字段,需要额外的函数调用 Employee_from_cursor()
来访问数据库对象。
autoid引用
上述索引连接示例的替代技术是通过从 Employee 对象到其 Department 的直接引用来实现“外键”关系。autoid
字段可以用于此目的。例如,我们可以定义如下的数据库模式:
#定义 uint2 unsigned<2>
declare database autoid_ref_db;
class Department
{
autoid[1000];
string name;
string code;
unique tree<name> Iname;
unique tree<code> Icode;
};
class Employee
{
string name;
autoid_t dept;
unique tree<name> Iname;
unique tree<dept,name> Idept_name;
};
请注意,Department 类不再具有 dept_no
字段,因此也没有索引 Idept_no
。相反,它有声明 autoid[1000]
; 。这实际上会导致为 Department 对象的自动递增的 autoid
值维护一个“隐藏”的哈希索引,因为新对象被创建。并且请注意 Employee 类中的字段 dept
,它将存储相关 Department 对象的 autoid
。
请注意,当使用 SQL 接口访问被引用的表时,autoid_t
字段声明有一种替代形式是有用的。例如,可以这样定义 Employee 类:
class Employee
{
string name;
autoid_t<Department> dept;
unique tree<name> Iname;
unique tree<dept,name> Idept_name;
};
此语法明确了由字段值所引用的类。SmartEDB 运行时不会对该声明进行验证,也不会以任何形式加以使用(实际上,其可为任何整数值)。然而,SmartESQL 运行时“信赖”此声明,并借此在 SQL 中实现引用。同样,不存在任何级联规定或任何类型的引用完整性检查。所以,例如,倘若尝试删除一个被一个或多个员工对象所引用的部门对象,就必须谨慎处理。
要演示如何实现这个连接,请考虑以下代码片段(可参阅SDK示例 05_indexes/joins/autoid_ref )。我们像上面的index_join
样例那样填充Department类,但请注意,当插入Employee对象时,我们从相应的Department对象中提取autoid
:
printf("\nCreate employees and join each to a department:\n");
for (i = 0; i < sizeof(employee_departments) / sizeof(employee_departments[0])
&& MCO_S_OK == rc; ++i)
{
rc = mco_trans_start(db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &t);
if (MCO_S_OK == rc)
{
// 通过代码查找部门;提取自动编号;创建员工并为其分配姓名、部门(自动编号)
Department dept;
rc = Department_Icode_find(t, employee_departments[i].code,
strlen(employee_departments[i].code), &dept);
if (MCO_S_OK == rc)
{
autoid_t dept_id = 0;
rc = Department_autoid_get(&dept, &dept_id);
if (MCO_S_OK == rc)
{
Employee emp;
Employee_new(t, &emp);
Employee_name_put(&emp, employee_departments[i].name,
strlen(employee_departments[i].name));
Employee_dept_put(&emp, dept_id);
printf("\n\t%d) %s, Department.Autoid=%ld", i,
employee_departments[i].name, (long)dept_id);
rc = mco_trans_commit(t);
}
...
}
}
}
显示给定员工所在部门中所有同事的代码与上述 index_join
示例中的代码基本相同,唯一的区别在于我们调用 Department_autoid_find()
函数通过自动生成的 autoid
查找 Department 对象。这类似于调用 Department_Idept_no_find()
函数,后者使用哈希索引 Idept_no
来查找 Department 对象,但不同之处在于,autoid
是由系统自动生成的,而 dept_no 字段需要手动指定唯一值。为了按字母顺序显示员工姓名,仍然采用在复合索引 Employee_Idept_name
上遍历游标的技术。
// 1. 按姓名查找员工记录
rc = Employee_Iname_find(t, search_name, (uint2)strlen(search_name), &emp);
if (MCO_S_OK == rc)
{
printf("\n\n%s's co-workers in ", search_name);
Employee_dept_get(&emp, &dept_id1);
// 2. 通过其autoid查找部门对象并显示部门名称
rc = Department_autoid_find(t, dept_id1, &dept1);
if (MCO_S_OK == rc)
{
Department_name_get(&dept1, name, sizeof(name), &len);
printf("%s are:\n", name);
// 3. 将游标定位在Idept_name索引中,指向具有此autoid的第一个对象
Employee_Idept_name_index_cursor(t, &csr);
rc = Employee_Idept_name_search(t, &csr, MCO_GE, dept_id1, "", 0);
if (MCO_S_OK == rc)
{
while (MCO_S_OK == rc)
{
// 检查当前员工是否与找到的员工相同
Employee_from_cursor(t, &csr, &emp2);
Employee_name_get(&emp2, name, sizeof(name), &len);
// 如果两个名称不相等,则显示名称(如果dept_id仍然相同)
if (0 != strcmp(name, search_name))
{
// 确认部门编号(dept_id)是否仍然相同,否则退出循环。
Employee_dept_get(&emp2, &dept_id2);
if (dept_id1 != dept_id2)
break;
printf("\n\t%s", name);
}
rc = mco_cursor_next(t, &csr);
}
}
}
}
优点和缺点
通过移除字段 dept_no
,该实现减少了内存占用,并且由于为 autoid
生成了哈希索引,Department 对象的查找同样高效。然而,通过适当放宽应用程序的需求,可以进一步优化性能。
需要注意的是,无论是这种关系方法还是 index_join
关系方法,性能和内存消耗的主要瓶颈都来自于 B-Tree 索引:Employee_Idept_name
、Employee_Iname
和 Department_Iname
。例如,如果不需要按名称查找 Department 对象,则可以删除 Department_Iname
索引;此外,如果不需要按字母顺序显示给定部门的员工,那么可以取消复合索引 Employee_Idept_name
。以下示例展示了这一优化策略。
autoid向量
我们可以不在 Employee 对象中存储其所属 Department 的 autoid
,而是在 Department 对象中存储一个 Employee autoid
的数组或向量。例如(请参阅 SDK 示例 05_indexes/joins/autoid_vector ),我们可能会如下定义数据库模式:
#define uint2 unsigned<2>
declare database autoid_vector_db;
class Department
{
autoid[1000];
string name;
string code;
vector<autoid_t> employees;
unique hash<code> Icode[1000];
};
class Employee
{
autoid[1000];
string name;
autoid_t dept;
unique tree<name> Iname;
};
请注意,Department 和 Employee 类均使用 autoid
进行定义。Employee 的autoid
将存储在 Department 类的 employees
向量中。我们将在 Employee 类的 dept
字段中使用 Department 的 autoid
,如同上述 autoid_ref
示例所示,以便从 Employee 对象高效访问 Department 对象。
填充数据库的代码变得较为复杂,因为我们需要为 Department 对象中的autoid
动态向量分配空间,并在插入 Employee 对象时分配这些autoid
。因此,对于每个 Employee 对象,我们将执行以下代码来分配空间并将 Employee autoid
插入到 Department 的 employees
向量中:
// 获取这个Department的autoid,然后创建新的Employee对象,并将其autoid插入到向量中
Department_autoid_get(&dept, &dept_id);
Employee_new(t, &emp);
Employee_name_put(&emp, employee_departments[i].name,
strlen(employee_departments[i].name));
Employee_dept_put(&emp, dept_id);
Employee_autoid_get(&emp, &emp_id);
// 为新的向量元素分配空间,并插入Employee对象的autoid
Department_employees_size(&dept, &vector_size);
Department_employees_alloc(&dept, vector_size + 1);
Department_employees_put(&dept, vector_size, emp_id);
现在,要显示给定员工所在部门的所有同事的代码,只需像上面的 autoid_ref
示例中那样,通过调用 Department_autoid_find()
函数来查找 Department 对象,然后从向量中列出该部门的员工,具体如下:
Department_name_get(&dept1, name, sizeof(name), &len);
printf("%s are:\n", name);
// 滚动Employee autoid向量,找到Employeeobject并显示其名称
Department_employees_size(&dept1, &vector_size);
for (short j = 0; j < vector_size; j++)
{
rc = Department_employees_at(&dept1, j, &emp_id2);
// 当vector元素的值为0时结束循环
if (0 == emp_id2)
break;
// 如果这是“search_name”对象的autoid,则跳过
if (emp_id1 != emp_id2)
{
// 通过autoid查找Employee对象并显示其名称
rc = Employee_autoid_find(t, emp_id2, &emp2);
if (MCO_S_OK == rc)
{
Employee_name_get(&emp2, name, sizeof(name), &len);
printf("\t%s\n", name);
}
}
}
优点和缺点
通过消除两个B-Tree索引,该实现显著减少了内存使用,并降低了后台树结构的维护开销。然而,这一优化是以不再按字母顺序对雇员列表进行排序为代价的。尽管如此,在应用程序中引入排序算法可以轻松解决这一问题;但需要注意的是,如果每个部门的雇员数量非常庞大,这种方法可能会变得不可行。