动态表名

优质
小牛编辑
135浏览
2023-12-01

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

动态表名的常用形式

通常情况下动态表名都是通过一个后缀来表示的。比如我们要记录全中国所有的公司以及其雇员,通常的设计是建立
两张数据表, 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 之间的代码,就是动态表名变量的生命周期。当然,这个写法存在两个潜在
的危险。

  1. 可能在 ThreadLocal 留下垃圾动态表名变量
  2. 会清除掉 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 是比较独特的。它基本做到了这两个效果

  1. 如果你不希望使用动态表名,你根本不会看到 Nutz.Dao 关于动态表名的设计
  2. 如果你希望使用动态表名,你并不需要学习更多的配置方法

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