后端代码规范

前言

虽然oinone框架减少了很多的代码,但是低代码部分的代码质量也需要高度关注,不管是写的代码bug多,或者说被吐槽代码不行,还是说写的代码经常被重构,核心点还是没有代码规范的意识和技巧,下面摘录了一些常见的规范要求,去提高后端的代码质量,代码质量提高后,自然效率也会提升。

常见代码规范

**1、规范命名**

命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点:

**见名知意**

比如需要定义一个变量需要来计数

int i = 0;

名称 i 没有任何的实际意义,没有体现出数量的意思,所以我们应当指明数量的名称

int count = 0;
**能够读的出来**

如下代码:

private String sfzh;
private String dhhm;

这些变量的名称,根本读不出来,更别说实际意义了。

所以我们可以使用正确的可以读出来的英文来命名

private String idCardNo;
private String phone;

**2、规范代码格式**

好的代码格式能够让人感觉看起来代码更加舒适。

好的代码格式应当遵守以下几点:

  • 合适的空格
  • 代码对齐,比如大括号要对齐
  • 及时换行,一行不要写太多代码

好在现在开发工具支持一键格式化,可以帮助美化代码格式,大家统一使用idea的规范即可。

**3、写好代码注释**

在《代码整洁之道》这本书中作者提到了一个观点,注释的恰当用法是用来弥补我们在用代码表达意图时的失败。换句话说,当无法通过读代码来了解代码所表达的意思的时候,就需要用注释来说明。

书的作者之所以这么说,是因为作者觉得随着时间的推移,代码可能会变动,如果不及时更新注释,那么注释就容易产生误导,偏离代码的实际意义。而不及时更新注释的原因是,程序员不喜欢写注释。😒

但是这不意味着可以不写注释,当通过代码如果无法表达意思的时候,就需要注释,比如如下代码:

for (Integer id : ids) {
    if (id == 0) {
        continue;
    }
    //做其他事
}

为什么 id == 0 需要跳过,代码是无法看出来了,就需要注释了。

好的注释应当满足一下几点:

  • 解释代码的意图,说明为什么这么写,用来做什么
  • 对参数和返回值注释,入参代表什么,出参代表什么
  • 有警示作用,比如说入参不能为空,或者代码是不是有坑
  • 当代码还未完成时可以使用 todo 注释来标记
  • 代码review发现漏洞时 可以使用 fixme 注释来标记

**4、try catch 内部代码抽成一个方法**

try catch代码有时会干扰我们阅读核心的代码逻辑,这时就可以把try catch内部主逻辑抽离成一个单独的方法

如下图是Eureka服务端源码中服务下线的实现中的一段代码

后端代码规范

整个方法非常长,try中代码是真正的服务下线的代码实现,finally可以保证读锁最终一定可以释放。

所以这段代码其实就可以对核心的逻辑进行抽取。

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        doInternalCancel(appName, id, isReplication);
    } finally {
        read.unlock();
    }

    // 剩余代码
}

private boolean doInternalCancel(String appName, String id, boolean isReplication) {
    //真正处理下线的逻辑
}

**5、方法别太长**

方法别太长就是字面的意思。一旦代码太长,给人的第一眼感觉就很复杂,让人不想读下去;

同时方法太长的代码可能读起来容易让人摸不着头脑,不知道哪一些代码是同一个业务的功能。

比如代码中有那种2000+行大类,各种if else判断,光理清代码思路就需要用很久时间。🤷🏻‍♀️

所以一旦方法过长,可以尝试将相同业务功能的代码单独抽取一个方法,最后在主方法中调用即可。

**6、抽取重复代码**

当一份代码重复出现在程序的多处地方,就会造成程序又臭又长,当这份代码的结构要修改时,每一处出现这份代码的地方都得修改,导致程序的扩展性很差。

所以一般遇到这种情况,可以抽取成一个工具类,还可以抽成一个公共的父类。

**7、多用return**

在有时我们平时写代码的情况可能会出现if条件套if的情况,当if条件过多的时候可能会出现如下情况:

if (条件1) {
    if (条件2) {
        if (条件3) {
            if (条件4) {
                if (条件5) {
                    System.out.println("11111");
                }
            }
        }
    }
}

面对这种情况,可以换种思路,使用return来优化

if (!条件1) {
    return;
}
if (!条件2) {
    return;
}
if (!条件3) {
    return;
}
if (!条件4) {
    return;
}
if (!条件5) {
    return;
}
System.out.println("11111");

这样优化就感觉看起来更加直观

**8、if条件表达式不要太复杂**

比如在如下代码:

if (((StringUtils.isBlank(person.getName())
        || "11111".equals(person.getName()))
        && (person.getAge() != null && person.getAge() > 10))
        && "汉".equals(person.getNational())) {
    // 处理逻辑
}

这段逻辑,这种条件表达式乍一看不知道是什么,仔细一看还是不知道是什么,这时就可以这么优化

boolean sanyouOrBlank = StringUtils.isBlank(person.getName()) || "11111".equals(person.getName());
boolean ageGreaterThanTen = person.getAge() != null && person.getAge() > 10;
boolean isHanNational = "汉".equals(person.getNational());

if (sanyouOrBlank
    && ageGreaterThanTen
    && isHanNational) {
    // 处理逻辑
}

此时就很容易看懂if的逻辑了

**9、优雅地参数校验**

当前端传递给后端参数的时候,通常需要对参数进场检验,一般可能会这么写

public void addPerson(ModelA add) {
    if (StringUtils.isBlank(add.getName())) {
        throw new BizException("人员姓名不能为空");
    }

    if (StringUtils.isBlank(add.getIdCardNo())) {
        throw new BizException("身份证号不能为空");
    }

    // 处理新增逻辑
}

这种写虽然可以,但是当字段的多的时候,光校验就占据了很长的代码,不够优雅。

针对参数校验这个问题,oinone其实提供了一些方案:

模型校验:模型之元数据详解,可以通过check

动作校验:Action之校验,可以通过@Validation 注解进行校验

其次:可以抽通用的校验Utils,进行校验的归集

**10、统一返回值**

及时我们有GraphQL统一了一层默认返回参数,但是涉及到移动端或者特殊Action的时候,后端在设计Action的时候,最好还是统一返回值。

