动态表名

Jul 10, 2017 10:38:44 AM

作者:zozoh

为什么需要动态表名

当数据量比较大的时候,为了提高数据库操作的效率,尤其是查询的效率,其中一种解决方案就是将数据表拆分。拆分的数据表,结构完全一致,只不过是表的名字,按照某种规律,而成为一组。

动态表名的常用形式

通常情况下动态表名都是通过一个后缀来表示的。比如我们要记录全中国所有的公司以及其雇员,通常的设计是建立两张数据表, 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 注解本身就支持动态表名,比如:

  1. @Table("t_employee_${cid}")
  2. public class Employee{
  3. // The class fields and methods...
  4. }

@Table 注解支持字符串模板的写法,在你希望放置动态表名变量的位置插入 ${变量名} ,如 ${cid},那么${cid} 会在运行时被 Nutz.Dao 替换。

如何替换?用什么替换?请看下面一节。

在调用时应用动态表名

Nutz.Dao 提供了一个小巧的类: org.nutz.dao.TableName。 通过这个类你可以随意设置你的动态表名:

  1. public void demoTableName(final Dao dao) {
  2. TableName.run(3, new Runnable() {
  3. public void run() {
  4. Employee peter = dao.fetch(Employee.class, "peter");
  5. System.out.println(peter);
  6. }
  7. });
  8. }

通过创建一个匿名 java.lang.Runnable 对象,你可以像静态 POJO 一样使用 Dao 接口的一切方法。因为通过你传入的参数 3 (TableName.run 方法的第一个参数), 以及前面的 @Table 声明,Nutz.Dao 已经很清楚如何操作数据库了,它会用 3 替代 $cid。

如果细心一些,你会发现在 TableName.run 方法的声明是:

  1. public static void run(Object refer, Runnable atom);

是的,第一个参数是个 Object,也就是说,你可以传入任何对象,上面的例子我们传入的是个整数,Java编译器会自动将其包裹成 Integer 对象。考虑到动态表名可能存在的复杂性(在前面一节“动态表名的常用形式”提到),你还可以传入一个 Map 或者一个POJO, Nutz.Dao 会根据你的传入为动态表名的多个变量同时赋值。下面我列出一个完整的动态表名赋值规则

动态表名赋值规则

  • 当传入参数为数字或字符串
    • 所有的动态表名变量将被其替换
  • 当传入参数为 Map
    • 按照动态表名变量的名称在 Map 中查找值,并进行替换
    • 大小写敏感
    • 未找到的变量将被空串替换
  • 当传入参数为 任意Java对象(POJO)
    • 按照动态表名变量名称在对象中查找对应字段的值,并进行替换
    • 大小写敏感
    • 未找到的变量将被空串替换
  • 当传入参数为null
    • 所有变量将被空串替换

更灵活的应用动态表名

使用 TableName.run 提供的动态表名模板的方式设置动态表名是很好的做法,也很安全。因为不会在 ThreadLocal里面留下垃圾动态表名变量值。但是通过模板的写法另外一方面也限制了一定线程灵活性。所以上述例子还有另外一个写法:

  1. public void demoTableName2(final Dao dao) {
  2. TableName.set(3);
  3. Employee peter = dao.fetch(Employee.class, "peter");
  4. System.out.println(peter);
  5. TableName.clear();
  6. }

在 TableName.set 和 TableName.clear 之间的代码,就是动态表名变量的生命周期。当然,这个写法存在两个潜在的危险。

  • 可能在 ThreadLocal 留下垃圾动态表名变量
  • 会清除掉 ThreadLocal 所有的动态表名变量
    关于第一点,可以用 try…catch…finally 来解决:
  1. public void demoTableName3(final Dao dao) {
  2. try {
  3. TableName.set(3);
  4. Employee peter = dao.fetch(Employee.class, "peter");
  5. System.out.println(peter);
  6. } finally {
  7. TableName.clear();
  8. }
  9. }

这样,永远都不会留下垃圾动态表名了

