19.1 扩展开发教程
对于那些从事开发扩展的人来说,有很多有用的资源。(插件/附加组件)到DataCleaner。为了帮助你,这里有一个有用的拓展开发。如果您认为此开发还不够,请告知我们:
- 教程1:开发转换器transformer
- 教程2:开发分析器analyzer
- 教程3:实现自定义数据存储
- Javadoc: DataCleaner
- Javadoc: MetaModel
===================================================================
教程1:开发转换器transformer
(2010/09/07)
使用DataCleaner Java API开发值转换器
在这篇博客文章中,我将演示DataCleaner的Java Api来创建转换器,即用于基于数据集的现有值进行转换/可变/标记化/生成新值的组件。您将需要Java编程技能来学习本教程。
我发现解释这个过程最简单的方法就是运行一个例子。所以这里是我的例子:我想把人的出生日期(表示为Date 字段)转换为年龄字段(表示为number 字段)。场景描述如下:
转换后,我将能够独立处理年龄字段,例如,通过数字分析、值分布或应用一些依赖于年龄的业务规则。
构建转换器transformer 的要求如下:
- 该类必须要实现Transformer接口
- 该类上面必须用 (javax.inject)@Named注解。该注解接受一个参数:转换器的可读名称。我们将这样注释:@Named(“Date to age”)
- 为了从传入字段中读取数据,我们需要注入一个 InputColumn<E> 实例(或者一个数组),其中<E>是传入字段的数据类型。为了注入,我们使用@Configured注释。在我们的示例中,这转换为:@Configured InputColumn<Date> dateColumn;
在这些步骤之后,我们的代码将如下所示:
@Named("Date to age")//就是转换器的名字
public class DateToAgeTransformer implements Transformer {
@Configured
InputColumn<Date> dateColumn;//从数据表中传入的字段数据
@Override
public OutputColumns getOutputColumns() {
// TODO
return null;
}
@Override
public Object[] transform(InputRow inputRow) {
// TODO
return null;
}
}
如我们所见,Transformer接口定义了两种方法,我们需要实现它们。他们是:
- getOutputColumns(): 框架调用此方法以确定转换器将生成哪些虚拟列。在我们的例子中,它非常简单:transformer为age创建虚拟列(以天为单位和以年为单位,只是为了使它更灵活)。因此,方法主体应该是:
return new OutputColumns(Integer.class, "Age in days", "Age in years");
- transform(InputRow): 对于每一行要转换的值,都将调用此方法。方法的返回类型是表示行的新值的对象数组。返回数组的索引应与输出列匹配,即索引0表示“以天为单位的年限”,索引1表示“以年为单位的年限”。让我们看一下方法实现:
Integer[] result = new Integer[2];
Date date = inputRow.getValue(dateColumn);
if (date != null) {
long diffMillis = today.getTime() - date.getTime();
int diffDays = (int) (diffMillis / (1000 * 60 * 60 * 24));
result[0] = diffDays;
// use Joda time to easily calculate the diff in years
int diffYears = Years.yearsBetween(new DateTime(date), new DateTime(today)).getYears();
result[1] = diffYears;
}
return result;
当然,在编写本教程的过程中,我并没有在没有嵌入代码的情况下完成所有的工作,这样您就可以实际使用它了。这里提供了 ”Date to age” 转换器的代码,这里还有一个单元测试,可以用来演示如何对转换器进行单元测试。我希望你们中的一些人参与开发转换器,让我知道结果如何。在我的下一篇博客文章中,我将解释如何构建分析器,这是为DataCleaner开发组件时显而易见的下一步。
这里还有其他一些转换器的好例子:
- 转换为日期转换器,它将尝试将任何值转换为日期。这对于我刚刚在本教程中解释的转换器来说可能很有用。换句话说:如果要转换的出生日期存储在基于字符串的字段中,那么这两个转换器可能需要链接。
- 标记器转换器,因为它具有基于用户配置的灵活数量的输出列。请注意该例子中用到的的 @Configured Integer numTokens变量。
===================================================================
教程2:开发分析器analyzer
(2010/09/26)
使用 AnalyzerBeans Java API 开发分析器
上文已经写了如何开发一个转换器。现在是时候来看看如何开发一个分析器了,它是一个用来消耗数据并将其转换成一个可供人阅读的结果的组件,希望它是有用的。Java API的Javadocs位于这里。AnalyzerBeans 中已经有很多不同的分析器,当您决定开发自己的分析器时,不妨看看这些分析器:
- 对于典型的度量,有数字分析器(Number analyzer)和字符串分析器(String analyzer)之类的分析器。这些分析器计算这些数据类型的标准化度量。
- 有一个值分布分析器(Value distribution)很有趣,因为它使用一个后备数据库(使用 @Provided注释)来计算唯一值,如果值超过了可用内存量。
- 日期间隔分析器(Date gap analyzer)也是一个很好的例子,因为它命名了输入列,用于构建从日期到日期的时间线。
- 模式查找器(PatternFinder)分析器,你可以在我以前的一篇博文中读到更多。
让我们从一个简单的例子开始。假设您想构建一个非常简单的分析器,它使用基于日期date 或时间time 的值,并根据星期几确定值的分布(即,如何将值的分布分组在星期一、星期二、星期三等)。虽然这是一个相当幼稚的分析器示例,但它也能很好地工作作为一个示例。
我们将从构建分析器(analyzer)的要求开始:
- 您需要定义一个实现 Analyzer<R>的类。泛型 ‘R’ 参数定义了分析器的结果类型。我们可以重用内置的结果类型,也可以编写自己的结果类型。
- 该类需要用@AnalyzerBean注解。此注解接受一个参数:分析器的显示名称。
- 您需要使用@Configured注释注入一个或多个InputColumn<E>,以便使用传入的数据。<E>参数定义感兴趣的数据类型,它还用于确定分析器支持哪些类型的数据类型。在本例中,我们将使用Date作为InputColumn类型,因为我们希望分析器使用日期值。
因此我们的类是按照上述要求创建的:
@AnalyzerBean("Average date analyzer")
public class AverageDateAnalyzer implements Analyzer<CrosstabResult> {
@Configured
InputColumn<Date> dateColumn;
public void run(InputRow row, int distinctCount) { ... }
public CrosstabResult getResult() { ... }
}
请注意,我们使用的是内置的结果类型CrosstabResult,它表示由维度交叉表组成的结果。我们可以使用其他内置的结果类型,也可以创建自己的结果类型,唯一的要求是它得实现AnalyzerResult接口。
分析器的其余部分应该是“熟知的Java”,但当然也要使用AnalyzerBeans中提供的API。这些事我以前都解释过,但我会再解释一遍。
所以现在来考虑如何实现具体的分析器逻辑。我们将使用常规 map 来保存分布值。我们将把工作日的数字映射到这个 map 上并计数。但我们需要为正在分析的每一列保留一个计数,因此它将是一个嵌套map:
private Map<InputColumn<Date>, Map<Integer, Integer>> distributionMap;
要初始化映射,我们需要先注入InputColumn。相反,我们可以用在方法上添加 @Initialize 注解,这将使AnalyzerBeans在bean被正确初始化时调用该方法。
@Initialize
public void init() {
distributionMap = new HashMap<InputColumn<Date>, Map<Integer, Integer>>();
for (InputColumn<Date> col : dateColumns) {
Map<Integer, Integer> countMap = new HashMap<Integer, Integer>(7);
for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
// put a count of 0 for each day of the week
countMap.put(i, 0);
}
distributionMap.put(col, countMap);
}
}
现在 map 已经初始化,我们可以继续实现run(…)方法:
@Override
public void run(InputRow row, int distinctCount) {
for (InputColumn<Date> col : dateColumns) {
Date value = row.getValue(col);
if (value != null) {
Calendar c = Calendar.getInstance();
c.setTime(value);
int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
Map<Integer, Integer> countMap = distributionMap.get(col);
int count = countMap.get(dayOfWeek);
count += distinctCount;
countMap.put(dayOfWeek, count);
}
}
}
这应该相当于“Java日常使用”。如果您是一名经验丰富的Java开发人员,那么您唯一应该了解的是使用InputColumns作为限定符从InputRow提取值的方法:
Date value = row.getValue(col);
请注意,value变量具有日期Date 类型。AnalyzerBeans API 在很大程度上利用了类型安全性。由于注入的InputColumn被定义为日期列,这意味着我们可以安全大胆地假设传入行中的值一定是日期类型。此外,日期列将用于验证AnalyzerBeans job 的配置,如果用户尝试使用非日期列配置该特定Analyzer,则会向用户发送早期错误消息。现在开始创建结果。如前所述,我们将为此使用交叉表结果CrosstabResult 。交叉表结果是一种非常动态的结果类型,可以用于许多目的。它类似于DataCleaners结果矩阵,但增加了一些特性。下面是我们如何该分析器的结果交叉表:
@Override
public CrosstabResult getResult() {
CrosstabDimension columnDimension = new CrosstabDimension("Column");
CrosstabDimension weekdayDimension = new CrosstabDimension("Weekday");
weekdayDimension.addCategory("Sunday").addCategory("Monday")
.addCategory("Tuesday").addCategory("Wednesday").addCategory("Thursday")
.addCategory("Friday").addCategory("Saturday");
Crosstab crosstab = new Crosstab(Integer.class, columnDimension, weekdayDimension);
for (InputColumn col : dateColumns) {
columnDimension.addCategory(col.getName());
CrosstabNavigator nav = crosstab.where(columnDimension, col.getName());
Map countMap = distributionMap.get(col);
nav.where(weekdayDimension, "Sunday").put(countMap.get(Calendar.SUNDAY));
nav.where(weekdayDimension, "Monday").put(countMap.get(Calendar.MONDAY));
nav.where(weekdayDimension, "Tuesday").put(countMap.get(Calendar.TUESDAY));
nav.where(weekdayDimension, "Wednesday").put(countMap.get(Calendar.WEDNESDAY));
nav.where(weekdayDimension, "Thursday").put(countMap.get(Calendar.THURSDAY));
nav.where(weekdayDimension, "Friday").put(countMap.get(Calendar.FRIDAY));
nav.where(weekdayDimension, "Saturday").put(countMap.get(Calendar.SATURDAY));
}
return new CrosstabResult(getClass(), crosstab);
}
现在我们结束了。你可以看看这里的最终结果。当我用三列中的小数据样本运行此分析器时,结果如下所示:
Order date Shipment date Delivery date
Sunday 0 0 0
Monday 2 0 1
Tuesday 0 2 1
Wednesday 0 0 0
Thursday 1 0 0
Friday 1 1 2
Saturday 0 1 0
你也可以在这里查看这个分析器的单元测试。(网页失效了,但源代码中有)
===================================================================
教程3:实现自定义数据存储
(2012/04/09)
在DataCleaner中实现自定义数据存储
DataCleaner的超级用户、合作伙伴和开发人员经常问我一个问题:如何在DataCleaner中为我的system/file-format XYZ构建自定义数据存储?最近,我处理这个问题是为了在即将到来的与Pentaho-Kettle的集成中使用,对于一个拥有自己开发的数据库代理系统的人工接口客户,就在今天,当它在DataCleaner论坛上被询问时。在这篇博文中,我将指导您完成这个过程,这需要一些基本的Java编程技能,但如果这一点已经到位的话,它并不十分复杂。
首先,我应该说(对于那些喜欢“只看代码”的人来说),在DataCleaner的源代码已经有了一个简单示例( sample extension)。看一看这个 org.eobjects.datacleaner.sample.SampleDatastore 类。一旦您阅读、理解并编译了Java代码,您所需要做的就是在DataCleaner中的conf.xml文件 注册数据存储 (在< datastore-catalog >便签中):
<custom-datastore class-name="org.eobjects.datacleaner.sample.SampleDatastore">
<property name="Name" value="My datastore" />
</custom-datastore>
工作原理???
首先,DataCleaner中的数据存储需要实现 Datastore 接口。但是我建议使用名为UsageAwareDatastore的抽象实现,而不是直接实现接口。这个抽象实现处理对数据存储的并发访问,重用现有连接等等。在扩展UsageAwareDatastore类时,您仍然需要提供的主要是 createDatastoreConnection() 方法,该方法在请求新连接时被调用。让我们看看新建一个数据存储实现是什么样子的:
public class ExampleDatastore extends UsageAwareDatastore<DataContext> {
private static final long serialVersionUID = 1L;
public ExampleDatastore() {
super("My datastore");
}
@Override
protected UsageAwareDatastoreConnection createDatastoreConnection() {
// TODO Auto-generated method stub
return null;
}
@Override
public PerformanceCharacteristics getPerformanceCharacteristics() {
// TODO Auto-generated method stub
return null;
}
}
注意,我已经创建了一个无参数构造函数。这对于自定义数据存储是必需的,因为数据存储将由DataCleaner实例化。稍后我们将重点讨论如何调整名称(“My datastore”)。
首先,我们来看看两个未实现的方法:
- createDatastoreConnection() : c用于创建新连接。DataCleaner构建在元模型框架之上,用于数据访问。您需要返回一个新的DatastoreConnectionImpl(…)。该类接受一个重要参数,即MetaModel DataContext implementation。通常在给定的配置情况下,已经有了一个可以使用的DataContext,例如JdbcDataContext、CsvDataContext、ExcelDataContext、MongoDbDatacontext或其他什么。
- getPerformanceCharacteristics(): DataCleaner在执行作业时使用该方法来确定查询计划。通常只返回一个新的PerformanceCharacteristics(false);,阅读javadoc了解更多信息
可参数化属性???
到目前为止,您应该能够实现一个定制的数据存储,这有望满足您的基本需求。但是也许你想用不同的文件、不同的主机名等重用datastore类,换句话说:也许你想让你的用户定义datastore的某些属性。
拯救方法是@Configured注解,它是DataCleaner中广泛使用的注解。它允许您在类中注释应该由用户配置的字段。字段的类型可以是字符串、整数、文件等。让我们看看如何公开特有连接的属性:
public class ExampleDatastore extends UsageAwareDatastore<DataContext> {
// ...
@Configured
String datastoreName;
@Configured
String hostname;
@Configured
Integer port;
@Configured
String systemId;
// ...
}
以及通常如何使用它们来实现方法:
public class ExampleDatastore extends UsageAwareDatastore<DataContext> {
// ...
@Override
public String getName() {
return datastoreName;
}
@Override
protected UsageAwareDatastoreConnection createDatastoreConnection() {
DataContext dataContext = createDataContext(hostname, port, systemId);
return new DatastoreConnectionImpl(dataContext, this);
}
}
如果我想使用上面的参数配置配出一个数据存储,我可以在我的conf.xml格式文件如下:
<custom-datastore class-name="foo.bar.ExampleDatastore">
<property name="Datastore name" value="My datastore" />
<property name="Hostname" value="localhost" />
<property name="Port" value="1234" />
<property name="System id" value="foobar" />
</custom-datastore>
请注意,属性的名称是通过反转Java使用的camelCase表示法推断出来的,这样 “datastoreName” 就变成了 “Datastore name” ,以此类推。或者,您可以在 @Configured 注解中提供显式名称。
我希望这个介绍教程对你有意义。我再次敦促您查看示例Sample DataCleaner extension,它还包括构建设置(基于Maven)和自定义元模型DataContext实现。