{  
    "code":0,
    "message":"成功",
    "data":"返回数据"
}

不仅是给前端参数,也包括提供给第三方的接口等,这样接口调用方法可以按照固定的格式解析代码,不用进行判断。如果不一样,相信我,前端半夜都一定会来找你。

oinone中定义了很多的基类模型,可以用于统一回参的规范。

**11、统一异常处理**

如果系统需要做异常码的规范和翻译,那统一异常码是必须要做的事情,比如一个跨境系统,报错中文,其实就是个灾难,所以大家还是统一使用PamisException进行异常抛出。

public ModelA addPerson(Model modelA) {
  Model a = xxxxService.queryById(modelA.getId());
  if(a == null){
    throw PamirsException.construct(ModuleExpEnumerate.XXX_ERROR).errThrow();
  }
  return modelA;
}

当异常统一后,可以使用AOP进行统一的异常处理和优化,或者业务实现。

**12、尽量不传递null值**

这个很好理解,不传null值可以避免方法不支持为null入参时产生的空指针问题。

当然为了更好的表明该方法是不是可以传null值,可以通过@NonNull和@Nullable注解来标记。@NonNull就表示不能传null值,@Nullable就是可以传null值。

//示例1
public void updatePerson(@Nullable Person person) {
    if (person == null) {
        return;
    }
    personService.updateById(person);
}

//示例2
public void updatePerson(@NonNull Person person) {
    personService.updateById(person);
}

**13、尽量不返回null值**

尽量不返回null值是为了减少调用者对返回值的为null判断,如果无法避免返回null值,可以通过返回Optional去判断下。

public List<T> getPersonById(Long personId) {
    return Optional.ofNullable(personService.selectById(personId)).orElse(new ArrayList<>());
}

如果不想这么写,也可以通过@NonNull和@Nullable表示方法会不会返回null值。

**14、日志打印规范**

好的日志打印能帮助我们快速定位问题

好的日志应该遵循以下几点:

  • 可搜索性,要有明确的关键字信息
  • 异常日志需要打印出堆栈信息
  • 合适的日志级别,比如异常使用error,正常使用info
  • 日志内容太大不打印,比如有时需要将图片转成Base64,那么这个Base64就可以不用打印,如果只是为了调试某个功能,可以尝试使用debug日志,测试环境可以指定logback打印某些package的打印debug级别日志

**15、统一类库**

在一个项目中,可能会由于引入的依赖不同导致引入了很多相似功能的类库,比如常见的json类库,又或者是一些常用的工具类,当遇到这种情况下,应当规范在项目中到底应该使用什么类库,而不是一会用JsonUtils,一会使用JsonObject,我们可以统一使用Oinone的类库作为我们的使用标准。

**16、尽量使用工具类**

比如在对集合判空的时候,可以这么写

public void updatePersons(List<Person> persons) {
    if (persons != null && persons.size() > 0) {

    }
   persons.stream().map(Person::getId).collect(Collectors.toSet());
}

但是一般不推荐这么写,可以通过一些判断的工具类来写,可以统一使用工具了去进行集合或者Map的处理,可以参考:

AriesCollectionUtils�

public void updatePersons(List<Person> persons) {
    if (CollectionUtils.isNotEmpty(persons)) {

    }
    Map<Long,Person> personMap = AriesCollectionUtils.toMap(persons,Person::getId);
}

不仅集合,比如字符串的判断等等,就使用工具类,不要手动判断。

**17、尽量不要重复造轮子**

就拿格式化日期来来说,我们一般封装成一个工具类来调用,比如如下代码

private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String formatDateTime(Date date) {
    return DATE_TIME_FORMAT.format(date);
}

这段代码看似没啥问题,但是却忽略了SimpleDateFormat是个线程不安全的类,所以这就会引起坑。

一般对于这种已经有开源的项目并且已经做得很好的时候,比如平台底层的DateUtils,就可以把轮子直接拿过来用了。

**18、类和方法单一职责**

单一职责原则是设计模式的七大设计原则之一,它的核心意思就是字面的意思,一个类或者一个方法只做单一的功能。

就拿Nacos来说,在Nacos1.x的版本中,有这么一个接口HttpAgent

后端代码规范

这个类只干了一件事,那就是封装http请求参数,向Nacos服务端发送请求,接收响应,这其实就是单一职责原则的体现。

当其它的地方需要向Nacos服务端发送请求时,只需要通过这个接口的实现,传入参数就可以发送请求了,而不需要关心如何携带服务端鉴权参数、http请求参数如何组装等问题。

**19、继承不要太深**

继承的弊端:

  • 灵活性低。java语言是单继承的,无法同时继承很多类,并且继承容易导致代码层次太深,不易于维护
  • 耦合性高。一旦父类的代码修改,可能会影响到子类的行为

所以一般推荐使用聚合/组合代替继承。

聚合/组合的意思就是通过成员变量的方式来使用类。

比如说,OrderService需要使用UserService,可以注入一个UserService而非通过继承UserService。

聚合和组合的区别就是,组合是当对象一创建的时候,就直接给属性赋值,而聚合的方式可以通过set方式来设置。

组合:

public class OrderService {

    private UserService userService = new UserService();

}

聚合:

public class OrderService {

    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

现实写代码的时候,其实使用@Autowired�也可以。

public class OrderService {

    @Autowired
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

**20、使用设计模式优化代码**

在平时开发中,使用设计模式可以增加代码的扩展性。

比如说,当你需要做一个可以根据不同的平台做不同消息推送的功能时,就可以使用策略模式的方式来优化。

设计一个接口:

public interface MessageNotifier {

    /**
     * 是否支持改类型的通知的方式
     *
     * @param type 0:短信 1:app
     * @return
     */
    boolean support(int type);

    /**
     * 通知
     *
     * @param user
     * @param content
     */
    void notify(User user, String content);

}

短信通知实现:

@Component
public class SMSMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 0;
    }

    @Override
    public void notify(User user, String content) {
        //调用短信通知的api发送短信
    }
}

app通知实现:

public class AppMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 1;
    }

    @Override
    public void notify(User user, String content) {
       //调用通知app通知的api
    }
}

最后提供一个方法,当需要进行消息通知时,调用notifyMessage,传入相应的参数就行。

