动态表名
当数据量比较大的时候,为了提高数据库操作的效率,尤其是查询的效率,其中一种解决方案就是将数据表拆分。
拆分的数据表,结构完全一致,只不过是表的名字,按照某种规律,而成为一组。
动态表名的常用形式
通常情况下动态表名都是通过一个后缀来表示的。比如我们要记录全中国所有的公司以及其雇员,通常的设计是建立
两张数据表, t_company 记录公司,t_employee 记录雇员。由于考虑到 t_employee 的数量可能太过庞大,我们可
以将 t_employee 进行拆分,为每个公司建立一张雇员表。 比如 t_employee_1 记录 ID 为 1 的公司所有雇员,
t_employee_19 记录 ID 为 19 的公司所有雇员。
当然,我们也不能排除动态表名的其他形式,比如,如果公司也是动态表名: t_company_3 表示在 ID 为 3 的国家
的公司。那么雇员表有可能被设计成 t_employee_3_10, 在 ID 为 3 的国家且 ID 为 10 的公司所有的雇员记录
另外,表名的中的变量可能不只是数字,也可能不只是后缀。考虑到这样的情况,同时也希望不增加 org.nutz.dao.Dao
接口的复杂程度, Nutz.Dao 将怎样为这样的数据库操作方法提供支持的呢?
Nutz对于动态表名的支持
在 POJO 中声明动态表名
毫无疑问,首先,需要配置你的 POJO。 Nutz.Dao 提供的 @Table 注解本身就支持动态表名,比如:
@Table("t_employee_${cid}")
public class Employee{
// The class fields and methods...
}
@Table 注解支持字符串模板的写法,在你希望放置动态表名变量的位置插入 ${变量名}
,如 ${cid}
,那么${cid}
会在运行时被 Nutz.Dao 替换。
如何替换?用什么替换?请看下面一节。
在调用时应用动态表名
Nutz.Dao 提供了一个小巧的类: org.nutz.dao.TableName。 通过这个类你可以随意设置你的动态表名:
public void demoTableName(final Dao dao) {
TableName.run(3, new Runnable() {
public void run() {
Employee peter = dao.fetch(Employee.class, "peter");
System.out.println(peter);
}
});
}
通过创建一个匿名 java.lang.Runnable 对象,你可以像静态 POJO 一样使用 Dao 接口的一切方法。因为通过你
传入的参数 3 (TableName.run 方法的第一个参数), 以及前面的 @Table 声明,Nutz.Dao 已经很清楚如何操作
数据库了,它会用 3 替代 $cid。
如果细心一些,你会发现在 TableName.run 方法的声明是:
public static void run(Object refer, Runnable atom);
是的,第一个参数是个 Object,也就是说,你可以传入任何对象,上面的例子我们传入的是个整数,Java编译器
会自动将其包裹成 Integer 对象。
考虑到动态表名可能存在的复杂性(在前面一节“动态表名的常用形式”提到),你还可以传入一个 Map 或者一个
POJO, Nutz.Dao 会根据你的传入为动态表名的多个变量同时赋值。下面我列出一个完整的动态表名赋值规则
动态表名赋值规则
- 当传入参数为数字或字符串
- 所有的动态表名变量将被其替换
- 当传入参数为 Map
- 按照动态表名变量的名称在 Map 中查找值,并进行替换
- 大小写敏感
- 未找到的变量将被空串替换
- 当传入参数为 任意Java对象(POJO)
- 按照动态表名变量名称在对象中查找对应字段的值,并进行替换
- 大小写敏感
- 未找到的变量将被空串替换
- 当传入参数为null
- 所有变量将被空串替换
更灵活的应用动态表名
使用 TableName.run 提供的动态表名模板的方式设置动态表名是很好的做法,也很安全。因为不会在 ThreadLocal
里面留下垃圾动态表名变量值。但是通过模板的写法另外一方面也限制了一定线程灵活性。所以上述例子还有另外
一个写法:
public void demoTableName2(final Dao dao) {
TableName.set(3);
Employee peter = dao.fetch(Employee.class, "peter");
System.out.println(peter);
TableName.clear();
}
在 TableName.set 和 TableName.clear 之间的代码,就是动态表名变量的生命周期。当然,这个写法存在两个潜在
的危险。
- 可能在 ThreadLocal 留下垃圾动态表名变量
- 会清除掉 ThreadLocal 所有的动态表名变量
关于第一点,可以用 try...catch...finally 来解决:
public void demoTableName3(final Dao dao) {
try {
TableName.set(3);
Employee peter = dao.fetch(Employee.class, "peter");
System.out.println(peter);
} finally {
TableName.clear();
}
}
这样,永远都不会留下垃圾动态表名了
关于第二点,是的,如果在 TableName.set(3) 之前你曾经设置了另一个动态表名变量,比如
public void demoTableName_multi(final Dao dao) {
TableName.set(10);
Employee john = dao.fetch(Employee.class, "john");
System.out.println(john);
TableName.set(3);
Employee peter = dao.fetch(Employee.class, "peter");
System.out.println(peter);
TableName.clear();
Employee mary = dao.fetch(Employee.class, "Mary");
System.out.println(mary);
TableName.clear();
}
当执行到 Employee mary = dao.fetch(Employee.class, "Mary"); 一定会出错,不是吗? 所以比较安全的写法是
public void demoTableName_multi(final Dao dao) {
TableName.set(10);
Employee john = dao.fetch(Employee.class, "john");
System.out.println(john);
Object old = TableName.get();
try {
TableName.set(3);
Employee peter = dao.fetch(Employee.class, "peter");
System.out.println(peter);
} finally {
TableName.set(old);
}
Employee mary = dao.fetch(Employee.class, "Mary");
System.out.println(mary);
TableName.clear();
}
比较麻烦是吗? 是的,使用 TableName.get ... TableName.set ... TableName.clear 虽然带来更大的灵活性,
但是写起来有点麻烦,这也是为什么我要提供模板写法,上面的例子用模板的写法看起来是这个样子的:
public void demoTableName_multi_temp(final Dao dao) {
TableName.run(10, new Runnable() {
public void run() {
Employee john = dao.fetch(Employee.class, "john");
System.out.println(john);
TableName.run(3, new Runnable() {
public void run() {
Employee peter = dao.fetch(Employee.class, "peter");
System.out.println(peter);
}
});
Employee mary = dao.fetch(Employee.class, "Mary");
System.out.println(mary);
}
});
}
看,虽然行数并没有减少,但是你不会犯错了。是的,模板写法主要的目的是 为了让你不会出错。
Java 语法上的局限让上述写法不可避免的有点显得臃肿,但是,层次的确清晰了一些,不是吗?在这个方面,我也
期待这 Java 能向函数式编程靠近一些,提供闭包或者匿名函数,当然,前提是不能损害现在 Java 语言结构清晰
易于调试的优点。
在映射中的动态表名
通过Java注解 @One, @Many, @ManyMany,Nutz.Dao 支持对象间的映射。很自然的,对象间的映射自动的会支持动态
表名。比如,如果 Company 对象有个成员变量 private List<Employee> employees;
由于 Employee 是动态表名的
所以当获取 employee 的时候,同样也能支持动态表名。
一对一映射
比如,我们的 Company 对象需要一个新的字段存储 CEO 的 ID,以及另外一个字段存储 CEO 对象本身。按照
Nutz.Dao 对于一对一映射的定义:
当对象A有字段f1指向对象B的主键,且在对象A上有字段b类型为B,且声明了@One(target=B.class,field="f1",则称A.b为对于B的一对一映射
那么,我们的 Company 对象必然会有类似如下的代码:
@Table("t_company")
public class Company {
@Id
private int id;
@Name
private String name;
@Column
private int ceoId;
@One(target = Employee.class, field = "ceoId")
private Employee CEO;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCeoId() {
return ceoId;
}
public void setCeoId(int ceoId) {
this.ceoId = ceoId;
}
public Employee getCEO() {
return CEO;
}
public void setCEO(Employee ceo) {
CEO = ceo;
}
}
比如,我们要在控制台上打印"nutz" 的公司的 CEO 的名字时,代码将为:
public void demoTableName_links_one(final Dao dao) {
final Company nutz = dao.fetch(Company.class,"nutz");
TableName.run(nutz.getId(), new Runnable(){
public void run() {
dao.fetchLinks(nutz, "CEO");
}
});
System.out.println(nutz.getCEO().getName());
}
一对多映射
下面我们来增加一对多映射,是的,一个公司有很多雇员,不是吗?那么我们就为 Company 类增加一个新的字段
@Table("t_company")
public class Company {
// ... another fields ...
@Many(target = Employee.class, field = "companyId")
private List<Employee> employees;
public List<Employee> getEmployees() {
return employees;
}
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
// ... another methods ...
}
可以看到,在字段 employees 增加了 @Many 说明,就像一般的一对多映射一样, field 项声明了 Employee 类
必须有一个名叫(companyId)的字段指向 Company 的主键。所以 Employee 类的代码为:
@Table("t_employee_${cid}")
public class Employee {
@Id
private int id;
@Name
private String name;
@Column("comid")
private int companyId;
public int getCompanyId() {
return companyId;
}
public void setCompanyId(int companyId) {
this.companyId = companyId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
一切设置完毕,我们就可以这么调用:
public void demoTableName_links_many(final Dao dao) {
final Company nutz = dao.fetch(Company.class, "nutz");
TableName.run(nutz.getId(), new Runnable() {
public void run() {
dao.fetchLinks(nutz, "employess");
}
});
for (Employee e : nutz.getEmployees())
System.out.println(e.getName());
}
上面的程序会逐行打印出 nutz 公司所有的雇员名称。
多对多映射
多对多映射是通过一个中间表进行的数据关联,比如数据库中有数据表 A,B, 可以在建立一张表 C,描
述 A 表和 B 表的数据关联。一般的关联表至少有两个字段,一个是 A 表的主键,另一个记录 B 表的主
键。如果是复合主键或者要记录关联的权重,关联表的设计将会更加复杂。
Nutz.Dao 提供了 @ManyMany 注解帮助你的 POJO 来声明多对多关联,比如在 Employee 类中可以增加一个新
的字段,表示某个雇员的下属。
@ManyMany(target = Employee.class, relation = "t_employee_staff_${cid}", from = "eid", to = "sid")
private List<Employee> staffs;
请注意
- relation 项就是关联表的名称,这个名称也可以写成动态的。
- from 项是关联表字段的名称,将对应到本 POJO 的主键,这里的 eid 将对应 Employee.id
- to 项也是关联表字段的名称,将对应到 target 项声明的主键,这里的 sid 也将对应 Employee.id
在调用代码里可以这样调用
public void demoTableName_links_manymany(final Dao dao) {
final Company nutz = dao.fetch(Company.class, "nutz");
TableName.run(nutz.getId(), new Runnable() {
public void run() {
Employee peter = dao.fetch(Employee.class,"Peter");
dao.fetchLinks(peter, "staffs");
for (Employee e : peter.getStaffs())
System.out.println(e.getName());
}
});
}
这段代码将 nutz 公司雇员 Peter 的所有下属逐行打印出来。
无需匿名内部类的写法
- 请参考过滤字段的描述
总结一下
关于动态表名的这一节写的比较长,因为我认为动态表名的支持,Nutz.Dao 是比较独特的。它基本做到了这两个效果
- 如果你不希望使用动态表名,你根本不会看到 Nutz.Dao 关于动态表名的设计
- 如果你希望使用动态表名,你并不需要学习更多的配置方法
并不是因为我是 Nutz 的作者而努力的在为自己吹嘘,如果你细心体会 Nutz 各个模块的设计,所有的设计基本是本着
上述两个原则,即需要的时候你会看见,不需要的时候尽量让你看不见。
由于我是个职业界面设计师,所以我会自然而然的将我设计 UE 时很多原则应用在编程接口的设计上,事实上我发现,
这的确在某种程度上让程序接口更简洁更轻便了,所以自然也就会对程序员更友好了。当然我并不否认 Nutz 可能依然
存在一些脑残设计,我和你们一样不能忍受它们,如果发现它们请第一时间通知我。在讨论区发个贴就是个很好的办法。