关于第二点,是的,如果在 TableName.set(3) 之前你曾经设置了另一个动态表名变量,比如

  1. public void demoTableName_multi(final Dao dao) {
  2. TableName.set(10);
  3. Employee john = dao.fetch(Employee.class, "john");
  4. System.out.println(john);
  5. TableName.set(3);
  6. Employee peter = dao.fetch(Employee.class, "peter");
  7. System.out.println(peter);
  8. TableName.clear();
  9. Employee mary = dao.fetch(Employee.class, "Mary");
  10. System.out.println(mary);
  11. TableName.clear();
  12. }

当执行到 Employee mary = dao.fetch(Employee.class, "Mary"); 一定会出错,不是吗? 所以比较安全的写法是

  1. public void demoTableName_multi(final Dao dao) {
  2. TableName.set(10);
  3. Employee john = dao.fetch(Employee.class, "john");
  4. System.out.println(john);
  5. Object old = TableName.get();
  6. try {
  7. TableName.set(3);
  8. Employee peter = dao.fetch(Employee.class, "peter");
  9. System.out.println(peter);
  10. } finally {
  11. TableName.set(old);
  12. }
  13. Employee mary = dao.fetch(Employee.class, "Mary");
  14. System.out.println(mary);
  15. TableName.clear();
  16. }

比较麻烦是吗? 是的,使用 TableName.get … TableName.set … TableName.clear 虽然带来更大的灵活性,但是写起来有点麻烦,这也是为什么我要提供模板写法,上面的例子用模板的写法看起来是这个样子的:

  1. public void demoTableName_multi_temp(final Dao dao) {
  2. TableName.run(10, new Runnable() {
  3. public void run() {
  4. Employee john = dao.fetch(Employee.class, "john");
  5. System.out.println(john);
  6. TableName.run(3, new Runnable() {
  7. public void run() {
  8. Employee peter = dao.fetch(Employee.class, "peter");
  9. System.out.println(peter);
  10. }
  11. });
  12. Employee mary = dao.fetch(Employee.class, "Mary");
  13. System.out.println(mary);
  14. }
  15. });
  16. }

看,虽然行数并没有减少,但是你不会犯错了。是的,模板写法主要的目的是 为了让你不会出错。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 对象必然会有类似如下的代码:

  1. @Table("t_company")
  2. public class Company {
  3. @Id
  4. private int id;
  5. @Name
  6. private String name;
  7. @Column
  8. private int ceoId;
  9. @One(target = Employee.class, field = "ceoId")
  10. private Employee CEO;
  11. public int getId() {
  12. return id;
  13. }
  14. public void setId(int id) {
  15. this.id = id;
  16. }
  17. public String getName() {
  18. return name;
  19. }
  20. public void setName(String name) {
  21. this.name = name;
  22. }
  23. public int getCeoId() {
  24. return ceoId;
  25. }
  26. public void setCeoId(int ceoId) {
  27. this.ceoId = ceoId;
  28. }
  29. public Employee getCEO() {
  30. return CEO;
  31. }
  32. public void setCEO(Employee ceo) {
  33. CEO = ceo;
  34. }
  35. }

比如,我们要在控制台上打印"nutz" 的公司的 CEO 的名字时,代码将为:

  1. public void demoTableName_links_one(final Dao dao) {
  2. final Company nutz = dao.fetch(Company.class,"nutz");
  3. TableName.run(nutz.getId(), new Runnable(){
  4. public void run() {
  5. dao.fetchLinks(nutz, "CEO");
  6. }
  7. });
  8. System.out.println(nutz.getCEO().getName());
  9. }

一对多映射

下面我们来增加一对多映射,是的,一个公司有很多雇员,不是吗?那么我们就为 Company 类增加一个新的字段

  1. @Table("t_company")
  2. public class Company {
  3. // ... another fields ...
  4. @Many(target = Employee.class, field = "companyId")
  5. private List<Employee> employees;
  6. public List<Employee> getEmployees() {
  7. return employees;
  8. }
  9. public void setEmployees(List<Employee> employees) {
  10. this.employees = employees;
  11. }
  12. // ... another methods ...
  13. }