@Autowired
private List<MessageNotifier> messageNotifiers;

public void notifyMessage(User user, String content, int notifyType) {
    for (MessageNotifier messageNotifier : messageNotifiers) {
        if (messageNotifier.support(notifyType)) {
            messageNotifier.notify(user, content);
        }
    }
}

假设此时需要支持通过邮件通知,只需要有对应实现就行,这里就可以使用【工厂模式】去协助处理不同的发送者。

**21、不滥用设计模式**

用好设计模式可以增加代码的扩展性,但是滥用设计模式确是不可取的。

public void printPerson(Person person) {
    StringBuilder sb = new StringBuilder();
    if (StringUtils.isNotBlank(person.getName())) {
        sb.append("姓名:").append(person.getName());
    }
    if (StringUtils.isNotBlank(person.getIdCardNo())) {
        sb.append("身份证号:").append(person.getIdCardNo());
    }

    // 省略
    System.out.println(sb.toString());
}

比如上面打印Person信息的代码,用if判断就能够做到效果,你说我要不用责任链或者什么设计模式来优化一下吧,没必要。

**22、面向接口编程**

在一些可替换的场景中,应该引用父类或者抽象,而非实现。

举个例子,在实际项目中可能需要对一些图片进行存储,但是存储的方式很多,比如可以选择阿里云的OSS,又或者是七牛云,MINIO存储服务器等等。所以对于存储图片这个功能来说,这些具体的实现是可以相互替换的。

所以在项目中,我们不应当在代码中耦合一个具体的实现,而是可以提供一个存储接口

public interface FileStorage {

    String store(String fileName, byte[] bytes);

}

如果选择了阿里云OSS作为存储服务器,那么就可以基于OSS实现一个FileStorage,在项目中哪里需要存储的时候,只要实现注入这个接口就可以了。

@Autowired
private FileStorage fileStorage;

假设用了一段时间之后,发现阿里云的OSS比较贵,此时想换成七牛云的,那么此时只需要基于七牛云的接口实现FileStorage接口,然后注入到IOC,那么原有代码用到FileStorage根本不需要动,实现轻松的替换。

**23、经常重构旧的代码**

随着时间的推移,业务的增长,有的代码可能不再适用,或者有了更好的设计方式,那么可以及时的重构业务代码。

就拿上面的消息通知为例,在业务刚开始的时候可能只支持短信通知,于是在代码中就直接耦合了短信通知的代码。但是随着业务的增长,逐渐需要支持app、邮件之类的通知,那么此时就可以重构以前的代码,抽出一个策略接口,进行代码优化。

重构过程中,不要通过注释代码的方式去做逻辑保留,会导致代码可读性更差,建议直接删除,调整代码结构,保证代码简洁清晰。

**24、null值判断**

空指针是代码开发中的一个难题,作为程序员的基本修改,应该要防止空指针。

可能产生空指针的原因:

  • 数据返回对象为null
  • 自动拆箱导致空指针
  • 调用方法返回的对象可能为空值

所以在需要这些的时候,需要强制判断是否为null。前面也提到可以使用Optional来优雅地进行null值判断。

**25、模型类充血模式**

针对于很多场景,希望做到模型类的统一处理变化,可以写一些充血方法,因为平台底层重写了lombok插件,所以重写模型的方法时候,需要去class中找到具体的get、set方法,例如:

public class ModelA {

  private String salesOrderId;

  public Long getSalesOrderId() {
    Object obj = this._d.get("salesOrderId");
    Long salesOrderId = obj == null ? null : Long.valueOf(obj.toString());
    //xxxx 做自己的逻辑
    return salesOrderId;
  }
}

**26、魔法值用常量表示**

public void sayHello(String province) {
    if ("广东省".equals(province)) {
        System.out.println("靓仔~~");
    } else {
        System.out.println("帅哥~~");
    }
}

代码里,广东省就是一个魔法值,那么就可以将用一个常量来保存

private static final String GUANG_DONG_PROVINCE = "广东省";

public void sayHello(String province) {
    if (GUANG_DONG_PROVINCE.equals(province)) {
        System.out.println("靓仔~~");
    } else {
        System.out.println("帅哥~~");
    }
}

**27、资源释放写到finally**

比如在使用一个api类锁或者进行IO操作的时候,需要主动写代码需释放资源,为了能够保证资源能够被真正释放,那么就需要在finally中写代码保证资源释放。

后端代码规范

如图所示,就是CopyOnWriteArrayList的add方法的实现,最终是在finally中进行锁的释放。

或者统一使用:JDK7 新特性之try-with-resources-CSDN博客

**28、使用线程池代替手动创建线程**

使用线程池还有以下好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

所以为了达到更好的利用资源,提高响应速度,就可以使用线程池的方式来代替手动创建线程。

如果对线程池不清楚的同学,可以看一下这篇文章:7000字+24张图带你彻底弄懂线程池

一个项目中最好是统一维护线程池的定义,不要出现线程池散落的情况。

**29、线程设置名称**

在日志打印的时候,日志是可以把线程的名字给打印出来。

后端代码规范

如上图,日志打印出来的就是tom猫的线程。

所以,设置线程的名称可以帮助我们更好的知道代码是通过哪个线程执行的,更容易排查问题。

**30、涉及线程间可见性加volatile**

在RocketMQ源码中有这么一段代码

后端代码规范

在消费者在从服务端拉取消息的时候,会单独开一个线程,执行while循环,只要stopped状态一直为false,那么就会一直循环下去,线程就一直会运行下去,拉取消息。

当消费者客户端关闭的时候,就会将stopped状态设置为true,告诉拉取消息的线程需要停止了。但是由于并发编程中存在可见性的问题,所以虽然客户端关闭线程将stopped状态设置为true,但是拉取消息的线程可能看不见,不能及时感知到数据的修改,还是认为stopped状态设置为false,那么就还会运行下去。

针对这种可见性的问题,java提供了一个volatile关键字来保证线程间的可见性。

后端代码规范

所以,源码中就加了volatile关键字。

加了volatile关键字之后,一旦客户端的线程将stopped状态设置为true时候,拉取消息的线程就能立马知道stopped已经是false了,那么再次执行while条件判断的时候,就不成立,线程就运行结束了,然后退出。

