ebean-orm / ebean

Ebean ORM
https://ebean.io
Apache License 2.0
1.46k stars 260 forks source link

不知道 ebean 有没有带 where 条件的更新 #459

Closed liuanxin closed 8 years ago

liuanxin commented 8 years ago

如果有一张订单表, 需要将状态由已创建变更为已支付, 对应的 sql 语句是:

update t_order set status = 'Pay' where id = xxx and status = 'Create'

然后根据返回的结果是否为 1 来判断更新有没有成功, 从而决定是否将事务进行回滚.

不想使用 version 的原因在于: 如果有其他的地方在做跟 status 无关的更新操作, 这里的更新将会失败, 还是想基于这一个属性做这种颗粒度的更新, 而且从性能的角度来说, 也多了一次 load version 的查询

v = select version from t_order where id = xxx

update t_order set status = 'Pay', version = v + 1
where id = xxx and version = v

虽然也能用 createQuery 的方式完成这个实现

public void pay(Long id) {
    String delete = "update t_order set status = :new and id = :id and status = :old";
    SqlUpdate update = ebeanServer.createSqlUpdate(delete);
    update.setParameter("id", id);
    update.setParameter("old", "Create");
    update.setParameter("new", "Pay");

    if (update.execute() != 1) {
        // rollback...
        return;
    }
}

但是, 在代码中嵌入 SQL, 多少还是挺难受的, 一旦表名或字段名发生变更(尽管这种情况并不多见)就难受了, 虽然可以挪到 xml, 但是在我看来那样更难受.

我的意思是, 能不能在 ebeanServer 中添加一个像下面这样的方法:

public interface EbeanServer {
    ...
    int update(Object newBean, Object oldBean) throws OptimisticLockException;
    ...
}

这样一来, 调用时, 我只需要

Order order = new Order();
order.setStatus("pay");

Order old = new Order();
old.setId(id);
old.setStatus("Create");

if (ebeanServer.update(order, old) != 1) {
    log...

    // trigger transaction rollback or do anything with business-related operations
    throw new ServiceException("change status fail. please check! order_id: {}", id);
}

当然, 可能带 where 的条件规则会很多, 不仅仅只是这么简单了. 比如

update t_order set status = 'Pay'
where id = xxx and (status = 'Create' or status = 'Confirm')

delete t_system_log where is_delete = 1 and create_time <= '3 month ago'

会有这种需求, 是因为之前我用的 mybatis, 可以用 example 轻松组装出上面的 sql 出来

zhangshifeng commented 8 years ago

为什么不先查询订单,然后逻辑处理,最后更新订单

Order order = ebeanServer.find(Order.class, id);
if (order != null && order.canPay()) {
  order.doPay();
  ebeanServer.update(order);
} else {
  ...
}
liuanxin commented 8 years ago

@zhangshifeng 这样就不是乐观操作了, 也就是说, 下面的语句

update set colum = new where id= xxx and column = old

对数据库来说, 其内部会进行原子操作. 通过对 返回的结果是否为更新的行数值 判断, 还可以灵活的控制事务, 这对其他任何地方的逻辑都不会有侵入. 如果像上面那样操作, 在高并发的情况下, 就得使用 for update 这种极端的悲观锁机制了

zhangshifeng commented 8 years ago

@liuanxin 如果调用EbeanServer#update(Object) throws OptimisticLockException时,被更新实体中定义了版本字段,并且包含版本信息,那么框架就会进行乐观锁检查.

liuanxin commented 8 years ago

@zhangshifeng 嗯,所以我最开始就说了嘛,不想用 version 的原因就是想能基于某个字段做更颗粒度的控制(尽管会有 ABA 的风险),而不是基于单行的操作,而且使用 version,其内部实现总是会先 load 一下 version

rbygrave commented 8 years ago

I'll assume this discussion is complete and close this issue. Thanks.

icode commented 8 years ago

我很好奇,外国友人们是怎么知道你们讨论的是什么的?

icode commented 8 years ago

@liuanxin @zhangshifeng 我试了一下,不知道如何触发乐观锁 @Version

liuanxin commented 8 years ago

@icode 语言这种事... 通天塔到哪都会有, 主要还是自己 E 文不够好

@wenzhihong2003 +1

icode commented 8 years ago

@liuanxin @zhangshifeng @wenzhihong2003 加下qq吧,一起讨论ebean和java icode@log4ic.com

zhangshifeng commented 8 years ago

@icode 下面触发乐观锁的测试用例

package com.example.test;

import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.tests.model.basic.Customer;
import org.junit.Test;

import javax.persistence.OptimisticLockException;

import static org.junit.Assert.*;

public class TestOptimisticLockException extends BaseTestCase {

    @Test
    public void test() {
        Customer customer = new Customer();
        customer.setName("a");
        Ebean.save(customer);

        Customer customer1 = Ebean.find(Customer.class, customer.getId());
        customer1.setName("b");
        Customer customer2 = Ebean.find(Customer.class, customer.getId());
        customer2.setName("c");

        Ebean.update(customer1);
        try {
            Ebean.update(customer2);
            assertTrue("Never...", false);
        } catch (Exception e) {
            assertTrue("An OptimisticLockException", e instanceof OptimisticLockException);
        }

        Ebean.refresh(customer);
        assertEquals("b", customer.getName());
    }

}
icode commented 8 years ago