可以看到,在字段 employees 增加了 @Many 说明,就像一般的一对多映射一样, field 项声明了 Employee 类必须有一个名叫(companyId)的字段指向 Company 的主键。所以 Employee 类的代码为:

  1. @Table("t_employee_${cid}")
  2. public class Employee {
  3. @Id
  4. private int id;
  5. @Name
  6. private String name;
  7. @Column("comid")
  8. private int companyId;
  9. public int getCompanyId() {
  10. return companyId;
  11. }
  12. public void setCompanyId(int companyId) {
  13. this.companyId = companyId;
  14. }
  15. public int getId() {
  16. return id;
  17. }
  18. public void setId(int id) {
  19. this.id = id;
  20. }
  21. public String getName() {
  22. return name;
  23. }
  24. public void setName(String name) {
  25. this.name = name;
  26. }
  27. }

一切设置完毕,我们就可以这么调用:

  1. public void demoTableName_links_many(final Dao dao) {
  2. final Company nutz = dao.fetch(Company.class, "nutz");
  3. TableName.run(nutz.getId(), new Runnable() {
  4. public void run() {
  5. dao.fetchLinks(nutz, "employess");
  6. }
  7. });
  8. for (Employee e : nutz.getEmployees())
  9. System.out.println(e.getName());
  10. }

上面的程序会逐行打印出 nutz 公司所有的雇员名称。

多对多映射

多对多映射是通过一个中间表进行的数据关联,比如数据库中有数据表 A,B, 可以在建立一张表 C,描述 A 表和 B 表的数据关联。一般的关联表至少有两个字段,一个是 A 表的主键,另一个记录 B 表的主键。如果是复合主键或者要记录关联的权重,关联表的设计将会更加复杂。

Nutz.Dao 提供了 @ManyMany 注解帮助你的 POJO 来声明多对多关联,比如在 Employee 类中可以增加一个新的字段,表示某个雇员的下属。

  1. @ManyMany(target = Employee.class, relation = "t_employee_staff_${cid}", from = "eid", to = "sid")
  2. private List<Employee> staffs;

请注意

  • relation 项就是关联表的名称,这个名称也可以写成动态的。
  • from 项是关联表字段的名称,将对应到本 POJO 的主键,这里的 eid 将对应 Employee.id
  • to 项也是关联表字段的名称,将对应到 target 项声明的主键,这里的 sid 也将对应 Employee.id
    在调用代码里可以这样调用
  1. public void demoTableName_links_manymany(final Dao dao) {
  2. final Company nutz = dao.fetch(Company.class, "nutz");
  3. TableName.run(nutz.getId(), new Runnable() {
  4. public void run() {
  5. Employee peter = dao.fetch(Employee.class,"Peter");
  6. dao.fetchLinks(peter, "staffs");
  7. for (Employee e : peter.getStaffs())
  8. System.out.println(e.getName());
  9. }
  10. });
  11. }

这段代码将 nutz 公司雇员 Peter 的所有下属逐行打印出来。

无需匿名内部类的写法

总结一下

关于动态表名的这一节写的比较长,因为我认为动态表名的支持,Nutz.Dao 是比较独特的。它基本做到了这两个效果

  • 如果你不希望使用动态表名,你根本不会看到 Nutz.Dao 关于动态表名的设计
  • 如果你希望使用动态表名,你并不需要学习更多的配置方法
    并不是因为我是 Nutz 的作者而努力的在为自己吹嘘,如果你细心体会 Nutz 各个模块的设计,所有的设计基本是本着上述两个原则,即需要的时候你会看见,不需要的时候尽量让你看不见。由于我是个职业界面设计师,所以我会自然而然的将我设计 UE 时很多原则应用在编程接口的设计上,事实上我发现,这的确在某种程度上让程序接口更简洁更轻便了,所以自然也就会对程序员更友好了。当然我并不否认 Nutz 可能依然存在一些脑残设计,我和你们一样不能忍受它们,如果发现它们请第一时间通知我。在讨论区发个贴就是个很好的办法。

本页面的文字允许在知识共享 署名-相同方式共享 3.0协议GNU自由文档许可证下修改和再使用。

原文: http://nutzam.com/core/dao/dynamic_table_name.html