**31、考虑线程安全问题**

大家掌握多线程增加处理效率后,其实同时也会带来线程安全问题,比如ArrayList就是一个非线程安全的集合,多线程处理该集合数据会出现数据错乱的问题,所以我们要知道一些线程安全的类库。

例如 ArrayList就可以替换为CopyOnWriteArrayListCollections.synchronizedList() 方法去定义集合。

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

List<Integer> list = Collections.synchronizedList(new ArrayList<>());

所以,在实际中,需要多考虑考虑业务是否有线程安全问题,有集合读写安全问题,那么就用线程安全的集合,业务有安全的问题,那么就可以通过加锁的手段来解决。

**32、慎用异步**

虽然在使用多线程可以帮助我们提高接口的响应速度,但是也会带来很多问题。

事务问题

一旦使用了异步,就会导致两个线程不是同一个事务的,导致异常之后无法正常回滚数据。

cpu负载过高

之前有个小伙伴遇到需要同时处理几万调数据的需求,每条数据都需要调用很多次接口,为了达到老板期望的时间要求,使用了多线程跑,开了很多线程,此时会发现系统的cpu会飙升

意想不到的异常

还是上面的提到的例子,在测试的时候就发现,由于并发量激增,在请求第三方接口的时候,返回了很多错误信息,导致有的数据没有处理成功。

虽然说慎用异步,但不代表不用,如果可以保证事务的问题,或是CPU负载不会高的话,那么还是可以使用的。

消息队列(MQ)或者 Oinone的触发任务也是一个异步的解决方案:函数之触发与定时

**33、减小锁的范围**

减小锁的范围就是给需要加锁的代码加锁,不需要加锁的代码不要加锁。这样就能减少加锁的时间,从而可以较少锁互斥的时间,提高效率。

后端代码规范

比如CopyOnWriteArrayList的addAll方法的实现,lock.lock(); 代码完全可以放到代码的第一行,但是作者并没有,因为前面判断的代码不会有线程安全的问题,不放到加锁代码中可以减少锁抢占和占有的时间。

**34、有类型区分时定义好枚举**

比如在项目中不同的类型的业务可能需要上传各种各样的附件,此时就可以定义好不同的一个附件的枚举,来区分不同业务的附件。

不要在代码中直接写死,不定义枚举,代码阅读起来非常困难,直接看到数字都是懵逼的。。

**35、集合使用应当指明初始化大小**

比如在写代码的时候,经常会用到List、Map来临时存储数据,其中最常用的就是ArrayList和HashMap。但是用不好可能也会导致性能的问题。

比如说,在ArrayList中,底层是基于数组来存储的,数组是一旦确定大小是无法再改变容量的。但不断的往ArrayList中存储数据的时候,总有那么一刻会导致数组的容量满了,无法再存储其它元素,此时就需要对数组扩容。所谓的扩容就是新创建一个容量是原来1.5倍的数组,将原有的数据给拷贝到新的数组上,然后用新的数组替代原来的数组。

在扩容的过程中,由于涉及到数组的拷贝,就会导致性能消耗;同时HashMap也会由于扩容的问题,消耗性能。所以在使用这类集合时可以在构造的时候指定集合的容量大小。

**36、尽量不要使用BeanUtils来拷贝属性**

在开发中经常需要对JavaBean进行转换,但是又不想一个一个手动set,比较麻烦,所以一般会使用属性拷贝的一些工具,比如说Spring提供的BeanUtils来拷贝。不得不说,使用BeanUtils来拷贝属性是真的舒服,使用一行代码可以代替几行甚至十几行代码,我也喜欢用。

但是喜欢归喜欢,但是会带来性能问题,因为底层是通过反射来的拷贝属性的,所以尽量不要用BeanUtils来拷贝属性。

比如你可以装个generateallsetter插件转换的插件,帮你自动生成转换代码;

后端代码规范

或者使用ArgUtils针对模型处理,会更方便去处理;

或者可以使用性能更高的MapStruct来进行JavaBean转换,MapStruct底层是通过调用(settter/getter)来实现的,而不是反射来快速执行。

**37、使用StringBuilder进行字符串拼接**

如下代码:

String str1 = "123";
String str2 = "456";
String str3 = "789";
String str4 = str1 + str2 + str3;

使用 + 拼接字符串的时候,会创建一个StringBuilder,然后将要拼接的字符串追加到StringBuilder,再toString,这样如果多次拼接就会执行很多次的创建StringBuilder,z执行toString的操作。

所以可以手动通过StringBuilder拼接,这样只会创建一次StringBuilder,效率更高。

StringBuilder sb = new StringBuilder();
String str = sb.append("123").append("456").append("789").toString();

**38、使用编程式事务**

使用注解代码省了大家很多事情,但是代码层级太深时候,对于事务处理来说就需要看大量的代码去,很难去发现事务不一致的情况,对于性能也是一个灾难。

@Transactional一时爽,性能优化就是个灾难!

Tx.build(new TxConfig().setPropagation(Propagation.REQUIRED.value())).executeWithoutResult(status -> {

    //逻辑代码
});

**39、大事务拆小事务**

事务会导致很直接的性能问题,例如锁表、锁行等情况,代码执行就会很慢,所以我们可以通过编程式事务的写法控制事务包裹的代码内容,同时也可以通过new TxConfig().setPropagation(Propagation.REQUIRED.value()))�,去控制事务的传播行为,让事务能够快速结束。

比如查询之类准备数据的可以放在事务外处理,其次用了非强一致事务要求的,可以用Propagation.REQUIRED,去用新的事物处理,保障事物处理快。

List<T> data = xxxService.select(xx);
Tx.build(new TxConfig().setPropagation(Propagation.REQUIRED.value())).executeWithoutResult(status -> {
    data.updateById();                                                                                    
});

**40、谨慎方法内部调用动态代理的方法**

如下事务代码

@Service
public class PersonService {

    public void update(Person person) {
        // 处理
        updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person) {
        // 处理
    }

}

update调用了加了@Transactional注解的updatePerson方法,那么此时updatePerson的事务就是失效。

其实失效的原因不是事务的锅,是由AOP机制决定的,因为事务是基于AOP实现的。AOP是基于对象的代理,当内部方法调用时,走的不是动态代理对象的方法,而是原有对象的方法调用,如此就走不到动态代理的代码,就会失效了。

