NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

当Entity继承遇到Hibernate的@PrePersist和@PreUpdate

| Comments

上周做项目的时候遇到的关于实现审计日志方式的问题,这里记录一下。

假设你的数据库是这样设计的:

1
2
3
4
5
6
7
8
9
10
{
    "Customer": {
        "id": 1,
        "name": "benwei"
    },
    "AdvancedCustomer": {
        "customerId": 1,
        "level": 1
    }
}

一个Customer表和一个AdvancedCustomer表,AdvancedCustomer表中含有CustomerId作为外键。

在Java中的Entity实现是这样的:AdvancedCustomer继承自Customer,父类定义的继承策略是Joind。

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table
@Inheritance(Strategy=InheritanceType.JOINED)
public class Customer {
  
}

@Entity
@Table
public class AdvancedCustomer extends Customer {
  
}

Joind策略的含义是:A strategy in which fields that are specific to a subclass are mapped to a separate table than the fields that are common to the parent class, and a join is performed to instantiate the subclass.

通用的属性定义在父类中表,特殊的属性映射到另一个独立的表。

此时,你想要给应用添加一个审计功能。

你给Customer表添加LastModifiedBy和LastModifiedDate两个字段。

1
2
3
4
5
6
7
8
9
10
11
12
{
    "Customer": {
        "id": 1,
        "name": "benwei",
        "lastModifiedBy": "benwei",
        "lastModifiedDate": "21/6/2015 23:01:11"
    },
    "AdvancedCustomer": {
        "customerId": 1,
        "level": 1
    }
}

做法是使用EntityListener,在Listener中使用Hibernate的@PrePersist和@PreUpdate来监听事件的发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AuditListener {

    @PrePersist
    public void prePersist(Object entity) {
      if(entity instanceOf AdvancedCustomer) {
          ((AdvancedCustomer)entity).setLastModifiedDate(Datetime.now());  
      }
    }

    @PreUpdate
    public void preUpdate(Object entity) {
      ...
    }
}

@Entity
@Table
@EntityListeners([AuditListener.class])
public class AdvancedCustomer extends Customer {
  
}

问题来了,这个prePersist或者preUpdate方法会在什么时候触发呢?当修改AdvancedCustomer中的任意变量时,比如name,level,都会触发prePersist或者preUpdate。

但是,你到数据库中去查看审计事件变化时会发现,当创建一个新的Customer,或者更新Customer的名字字段都没有问题。

当update子类中的变量level的时候,lastModifiedDate并没有发生变化。这是为什么?

要找到原因,必须打开showSql属性,来查看Hibernate到底产生的SQL语句是什么。

你会发现,当修改level变量时,Hibernate只产生了一条update语句来更新AdvancedCustomer这张表。而创建Customer会同时更新AdvancedCustomer和Customer两张表,更新name字段,会更新Customer这张表。

也就是说,在PreUpdate触发之前,Hibernate在策略上已经决定了只更新AdvancedCustomer。即便之后改变了Customer中的lastModifiedDate,也没有改变它的行为。这里并不是说PreUpdate没有起到作用,而是Hibernate之决定更新一张表,至于更新什么内容,要等到PreUpdate之后决定(这一点可以从更新name时,lastModifiedDate发生了改变来证明)。

如何解决:

目前,我们没有完美的解决方案可以在仍然使用PreUpdate的情况下,保证审计信息更新正确。

出现这个问题的主要原因是因为我们的实现受到框架实现机制的限制。

所以,我们改变了实现的策略,既然受到框架本身实现策略的限制,我们就脱离框架,在还未计入框架管理范围之内,就将审计信息写入Entity内,那么可行的一种方式就是AOP。在触发Hibernate的save方法之前,将审计信息写入Entity。

在AngularJS环境下写单元测试:module,inject和$httpBackend

| Comments

Angular测试基础:module和inject

先来最简单的样例代码,Controller端代码

1
2
3
4
5
6
7
8
angular.module('angularGruntExampleApp')
    .controller('MainCtrl', function ($scope) {
        $scope.awesomeThings = [
            'HTML5 Boilerplate',
            'AngularJS',
            'Karma'
        ];
    });

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe('Controller: MainCtrl', function () {

    beforeEach(module('angularGruntExampleApp'));

    var MainCtrl, scope;

    beforeEach(inject(function ($controller, $rootScope) {
        scope = $rootScope.$new();
        MainCtrl = $controller('MainCtrl', {
            $scope: scope
        });
    }));

    it('should attach a list of awesomeThings to the scope', function () {
        expect(scope.awesomeThings.length).toBe(3);
    });
});

beforeEach()是Jasmine提供的全局方法,在每个测试方法执行之前,调用一次传入的回调函数。

module()方法是由angular-mocks提供,用来加载给定的Angular模块。

$rootScope.$new()创建了一个scope对象,并且在$controller获取MainCtrl时,将scope对象注入。

angular.mock.inject函数接受一个回调函数,回调函数的参数,是需要注入的外部依赖,可以是angular提供的服务,比如,这里的$controller和$rootScope,也可以是你想要测试的自定义服务,如下:

1
2
3
4
5
6
7
8
9
10
11
12
// Defined out reference variable outside
var myService;

// Wrap the parameter in underscores
beforeEach(inject(function(_myService_){
  myService = _myService_;
}));

// Use myService in a series of tests.
it('makes use of myService', function() {
  myService.doStuff();
});

在上面的代码中,注入的myService,带有下划线,这是inject方法提供的一个特性,因为,我们总是希望在describe这个作用域下定义的变量名可以和真实的Service名字一致,所以inject允许你在注入的参数中加入下划线以区分注入的参数和定义的变量。

$httpBackend

在单元测试中,我们希望单元测试可以快速的运行,并且没有外部依赖,所以,我们不希望真正的发送HTTP请求到真正的服务器。我们想要的是验证请求已发送,然后将预先定义的请求返回。

$httpBackend就是这样一个提供fake响应的服务器端mock对象实现。通过$httpBackend.expect和$httpBackend.when来制定响应结果和条件。

Flushing HTTP requests

在产品环境中,代码中对http服务器端的请求都是异步,但是在单元测试中,我们不太容易实现异步的测试。httpBackend提供的flush方法允许测试立即flush等待的请求,这样就可以让异步请求同步化,这样就可以在单元测试中同步的测试http请求。

使用$httpBackend非常的简单:

1
2
3
4
5
6
7
$httpBackend = $injector.get('$httpBackend'); //注入$httpBackend服务

$httpBackend.when('GET', '/customer/1').respond({customerId: '1',name:'benwei'});
scope.getCustomer('1'); // 调用scope的方法发出http请求
$httpBackend.flush(); // 让http请求立刻执行

expect(scope.customer).toEqual({customerId: '1',name:'benwei'});

参考资料:
1.http://docs.ngnice.com/api/ngMock
1.http://docs.ngnice.com/api/ngMock/service/$httpBackend