jooq实践
用法
sql语句
SELECT AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, COUNT(*)
FROM AUTHOR
JOIN BOOK ON AUTHOR.ID = BOOK.AUTHOR_ID
WHERE BOOK.LANGUAGE = 'DE'
AND BOOK.PUBLISHED > DATE '2008-01-01'
GROUP BY AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME
HAVING COUNT(*) > 5
ORDER BY AUTHOR.LAST_NAME ASC NULLS FIRST
LIMIT 2
OFFSET 1
java代码
dsl.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count())
.from(AUTHOR)
.join(BOOK).on(AUTHOR.ID.equal(BOOK.AUTHOR_ID))
.where(BOOK.LANGUAGE.eq("DE"))
.and(BOOK.PUBLISHED.gt(date("2008-01-01")))
.groupBy(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.having(count().gt(5))
.orderBy(AUTHOR.LAST_NAME.asc().nullsFirst())
.limit(2)
.offset(1)
示例代码
增:
public void addMerchantUrgentIsochrone(ITbMerchantUrgentIsochrone merchantUrgentIsochrone) {
TbMerchantUrgentIsochroneRecord record = dsl.newRecord(TB_MERCHANT_URGENT_ISOCHRONE, merchantUrgentIsochrone);
record.store();
merchantUrgentIsochrone.setId(record.getId());
}
删:
public int deleteRecords() {
return dsl.delete(TB_MERCHANT_URGENT_ISOCHRONE).where(TB_MERCHANT_URGENT_ISOCHRONE.CREATED_AT.ge(Timestamp.valueOf(LocalDateTime.now().minusDays(1)))).execute();
}
改:
public int update(ITbUserArea userArea) {
return dsl.update(TB_USER_AREA)
.set(TB_USER_AREA.GEOHASH, userArea.getGeohash())
.set(TB_USER_AREA.AREA, userArea.getArea())
.set(TB_USER_AREA.CITY_ID, userArea.getCityId())
.set(TB_USER_AREA.ORDER_COUNT, userArea.getOrderCount())
.where(TB_USER_AREA.ID.eq(userArea.getId()))
.execute();
}
查:
public List<ITbUserArea> getAreas(String preHash) {
return dsl.selectFrom(TB_USER_AREA)
.where(TB_USER_AREA.GEOHASH.like(preHash+"%"))
.fetchInto(TbUserArea.class);
}
jooq 可以执行sql语句
Result<Record> fetch(String var1) throws DataAccessException;
int execute(String var1) throws DataAccessException;
配置篇
maven配置
Maven依赖:(版本号可配)
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.9.5</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-meta</artifactId>
<version>3.9.5</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen</artifactId>
<version>3.9.5</version>
</dependency> 工具生成映射配置:
建立 xxx.xml(名字任意)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<configuration>
<jdbc>
<driver>...</driver>
<url>...</url>
<user>...</user>
<password>...</password>
</jdbc>
<generator>
<!-- 指定代码风格
- org.jooq.util.ScalaGenerator
- org.jooq.util.JavaGenerator
-->
<name>org.jooq.util.JavaGenerator</name> <database>
<!-- 数据库类型 -->
<name>org.jooq.util.postgres.PostgresDatabase</name>
<!-- <name>org.jooq.util.mysql.MySQLDatabase</name> -->
<excludes>
<!-- 对tb_table1,tb_table2,tb_table3 表不生成代码-->
tb_table1|tb_table2|tb_table3
</excludes>
<includeExcludeColumns>true</includeExcludeColumns>
<inputSchema>public</inputSchema> <!-- jooq转换对象类型,如用enum代替int
目标转换类型应实现org.jooq.Converter或org.jooq.Binding接口
相应的配置标签为<customType>,<forcedType>
参考地址: https://www.jooq.org/doc/3.9/manual/code-generation/custom-data-types/
https://www.jooq.org/doc/3.9/manual/code-generation/custom-data-type-bindings/
-->
<customTypes>
<customType>
<name>EffectiveStatus</name>
<type>xxx.isochrone.constant.EffectiveStatus</type>
<converter>xxx.geo.jooq.converter.EffectiveStatusConverter</converter>
</customType>
</customTypes>
<forcedTypes>
<!-- 使用converter -->
<forcedType>
<name>EffectiveStatus</name>
<expressions>.*\.tb_isochrone_audit_info\.effective_status</expressions>
<types>.*</types>
</forcedType>
<!-- 使用binding -->
<forcedType>
<userType>xxx.isochrone.pojos.GeographyPolygon</userType>
<binding>xxx.geo.jooq.binding.PGgeometryPolygonBinding</binding>
<expression>.*\.tb_isochrone.range|.*\.tb_merchant_area_isochrone.range</expression>
<types>.*</types>
</forcedType>
</forcedTypes>
</database> <generate>
<deprecated>false</deprecated>
<daos>true</daos>
<interfaces>true</interfaces>
</generate>
<target>
<!-- 生成的包名,生成的类在此包下 -->
<packageName>xxx.isochrone.jooq</packageName>
<!-- 输出的目录 -->
<directory>src/main/java</directory>
</target>
</generator>
</configuration> 相应pom.xml里添加对应的配置信息
<profiles>
<profile>
<id>db-gen</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.4.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<includeProjectDependencies>false</includeProjectDependencies>
<includePluginDependencies>true</includePluginDependencies>
<mainClass>org.jooq.util.GenerationTool</mainClass>
<cleanupDaemonThreads>false</cleanupDaemonThreads>
<arguments>
<!-- 匹配工具生成映射配置文件 -->
<argument>xxx.xml</argument>
</arguments>
</configuration>
<dependencies>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.9.5</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen</artifactId>
<version>3.9.5</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-meta</artifactId>
<version>3.9.5</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4.1208.jre7</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</profile>
</profiles>
执行代码自动生成命令
mvn -P db-gen exec:java
gradle配置
application.yml
spring:
jooq:
#请勿动,当使用了ejdbc的配置后,Spring Boot无法识别正确的Dialect
sqlDialect: Mysql
jooq模块下的gradle文件
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
resolutionStrategy {
cacheChangingModulesFor 0, 'seconds'
}
imports {
mavenBom 'io.spring.platform:platform-bom:Brussels-SR6'
mavenBom 'org.jooq:jooq:3.10.1'
}
dependencies {
dependency 'org.springframework.statemachine:spring-statemachine-core:1.2.6.RELEASE'
}
} apply plugin: 'nu.studer.jooq' emodule {
dependency 'tms-jooq-model'
}
dependencies {
compile 'org.jooq:jooq'
jooqRuntime 'mysql:mysql-connector-java'
} // Jooq Gradle Plugin 参考文档 https://github.com/etiennestuder/gradle-jooq-plugin
jooq {
version = '3.10.1'
tms(sourceSets.main) {
jdbc {
driver = 'com.mysql.jdbc.Driver'
url = 'jdbc:mysql://localhost:3306/test'
user = 'xxx'
password = 'xxx'
}
generator {
name = 'org.jooq.util.DefaultGenerator'
database {
name = 'org.jooq.util.mysql.MySQLDatabase'
inputSchema = 'scm_tms'
outputSchemaToDefault = true
includeExcludeColumns = true
//重构,相关字段迁移到tb_docker_deployment_log中
excludes = ".*_bak|.*\\.drc_check_time|.*\\.is_delete|.*\\.updated_at" forcedTypes {
forcedType {
userType = "xxx.common.model.model.GeoPoint"
converter = "xxx.jooq.model.converter.GeoPointConverter"
expression = ".*_lnglat"
types = ".*"
}
forcedType {
userType = "xxx.base.api.model.UsedType"
converter = "xxx.jooq.model.converter.VehicleIsUsedTypeConverter"
/*A Java regular expression matching fully-qualified columns. Use the pipe to separate several expressions. ->database.table.column*/
expression = ".*\\.base_vehicle\\.is_used"
/*Add a Java regular expression matching data types to be forced to have this type*/
types = ".*"
}
}
}
generate {
relations = true
deprecated = false
records = true
interfaces = true
pojos = true
daos = false
fluentSetters = true
}
target {
packageName = project.groupPrefix + "." + project.groupName + ".db.model"
directory = 'src/main/java'
}
}
}
} //不自动执行Jooq的代码生成和清除任务
//数据库更改后,手动进入 jooq模块
//执行 gradle cleanGenerateTmsJooqSchemaSource generateTmsJooqSchemaSource
//生成数据库对象在tms-db-model下
project.tasks.getByName('compileJava').dependsOn -= 'generateTmsJooqSchemaSource'
project.tasks.getByName('clean').dependsOn -= 'cleanGenerateTmsJooqSchemaSource'
jooq Converter示例
import xxx.GeoPoint;
import org.jooq.Converter; public class GeoPointConverter implements Converter<String, GeoPoint> {
@Override
public GeoPoint from(String databaseObject) {
return new GeoPoint(databaseObject);
} @Override
public String to(GeoPoint point) {
return point==null?"":point.gdFormat();
} @Override
public Class<String> fromType() {
return String.class;
} @Override
public Class<GeoPoint> toType() {
return GeoPoint.class;
}
} public class GeoPoint {
/**
* 经度值
*/
private Double longitude;
/**
* 纬度值
*/
private Double latitude; public GeoPoint() {
} public GeoPoint(String location) {
if (!StringUtils.isEmpty(location)) {
String[] lnglat = location.split(",");
this.longitude = Double.valueOf(lnglat[0]);
this.latitude = Double.valueOf(lnglat[1]);
}
} public Double getLongitude() {
return longitude;
} public void setLongitude(Double longitude) {
this.longitude = longitude;
} public Double getLatitude() {
return latitude;
} public void setLatitude(Double latitude) {
this.latitude = latitude;
} public String gdFormat() {
if (longitude == null || latitude == null) {
return "";
}
return longitude + "," + latitude;
} @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; GeoPoint geoPoint = (GeoPoint) o; if (longitude != null ? !longitude.equals(geoPoint.longitude) : geoPoint.longitude != null) return false;
return latitude != null ? latitude.equals(geoPoint.latitude) : geoPoint.latitude == null;
} @Override
public int hashCode() {
int result = longitude != null ? longitude.hashCode() : 0;
result = 31 * result + (latitude != null ? latitude.hashCode() : 0);
return result;
} @Override
public String toString() {
return "GeoPoint{" +
"longitude=" + longitude +
", latitude=" + latitude +
'}';
} }
import xxx.scm.tms.common.model.model.WarehouseType;
import org.jooq.Converter; /**
* WarehouseTypeConverter
*/
public class WarehouseTypeConverter implements Converter<Byte, WarehouseType> {
@Override
public WarehouseType from(Byte databaseObject) {
return WarehouseType.getTypeByCode(databaseObject);
} @Override
public Byte to(WarehouseType warehouseType) {
return warehouseType.getCode();
} @Override
public Class<Byte> fromType() {
return Byte.class;
} @Override
public Class<WarehouseType> toType() {
return WarehouseType.class;
}
} import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map; public enum WarehouseType {
NORMAL_TEMPERATURE((byte)1, "常温"),
COLD_STORAGE((byte)2, "冷藏"),
FREEZING((byte)3, "冷冻"); private byte code;
private String label; WarehouseType(byte code, String label) {
this.code = code;
this.label = label;
} public byte getCode() {
return code;
} public String getLabel() {
return label;
} private static Map<Byte, WarehouseType> map = new HashMap<>();
static {
EnumSet.allOf(WarehouseType.class)
.forEach(e -> map.put(e.getCode(), e));
} public static WarehouseType getTypeByCode(Byte code) {
return map.get(code);
}
}
高级用法代码示例
public List<BaseWarehouse> searchWarehouses(WarehouseCondition warehouseCondition) {
SelectConditionStep<?> step = dsl.selectFrom(Tables.BASE_WAREHOUSE).where(DSL.trueCondition()); StringBuilder likeValue = new StringBuilder(); if (!Objects.isNull(warehouseCondition.getWarehouseId())) {
step.and(Tables.BASE_WAREHOUSE.WAREHOUSE_ID.eq(warehouseCondition.getWarehouseId()));
} if (StringUtils.isNotBlank(warehouseCondition.getWarehouseName())) {
likeValue.setLength(0);
likeValue.append("%").append(warehouseCondition.getWarehouseName()).append("%");
step.and(Tables.BASE_WAREHOUSE.WAREHOUSE_NAME.likeIgnoreCase(likeValue.toString()));
} if (StringUtils.isNotBlank(warehouseCondition.getCompany())) {
likeValue.setLength(0);
likeValue.append("%").append(warehouseCondition.getCompany()).append("%");
step.and(Tables.BASE_WAREHOUSE.COMPANY.likeIgnoreCase(likeValue.toString()));
} if (StringUtils.isNotBlank(warehouseCondition.getProvinceName())) {
step.and(Tables.BASE_WAREHOUSE.PROVINCE_NAME.eq(warehouseCondition.getProvinceName()));
} if (!Objects.isNull(warehouseCondition.getProvinceId())) {
step.and(Tables.BASE_WAREHOUSE.PROVINCE_ID.eq(warehouseCondition.getProvinceId()));
} if (StringUtils.isNotBlank(warehouseCondition.getCityName())) {
step.and(Tables.BASE_WAREHOUSE.CITY_NAME.eq(warehouseCondition.getCityName()));
} if (!Objects.isNull(warehouseCondition.getCityId())) {
step.and(Tables.BASE_WAREHOUSE.CITY_ID.eq(warehouseCondition.getCityId()));
} if (StringUtils.isNotBlank(warehouseCondition.getDistrictName())) {
step.and(Tables.BASE_WAREHOUSE.DISTRICT_NAME.eq(warehouseCondition.getDistrictName()));
} if (!Objects.isNull(warehouseCondition.getDistrictId())) {
step.and(Tables.BASE_WAREHOUSE.DISTRICT_ID.eq(warehouseCondition.getDistrictId()));
} if (StringUtils.isNotBlank(warehouseCondition.getAddress())) {
likeValue.setLength(0);
likeValue.append("%").append(warehouseCondition.getAddress()).append("%");
step.and(Tables.BASE_WAREHOUSE.ADDRESS.like(likeValue.toString()));
} if (!Objects.isNull(warehouseCondition.getWarehouseLnglat())) {
step.and(Tables.BASE_WAREHOUSE.WAREHOUSE_LNGLAT.eq(warehouseCondition.getWarehouseLnglat()));
} if (StringUtils.isNotBlank(warehouseCondition.getContactName())) {
step.and(Tables.BASE_WAREHOUSE.CONTACT_NAME.eq(warehouseCondition.getContactName()));
}
if (StringUtils.isNotBlank(warehouseCondition.getContactPhone())) {
step.and(Tables.BASE_WAREHOUSE.CONTACT_PHONE.eq(warehouseCondition.getContactPhone()));
}
return step.fetchInto(BaseWarehouse.class);
} private Collection<SelectField<?>> getListField() {
Collection<SelectField<?>> fields = new ArrayList<>();
fields.add(SHIPMENT.SHIPMENT_ID);
fields.add(SHIPMENT.OUTBOUND_ID);
fields.add(SHIPMENT.BIZ_TYPE);
fields.add(SHIPMENT.BIZ_SUBTYPE);
fields.add(SHIPMENT.PICKUP_NAME);
fields.add(SHIPMENT.PICKUP_CONTACT_NAME);
fields.add(SHIPMENT.PICKUP_CONTACT_PHONE);
fields.add(SHIPMENT.PICKUP_PROVINCE_NAME);
fields.add(SHIPMENT.PICKUP_CITY_NAME);
fields.add(SHIPMENT.PICKUP_DISTRICT_NAME);
fields.add(SHIPMENT.PICKUP_ADDRESS);
fields.add(SHIPMENT.DESTINATION_NAME);
fields.add(SHIPMENT.DESTINATION_CONTACT_NAME);
fields.add(SHIPMENT.DESTINATION_CONTACT_PHONE);
fields.add(SHIPMENT.DESTINATION_PROVINCE_NAME);
fields.add(SHIPMENT.DESTINATION_CITY_NAME);
fields.add(SHIPMENT.DESTINATION_DISTRICT_NAME);
fields.add(SHIPMENT.DESTINATION_ADDRESS);
fields.add(SHIPMENT.DESTINATION_REMARK);
fields.add(SHIPMENT.ORDER_ID);
fields.add(SHIPMENT.ORDER_CREATED_AT);
fields.add(SHIPMENT.STATUS);
fields.add(DSL.ifnull(SHIPMENT_SKU.SHIPMENT_ID.count(), 0).as("COUNT"));
fields.add(DSL.ifnull(SHIPMENT_SKU.SKU_COUNT.sum(), BigDecimal.ZERO).as("SKU_COUNT"));
fields.add(DSL.ifnull(SHIPMENT_SKU.OUTBOUND_COUNT.sum(), BigDecimal.ZERO).as("OUTBOUND_COUNT"));
fields.add(DSL.ifnull(SHIPMENT_SKU.SIGN_COUNT.sum(), BigDecimal.ZERO).as("SIGN_COUNT")); fields.add(SHIPMENT.PICKUP_LNGLAT);
fields.add(SHIPMENT.DESTINATION_LNGLAT);
fields.add(SHIPMENT.EXPECT_ARRIVE_BEGIN_AT);
fields.add(SHIPMENT.EXPECT_ARRIVE_END_AT);
fields.add(SHIPMENT.BIZ_JSON);
fields.add(SHIPMENT.WAREHOUSE_ID);
fields.add(SHIPMENT.DRIVER_REMARK); //detail使用字段
fields.add(SHIPMENT.DRIVER_ID);
fields.add(SHIPMENT.CUSTOMER_SERVICE);
fields.add(SHIPMENT.CARRIER_ID);
fields.add(SHIPMENT.CARRIER_NAME);
fields.add(SHIPMENT.ROUTE_ID); return fields;
}
Result<Record> records = dsl.select(getListField())
.from(SHIPMENT)
.leftJoin(SHIPMENT_SKU)
.on(SHIPMENT.SHIPMENT_ID.eq(SHIPMENT_SKU.SHIPMENT_ID))
.where(getListConditions(query))
.groupBy(SHIPMENT.SHIPMENT_ID)
.orderBy(SHIPMENT.CREATED_AT.desc())
.offset(query.getOffset())
.limit(query.getLimit())
.fetch();
DSLContext executeUpdate
dsl.executeUpdate(dsl.newRecord(BASE_WAREHOUSE, baseWarehouse)), 如果vo类中的主键为null的时候
要指定 condition
乐观锁实现
https://www.jooq.org/doc/3.10/manual-single-page/#optimistic-locking
https://www.jooq.org/doc/3.10/manual-single-page/#codegen-config-record-version-timestamp-fields
jooq实践的更多相关文章
- 10个精妙的Java编码最佳实践
这是一个比Josh Bloch的Effective Java规则更精妙的10条Java编码实践的列表.和Josh Bloch的列表容易学习并且关注日常情况相比,这个列表将包含涉及API/SPI设计中不 ...
- 你知道吗?10个精妙的 Java 编码最佳实践
这是一个比Josh Bloch的Effective Java规则更精妙的10条Java编码实践的列表.和Josh Bloch的列表容易学习并且关注日常情况相比,这个列表将包含涉及API/SPI设计中不 ...
- 漫谈Code Review的错误实践
从刚开始工作时到现在,已经写了7年的代码,大部分代码都被人review过,自己也review了很多人的代码.在上一家公司的时候,我负责的一轮面试是专门进行Code Review的练习和经验谈. 通过在 ...
- 【转载】 漫谈Code Review的错误实践
原文地址: https://www.cnblogs.com/chaosyang/p/code-review-wrong-practices.html ------------------------- ...
- webp图片实践之路
最近,我们在项目中实践了webp图片,并且抽离出了工具模块,整合到了项目的基础模板中.传闻IOS10也将要支持webp,那么使用webp带来的性能提升将更加明显.估计在不久的将来,webp会成为标配. ...
- Hangfire项目实践分享
Hangfire项目实践分享 目录 Hangfire项目实践分享 目录 什么是Hangfire Hangfire基础 基于队列的任务处理(Fire-and-forget jobs) 延迟任务执行(De ...
- TDD在Unity3D游戏项目开发中的实践
0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...
- Logstash实践: 分布式系统的日志监控
文/赵杰 2015.11.04 1. 前言 服务端日志你有多重视? 我们没有日志 有日志,但基本不去控制需要输出的内容 经常微调日志,只输出我们想看和有用的 经常监控日志,一方面帮助日志微调,一方面及 ...
- 【大型网站技术实践】初级篇:借助Nginx搭建反向代理服务器
一.反向代理:Web服务器的“经纪人” 1.1 反向代理初印象 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从 ...
随机推荐
- mysql 开源~canal的深度解读1
一 简介:经过一段时间的研究,对canal有了一些见解 二 配置文件: 1 canal.properties (系统根配置文件) 主要参数列表 canal.properties (系统根配 ...
- sklearn中的回归器性能评估方法(转)
explained_variance_score() mean_absolute_error() mean_squared_error() r2_score() 以上四个函数的相同点: 这些函数都有一 ...
- Docker容器数据卷
⒈Docker容器中数据如何持久化? ①通过commit命令使容器反向为镜像 ②以容器数据卷的方式将数据抽离 ⒉容器数据卷的作用? ①容器数据的持久化 ②容器间继承.共享数据 ⒊能干嘛? 卷就是目录或 ...
- SpringBoot整合日志
Java日志 日志的接口层.抽象层 日志的实现 JCL(Jakarta Commons Logging) SLF4J(Simple Logging Facade for Java) Jboss-Log ...
- adboost方法(转载)
转载链接:http://blog.csdn.net/google19890102/article/details/46376603 一.集成方法(Ensemble Method) 集成方法主要 ...
- phantomjs 中如何使用xpath
function getNodeInfo(inputcsvPath) { var htmlnodeInfo = page.evaluate(function () { //_Ltg var XPATH ...
- PNG,JPEG,BMP,JIF图片格式详解及其对比
原文地址:http://blog.csdn.net/u012611878/article/details/52215985 图片格式详解 不知道大家有没有注意过网页里,手机里,平板里的图片,事实上,图 ...
- 多个SpingBoot项目的搭建与部署
最近几年SpringBoot大热,很多IT公司都开始采用SpringBoot来替换传统的SpringMVC项目.那么如何搭建一个适合开发的项目架构呢? 这里我主要介绍Java程序员使用最多的两种工具进 ...
- 06-jQuery的文档操作(重点)
之前js中咱们学习了js的DOM操作,也就是所谓的增删改查DOM操作.通过js的DOM的操作,大家也能发现,大量的繁琐代码实现我们想要的效果.那么jQuery的文档操作的API提供了便利的方法供我们操 ...
- 【Android开源库】美团等APP城市选择
CityPicker 现在使用比较多的类似美团等APP的城市选择界面. 2步即可实现,就是这么简单粗暴! Gif image APK 下载demo.apk体验. Install Gradle: com ...