我现在在不同request请求中分别查询和更新,更新的时候并没有自动更新version字段,为啥?

2015-11-24 14:25 GMT+08:00 zhangshifeng notifications@github.com:

customer2.setName("c");

zhangshifeng commented 8 years ago

是不是查询时没有把版本字段查出来,如果是,之后的更新SQL中就不包含版本字段,也不执行乐观锁检查

我现在在不同request请求中分别查询和更新,更新的时候并没有自动更新version字段,为啥?

icode commented 8 years ago

我更新的时候没有查,直接进行的更新,是因为这个问题么?

在 2015年11月24日 下午4:37,zhangshifeng notifications@github.com写道:

是不是查询时没有把版本字段查出来,如果是,之后的更新SQL中就不包含版本字段,也不执行乐观锁检查

我现在在不同request请求中分别查询和更新,更新的时候并没有自动更新version字段,为啥?

— Reply to this email directly or view it on GitHub https://github.com/ebean-orm/avaje-ebeanorm/issues/459#issuecomment-159195739 .

zhangshifeng commented 8 years ago

如果是直接构建,应该仅更新赋了值的字段

icode commented 8 years ago

这是不是bug?乐观锁不应该始终存在么?难道我必须要先查一次? @zhangshifeng

zhangshifeng commented 8 years ago

@icode 这应该不是bug

这里的乐观锁检查应该是通过下面两步共同实现:先在where子句中加入版本条件,然后在更新后检查影响行数.

where子句中的版本条件只有在使用基于版本的并发控制时才添加,影响行数每次都检查.也就是说只有使用基于版本的并发控制时才进行乐观锁检查,否则只是影响行数检查.

使用并发控制与否,在执行更新时会最终确认(PersistRequestBean#determineConcurrencyMode),没有定义版本字段或没有加载版本值都会被认为不使用并发控制.

如果你想使用基于版本的并发控制进行乐观锁检查,就应该需要获取版本信息.

icode commented 8 years ago

就是必须要先查询一次是吧?

在 15/11/24,zhangshifengnotifications@github.com 写道:

@icode 这应该不是bug

这里的乐观锁检查应该是通过下面两步共同实现:先在where子句中加入版本条件,然后在更新后检查影响行数.

where子句中的版本条件只有在使用基于版本的并发控制时才添加,影响行数每次都检查.也就是说只有使用基于版本的并发控制时才进行乐观锁检查,否则只是影响行数检查.

使用并发控制与否,在执行更新时会最终确认(PersistRequestBean#determineConcurrencyMode),没有定义版本字段或没有加载版本值都会被认为不使用并发控制.

如果你想使用基于版本的并发控制进行乐观锁检查,就应该需要获取版本信息.


Reply to this email directly or view it on GitHub: https://github.com/ebean-orm/avaje-ebeanorm/issues/459#issuecomment-159245240

liuanxin commented 8 years ago

@icode 其实在理论上也能想得明白这个道理. 不管是 rdbms 的 乐观锁实现, 还是 nosql 的 safeUpdate 都需要将原值传进去. 而乐观锁最终也都是基于 cas 指令, 基于绝大多数 cpu 都已经实现了的汇编指令(intel 上的 cmpxchg 指令集)处锁很小段的代码(保证这一非常小段代码的原子性即可). java 的 synchronized 和 rdbms 的 for update 是基于其线程总线锁一大段并排斥竞争来实现, 而 concurrent 包和 乐观锁就是放开了竞争, 但空间小了

说白了就是时间换空间. 代价是需要原先的值, 也就是需要先 load 一遍(如果从页面上来, 会有被篡改的风险)

icode commented 8 years ago

这么多人用ebean哦,有群没?大家加一个联系方式呗?我的框架也在用ebean

在 2015年11月25日 上午9:37,liuanxin notifications@github.com写道:

@icode https://github.com/icode 其实在理论上也能想得明白这个道理. 不管是 rdbms 的 乐观锁实现, 还是 nosql 的 safeUpdate 都需要将原值传进去. 而乐观锁最终也都是基于 cas 指令, 基于绝大多数 cpu 都已经实现了的汇编指令(intel 上的 cmpxchg 指令集)处锁很小段的代码(保证这一非常小段代码的原子性即可). java 的 synchronized 和 rdbms 的 for update 是基于其线程总线锁一大段并排斥竞争来实现, 而 concurrent 包和 乐观锁就是放开了竞争, 但空间小了

说白了就是时间换空间. 代价是需要原先的值, 也就是需要先 load 一遍(如果从页面上来, 会有被篡改的风险)

— Reply to this email directly or view it on GitHub https://github.com/ebean-orm/avaje-ebeanorm/issues/459#issuecomment-159458671 .

liuanxin commented 8 years ago

@icode 没有群呢. 我这一般都是用 telgram 或 hangout

icode commented 8 years ago

@liuanxin 你不是大陆人?

liuanxin commented 8 years ago

@icode 是, 但是平时用 qq 极少

wenzhihong2003 commented 8 years ago

@icode , 看到了你的 ameba 框架, 很佩服你. 看你的框架应该受play1的影响比较大吧. 目前看来, 很多的java web框架都受到了play1的影响. java在做web方面只要是思想打开了, 还是有很多花样可以做的. https://github.com/actframework/ 这个也不错. 主要开发者也是一个在悉尼的华人(@greenlaw110)(不知道这样说对不对).