如果实在需要让动态代理生效,可以注入自己的代理对象

@Service
public class PersonService {

    @Autowired
    private PersonService personService;

    public void update(Person person) {
        // 处理
        personService.updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person) {
        // 处理
    }

}

**41、不循环调用数据库**

不要在循环中访问数据库,这样会严重影响数据库性能。

比如需要查询一批人员的信息,人员的其他信息存在其他表中,错误的代码如下:

public List<Person> selectPersons(List<Long> personIds) {
  List<Person> persons = new Person().queryByIds(personIds);
  for (Person person : persons) {
    PersonOther personOther = new PersonOther().queryOtherById(person.getId());
    // 组装数据
    person.setOther(personOther);
  }
  return persons;
}

正确的写法是查询所有的数据,通过map在for循环中set值:

public List<Person> selectPersons(List<Long> personIds) {
  List<Person> persons = new Person().queryByIds(personIds);
  List<PersonOther> personOthers = new PersonOther().queryByPersonIds(personIds);
  Map<Long,PersonOther> otherMap = AriesCollectionUtils.toMap(personOthers,PersonOther::getPersonId); 
  for (Person person : persons) {
    // 组装数据
    person.setOther(otherMap.get(person.getId()));
  }
  return persons;
}

**42、需要什么字段select什么字段**

查询全字段有以下几点坏处:

**增加不必要的字段的网络传输**

比如有些文本的字段,存储的数据非常长,但是本次业务使用不到,但是如果查了就会把这个数据返回给客户端,增加了网络传输的负担

会导致无法使用到覆盖索引

比如说,现在有身份证号和姓名做了联合索引,现在只需要根据身份证号查询姓名,如果直接select name 的话,那么在遍历索引的时候,发现要查询的字段在索引中已经存在,那么此时就会直接从索引中将name字段的数据查出来,返回,而不会继续去查找聚簇索引,减少回表的操作。

所以建议是需要使用什么字段查询什么字段。oinone中的Wrapper支持这样的写法,如下:

LambdaQueryWrapper<AriesCompany> wrapper = Pops.<AriesCompany>lambdaQuery()
  .from(AriesCompany.MODEL_MODEL)
  .select(AriesCompany::getId)
  .eq(AriesCompany::getId, data.getId());

**43、用业务代码代替多表join**

如上面代码所示,原本也可以将两张表根据人员的id进行关联查询。但是不推荐这么,阿里也禁止多表join的操作

后端代码规范

而之所以会禁用,是因为join的效率比较低。

MySQL是使用了嵌套循环的方式来实现关联查询的,也就是for循环会套for循环的意思。用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出,这种效率肯定低。

其次,join对分表也是种伤害,慎用!

**44、装上阿里代码检查插件**

我们平时写代码由于各种原因,比如项目紧张、功能重要,一直催进度,导致写代码都来不及思考,怎么快怎么来,cv大法上线,虽然有心想写好代码,但是手确不听使唤。所以建议装一个阿里的代码规范插件,如果有代码不规范,会有提醒,这样就可以知道哪些是可以优化的了。

后端代码规范

如果你有强迫症,相信我,装了这款插件,你的代码会写的很漂亮。

后端代码规范

**45、及时跟团队沟通**

写代码的时候不能闭门造车,及时跟团队的同事们沟通,比如Oinone的一些特性你不知道,一些技术方案不了解,一些特殊逻辑不清晰,如果上来就直接写代码,很有可能就会踩坑。

:: 代码写的漂亮,也一定会变得更受欢迎,希望大家都成为受人喜爱和尊敬的程序猿**👨🏻‍💻**::

Oinone社区 作者:冯, 天宇原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/19837.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(3)
冯, 天宇的头像冯, 天宇数式员工
上一篇 2024年12月11日 am10:42
下一篇 2024年12月11日 pm2:57

相关推荐

  • 前端日期组件国际化支持方案

    在 oinone 平台中,系统默认支持基础的国际化翻译功能。但由于日期时间组件的国际化依赖对应语言包,而全量引入语言包会显著增加打包体积,因此前端默认仅集成了中、英文的日期时间支持。若需为日期时间组件扩展其他语言(如日语)的国际化支持,需手动导入对应语言包并完成配置,具体步骤如下: 假设我们现在国际化翻译切换成了日语,那么我们在日期时间也要支持日语,那么需要如下操作: 1: 重写 RootWidget 继承平台默认的 RootWidget,SPI 注册条件保持跟平台一致即可覆盖平台默认的RootWidget // CustomRootWidget.ts import { RootComponentSPI, RootWidget, SPIFactory } from '@oinone/kunlun-dependencies'; import Root from './Root.vue'; // 通过SPI注册覆盖平台默认的root组件 @SPIFactory.Register(RootComponentSPI.Token({ widget: 'root' })) export class CustomRootWidget extends RootWidget { public initialize() { super.initialize(); this.setComponent(Root); return this; } } 2: 覆盖 Root 组件的 Vue 文件 自定义的 Vue 文件需负责导入目标语言(如日语)的语言包,并根据当前语言环境动态切换配置。这里需要同时处理 ant-design-vue、element-plus 组件库及 dayjs 工具的语言包,确保日期组件的展示和交互统一适配目标语言。 <!– Root.vue –> <template> <a-config-provider :locale="antLocale"> <el-config-provider :locale="eleLocale"> <match :rootToken="root"> <template v-for="page in pages" :key="page.widget"> <route v-if="page.widget" :path="page.path" :slotName="page.slotName" :widget="page.widget"> <slot :name="page.slotName" /> </route> </template> <route :path="pagePath" slotName="page" :widgets="{ page: widgets.page }"> <slot name="page" /> </route> <route path="/" slotName="homePage"> <slot name="homePage" /> </route> </match> </el-config-provider> </a-config-provider> </template> <script lang="ts"> import { CurrentLanguage, EN_US_CODE, UrlHelper, ZH_CN_CODE } from '@oinone/kunlun-dependencies'; import { ConfigProvider as AConfigProvider } from 'ant-design-vue'; import { ElConfigProvider } from 'element-plus'; import dayjs from 'dayjs'; // 导入ant-design-vue语言包 import enUS from 'ant-design-vue/es/locale/en_US'; import zhCN from 'ant-design-vue/lib/locale/zh_CN'; import jaJP from 'ant-design-vue/lib/locale/ja_JP'; // 新增:日语语言包 // 导入 dayjs的语言包 import 'dayjs/locale/zh-cn'; import 'dayjs/locale/ja'; // 新增:日语语言包 // 导入element-plus语言包 import elEn from 'element-plus/dist/locale/en.mjs'; import elZhCn from 'element-plus/dist/locale/zh-cn.mjs'; import elJaJP from 'element-plus/dist/locale/ja.mjs'; // 新增:日语语言包 import { computed, defineComponent, onMounted,…

    2025年8月13日
    69100
  • Oinone环境保护(v5.2.3以上)

    概述 Oinone平台为合作伙伴提供了环境保护功能,以确保在一套环境可以在较为安全前提下修改配置文件,启动多个JVM等部署操作。 本章内容主要介绍与环境保护功能相关的启动参数。 名词解释 本地开发环境:开发人员在本地启动业务工程的环境 公共环境:包含设计器镜像和业务工程的环境 环境保护参数介绍 【注意】参数是加在程序实参 (Program arguments)上,通常可能错误的加在Active Profiles上了 -PenvProtected=${value} 是否启用环境保护,默认为true。 环境保护是通过与最近一次保存在数据库的base_platform_environment表中数据进行比对,并根据每个参数的配置特性进行判断,在启动时将有错误的内容打印在启动日志中,以便于开发者进行问题排查。 除此之外,环境保护功能还提供了一些生产配置的优化建议,开发者可以在启动时关注这些日志,从而对生产环境的配置进行调优。 -PsaveEnvironments=${value} 是否将此次启动的环境参数保存到数据库,默认为true。 在某些特殊情况下,为了避免公共环境中的保护参数发生不必要的变化,我们可以选择不保存此次启动时的配置参数到数据库中,这样就不会影响其他JVM启动时发生校验失败而无法启动的问题。 -PstrictProtected=${value} 是否使用严格校验模式,默认为false 通常我们建议在公共环境启用严格校验模式,这样可以最大程度的保护公共环境的元数据不受其他环境干扰。 PS:在启用严格校验模式时,需避免内外网使用不同连接地址的场景。如无法避免,则无法启用严格校验模式。 常见问题 需要迁移数据库,并更换了数据库连接地址该如何操作? 将原有数据库迁移到新数据库。 修改配置文件中数据库的连接地址。 在启动脚本中增加-PenvProtected=false关闭环境保护。 启动JVM服务可以看到有错误的日志提示,但不会中断本次启动。 移除启动脚本中的-PenvProtected=false或将值改为true,下次启动时将继续进行环境保护检查。 可查看数据库中base_platform_environment表中对应数据库连接配置已发生修改,此时若其他JVM在启动前未正确修改,则无法启动。 本地开发时需要修改Redis连接地址到本地,但希望不影响公共环境的使用该如何操作? PS:由于Redis中的元数据缓存是根据数据库差量进行同步的,此操作会导致公共环境在启动时无法正确刷新Redis中的元数据缓存,需要配合pamirs.distribution.session.allMetaRefresh参数进行操作。如无特殊必要,我们不建议使用该形式进行协同开发,多次修改配置会导致出错的概率增加。 本地环境首次启动时,除了修改Redis相关配置外,还需要配置pamirs.distribution.session.allMetaRefresh=true,将本地新连接的Redis进行初始化。 在本地启动时,增加-PenvProtected=false -PsaveEnvironments=false启动参数,以确保本地启动不会修改公共环境的配置,并且可以正常通过环境保护检测。 本地环境成功启动并正常开发功能后,需要发布到公共环境进行测试时,需要先修改公共环境中业务工程配置pamirs.distribution.session.allMetaRefresh=true后,再启动业务工程。 启动一次业务工程后,将配置还原为pamirs.distribution.session.allMetaRefresh=false。

    2024年10月21日
    1.4K00
  • 自定义用户中心菜单

    使用扩展点实现用户中心菜单替换 1. 工程中引起pamirs-user-api <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-user-api</artifactId> </dependency> 2. 实现TopBarUserBlockAction的后置扩展 实现HookAfter后置扩展接口 @Hook(model = {TopBarUserBlock.MODEL_MODEL}, fun = {"construct"})添加Hook注解注明是TopBarUserBlock模型的construct函数的后置扩展。 增加用户中心菜单 @Component @Order(1) @SPI.Service public class MyTopBarActionExt implements TopBarActionExtendApi { public void edit(List<TopBarAction> list) { list.add(new TopBarAction("top_demo", "top.Teacher", "uiView9a0caf1d574a42c9847a057a0c4a4ad1", ActionTypeEnum.VIEW, 1) .setDisplayName("商品管理") .setIcon("oinone-gongzuotai") ); } } 实现效果 替换原有的用户中心菜单 替换原有的菜单跳转 @Component public class DemoTopBarUserBlockDataHookAfter implements HookAfter { @Override @Hook(model = {TopBarUserBlock.MODEL_MODEL}, fun = {"construct"}) public Object run(Function function, Object ret) { if (ret == null) { return null; } TopBarUserBlock result = null; if (ret instanceof Object[]) { Object[] rets = (Object[]) ((Object[]) ret); if (rets.length == 1) { result = (TopBarUserBlock) rets[0]; //例:替换用户中心:修改密码菜单 //使用name和model查询出模型的ViewAction替换修改密码ViewAction ViewAction demoViewAction = PamirsSession.getContext().getExtendCache(ActionCacheApi.class).get(Dog.MODEL_MODEL, "changePassword"); //设置菜单的icon Map<String, Object> attributes = Optional.ofNullable(demoViewAction.getAttributes()).orElse(new HashMap<>()); attributes.put("icon", "oinone-xiugaimima"); demoViewAction.setAttributes(attributes); //UserViewAction第0个是修改密码ViewAction,使用自定义的ViewAction就可以实现替换 result.getUserViewAction().set(0, demoViewAction); } } else { result = (TopBarUserBlock) ret; } return result; } } 使用@UxRouteButton方式新增ViewAction,更多新增Action方式详见:Action的类型 @Model.model(Dog.MODEL_MODEL) @Component @UxRouteButton( action = @UxAction(name = "changePassword", displayName = "修改密码"), value = @UxRoute(model = Dog.MODEL_MODEL, openType = ActionTargetEnum.DIALOG)) public class DogAction { } 替换原有的个人设置头像跳转 修改点击头像绑定的跳转逻辑 @Order(10) @Component @SPI.Service public class DemoTopBarUserBlockDataApi implements TopBarUserBlockDataApi { @Override public TopBarUserBlock extendData(TopBarUserBlock data) { //例如增加一个菜单, PamirsDemo.MODEL_MODEL: 模型。 MenuuiMenu31f22466735a4abe8e0544b428ed88ac:viewAction的name。 Action demoViewAction =…

    2024年7月4日
    1.7K00
  • Oinone研发规范:元数据定义篇

    模型字段规范 配置注解 模型类必需使用@Model注解来标识当前类为模型类。可以使用@Model.model、@Fun注解模型的模型编码(也表示命名空间),先取@Model.model注解值,若为空则取@Fun注解值,若皆为空则取全限定类名。使用@Model.model注解配置模型编码,模型编码唯一标识一个模型。请勿使用Query和Mutation作为模型编码和技术名称的结尾。更多Model的详细元数据描述介绍参见模型元数据详解 模型命名规范 模型属性 默认取值规范 命名规则规范 name 默认取model.model的点分割最后一位 1. 使用大驼峰命名法 2. 仅支持数字、字母 3. 必须以字母开头 4. 长度必须小于等于128个字符 module 无默认值开发人员定义规范示例:{项目名称}_{模块功能示意名称} 1. 使用下划线命名法 2. 仅支持数字、大写或小写字母、下划线 3. 必须以字母开头4. 不能以下划线结尾 5. 长度必须小于等于128个字符 model 默认使用全类名,取lname的值开发人员定义规范示例:{项目名称}.{模功能示意名称}.{简单类名} 1. 仅支持数字、字母、点 2. 必须以字母开头 3. 不能以点结尾 4. 长度必须小于等于128个字符 displayName 空字符串 1. 长度必须小于等于128个字符 lname 符合java命名规范,真实的java全类名,无法指定,要符合model的约束,即为包名+类名 lname是不能定义的,为全类名:包名+类名,和model一样的校验规则:包名和类名的校验 summary 默认使用displayName属性 1. 不能使用分号 2. 长度必须小于等于128个字符 descripition NULL,注解无法定义 1. 长度必须小于等于65535个字符 table 默认使用name字段生成表名时,table字段的命名规则约束同样生效(大小驼峰命名转为下划线分割的表名称) 1. 仅支持数字、字母、下划线 2. 长度必须小于等于128个字符(此限制为系统存储约束,与数据库本身无关) type java属性类型与数据库存储类型可执行转换即可 ModelTypeEnum枚举值 字段命名规范 字段属性 默认取值规范 命名规则规范 name 默认使用java属性名 1. 仅支持数字、字母 2. 必须以小写字母开头 3. 长度必须小于等于128个字符 field 默认使用java属性名 与name使用相同命名规则约束 displayName 默认使用name属性 1. 长度必须小于等于128个字符 lname 使用java属性名,符合java命名规范,真实的属性名称,无法指定 与name使用相同命名规则约束 column 列名为属性名的小驼峰转下划线格式 1. 仅支持数字、字母、下划线 2. 长度必须小于等于128个字符(此限制为系统存储约束,与数据库本身无关) summary 默认使用displayName属性 1. 不能使用分号 2. 长度必须小于等于500个字符 模型继承规范 继承方式 继承方式可以分为五种: 抽象基类ABSTRACT,只保存不希望为每个子模型重复键入的信息的模型,抽象基类模型不生成数据表存储数据,只供其他模型继承模型可继承域使用,抽象基类可以继承抽象基类。 扩展继承EXTENDS,子模型与父模型的数据表相同,子模型继承父模型的字段与函数。存储模型之间的继承默认为扩展继承。 多表继承MULTI_TABLE,父模型不变,子模型获得父模型的可继承域生成新的模型;父子模型不同表,子模型会建立与父模型的一对一关联关系字段(而不是交叉表),使用主键关联,同时子模型会通过一对一关联关系引用父模型的所有字段。多表继承父模型需要使用@Model.MultiTable来标识,子模型需要使用@Model.MultiTableInherited来标识。 代理继承PROXY,为原始模型创建代理,可以增删改查代理模型的实体数据,就像使用原始(非代理)模型一样。不同之处在于代理继承并不关注更改字段,可以更改代理中的元信息、函数和动作,而无需更改原始内容。一个代理模型必须仅能继承一个非抽象模型类。一个代理模型可以继承任意数量的没有定义任何模型字段的抽象模型类。一个代理模型也可以继承任意数量继承相同父类的代理模型。 临时继承TRANSIENT,将父模型作为传输模型使用,并可以添加传输字段。 继承约束 通用约束 对于扩展继承,查询的时候,父模型只能查询到父模型字段的数据,子模型可以查询出父模型及子模型的字段数据(因为派生关系所以子模型复刻了一份父模型的字段到子模型中)。系统不会为抽象基类创建实际的数据库表,它们也没有默认的数据管理器,不能被实例化也无法直接保存,它们就是用来被继承的。抽象基类完全就是用来保存子模型们共有的内容部分,达到重用的目的。当它们被继承时,它们的字段会全部复制到子模型中。系统不支持非jar包依赖模型的继承。多表继承具有阻断效应,子模型无法继承多表继承父模型的存储父模型的字段,需要使用@Model.Advanced注解的inherited属性显示声明继承父模型的父模型。但是可以继承多表继承父模型的抽象父模型的字段。可以使用@Model.Advanced的unInheritedFields和unInheritedFunctions属性设置不从父类继承的字段和函数。 跨模块继承约束 如果模型间的继承是跨模块继承,应该与模型所属模块建立依赖关系;如果模块间有互斥关系,则不允许建立模块依赖关系,同理模型间也不允许存在继承关系。跨模块代理继承,对代理模型的非inJvm函数调用将使用远程调用方式;跨模块扩展(同表)继承将使用本地调用方式,如果是数据管理器函数,将直连数据源。 模型类型与继承约束 抽象模型可继承:抽象模型(Abstract) 临时模型可继承:抽象模型(Abstract)、传输模型(Transient) 存储模型可继承:抽象模型(Abstract)、存储模型(Store)、存储模型(多表,Multi-table Store),不可继承多个Store或Multi-table Store 多表存储模型(父)可继承:同扩展继承 多表存储模型(子)在继承单个Multi-table Store后可继承:抽象模型(Abstract)、存储模型(Store),不可继承多个Store 代理模型可继承: 抽象模型(Abstract),须搭配继承Store、Multi-table Store或Proxy 存储模型(Store),不可继承多个Store或Multi-table Store 存储模型(多表,Multi-table Store),不可继承多个Store或Multi-table Store 代理模型(Proxy),可继承多个Proxy,但多个父Proxy须继承自同一个Store或Multi-table Store,且不能再继承其他Store或Multi-table Store 同名字段以模型自身字段为有效配置,若模型自身不存在该字段,继承字段以第一个加载的字段为有效配置,所以在多重继承的情况下,未避免继承同名父模型字段的不确定性,在自身模型配置同名字段来确定生效配置。 相关链接:模型的继承 字段序列化方式 字段序列化注意点 必须使用Field#store属性将字段存储设置为NullableBoolEnum.TRUE。 使用Field#serialize属性指定序列化方式,默认为JSON。 如把PetItemDetail设置为存储模型,须在PetItem的petItemDetails字段上使用Field.Relation#store属性将关联关系存储设置为false。不然会同时存储petItemDetails字段和对应的PetItemDetail表记录 字段序列化方式说明 序列化方式 说明 备注 JSON JSON序列化 主要用于模型相关类型字段的序列化,是@Field.serialize默认选项 DOT 点拼接集合元素 COMM 逗号拼接集合元素 BIT 按位与,2次幂数求和 非@Field.serialize可选项列表,用于二进制枚举序列化不需要配置,由oinone自动推断 函数命名规范 模型属性 默认取值规范 命名规则规范 namespace 先取@Model.model注解值,先在本类查找注解,如果本类未配置或注解值为空则在父类或接口上查找;若为空则取@Fun注解值,先在本类查找注解,如果本类未配置或注解值为空则在父类或接口上查找;若皆为空则取全限定类名 长度必须小于等于128个字符 name 默认使用java方法名 仅支持数字、字母;必须以字母开头;长度必须小于等于128个字符;不能以get、set为开头作为函数名称 fun 默认使用name属性 长度必须小于等于128个字符 summary 默认使用displayName属性 不能使用分号;长度必须小于等于500个字符 descripition NULL,注解无法定义 长度必须小于等于65535个字符 openLevel 函数的开放等级默认值:{FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE} FunctionOpenEnum枚举值;LOCAL(2L, "本地调用", "本地调用"), REMOTE(4L, "远程调用", "远程调用"),…

    2024年2月20日
    1.5K00
  • 【OceanBase】后端部署使用 OceanBase 数据库(海扬/OB)

    OceanBase 数据库配置 驱动配置 Maven配置(4.2.5.3版本可用) <oceanbase.version>2.4.14</oceanbase.version> <dependency> <groupId>com.oceanbase</groupId> <artifactId>oceanbase-client</artifactId> <version>${oceanbase.version}</version> </dependency> PS: oceanbase 驱动必须使用 2.4.5 版本或以上,低于此版本的驱动无法使用自增ID功能,无法正常启动。点击查看官方JDBC版本发布记录 JDBC连接配置 OceanBase – Oracle 版 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.alipay.oceanbase.jdbc.Driver url: jdbc:oceanbase://10.xxx.xxx.xxx:1001/BASE?useServerPrepStmts=true&useOraclePrepareExecute=true&defaultFetchSize=4096 username: xxxxxx password: xxxxxx validConnectionCheckerClassName: com.alibaba.druid.pool.vendor.OracleValidConnectionChecker validationQuery: SELECT 1 FROM DUAL OceanBase – MySQL 版 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.alipay.oceanbase.jdbc.Driver url: jdbc:oceanbase://10.xxx.xxx.xxx:1001/base username: xxxxxx password: xxxxxx 连接 URL 配置 点击查看官方JDBC连接配置说明 URL 格式(OceanBase – Oracle 版) jdbc:oceanbase://${host}:${port}/${database}?useServerPrepStmts=true&useOraclePrepareExecute=true&defaultFetchSize=4096 在jdbc连接配置时,useServerPrepStmts=true&useOraclePrepareExecute=true 必须配置,否则自增主键无法正常使用。 defaultFetchSize=4096 意味着在使用服务端预处理时,游标每次获取的结果集行数,驱动默认值为 10,在进行大量数据获取时会出现卡顿的现象,因此推荐使用 4096 作为其结果集大小。过大可能会导致 OOM,过小可能还是会出现卡顿,该值需要按实际情况进行配置。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置(OceanBase – Oracle 版) PS:OceanBase – MySQL 版无需配置方言,只需修改数据库连接即可正常使用。 pamirs方言配置 pamirs: dialect: ds: base: type: OceanBase version: 4.2.5.3 major-version: oracle-4.2 pamirs: type: OceanBase version: 4.2.5.3 major-version: oracle-4.2 plus: configuration: jdbc-type-for-null: "NULL" using-model-as-property: true using-statement-handler-dialect: true mapper: batch: collectionCommit default-batch-config: read: 500 write: 100 数据库版本 type version majorVersion 4.2.5.3 OceanBase 4.2.5.3 oracle-4.2 PS:由于方言开发环境为4.2.5.3版本,其他类似版本(4.x)原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: Oracle version: 12.2 major-version: 12c type version majorVersion Oracle 12.2 12c PS:由于 schedule 的方言与 Oracle 数据库并无明显差异,OceanBase 数据库可以直接使用 Oracle 数据库方言。 其他配置(OceanBase – Oracle 版) 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: (CAST(SYSTIMESTAMP AS DATE) – TO_DATE('1970-01-01 08:00:00', 'YYYY-MM-DD HH24:MI:SS')) * 8640000000000

    2025年7月21日
    78900

Leave a Reply

登录后才能评论