`

java jdbc 汇总

阅读更多
JDBC
(Java Database Connectivity)
一、概述:
JDBC从物理结构上说就是Java语言访问数据库的一套接口集合。从本质上来说就是调用者(程序员)和实行者(数据库厂商)之间的协议。JDBC的实现由数据库厂商以驱动程序的形式提供。JDBC API为Java开发者使用数据库提供了统一的编程接口,它由一组Java类和接口组成,使得开发人员可以使用纯Java的方式来连接数据库,并进行操作。
1.在JDBC中包括了两个包:java.sql和javax.sql。
① java.sql   基本功能。这个包中的类和接口主要针对基本的数据库编程服务,如生成连接、执行语句以及准备语句和运行批处理查询等。同时也有一些高级的处理,比如批处理更新、事务隔离和可滚动结果集等。
② javax.sql   扩展功能。它主要为数据库方面的高级操作提供了接口和类。如为连接管理、分布式事务和旧有的连接提供了更好的抽象,它引入了容器管理的连接池、分布式事务和行集等。

主要对象和接口:
注:除了标出的Class,其它均为接口。
























驱动程序按照工作方式分为四类: (了解)
1、 JDBC-ODBC bridge + ODBC驱动
JDBC-ODBC bridge桥驱动将JDBC调用翻译成ODBC调用,再由ODBC驱动翻译成访问数据库命令。
优点:可以利用现存的ODBC数据源来访问数据库。
缺点:从效率和安全性的角度来说比较差。不适合用于实际项目。
2、 基于本地API的部分Java驱动
我们应用程序通过本地协议跟数据库打交道。然后将数据库执行的结果通过驱动程序中的Java部分返回给客户端程序。
优点:效率较高;
缺点:安全性较差。
3、 纯Java的中间服务器驱动



缺点:两段通信,效率比较差
优点:安全性较好
4、 纯Java本地协议:通过本地协议用纯Java直接访问数据库。
特点:效率高,安全性好。



二、JDBC编程的步骤
① 注册一个driver
注册驱动程序有三种方式:
方式一:  class.forName(“oracle.jdbc.driver.OracleDriver”);
Java规范中明确规定:所有的驱动程序必须在静态初始化代码块中将驱动注册到驱动程序管理器中。
Static{
Class.forName(“oracle.jdbc.driver.OracleDriver”);
}
方式二: Driver drv = new oracle.jdbc.dirver.OracleDriver();   //针对没有隐式注册时采用
         DriverManager.registerDriver(drv);
方式三:编译时在虚拟机中加载驱动
java –Djdbc.dirvers=驱动全名 类名
例: java –Djdbc.drivers=oracle.jdbc.driver.OracleDriver Lab1
     使用系统属性名,加载驱动; -D表示为系统属性赋值。

附:mysql的Driver的全名com.mysql.jdbc.Driver或org.gjt.mm.mysql           
      SQLServer的Driver的全名com.microsoft.jdbc.sqlserver.SQLServerDriver

② 建立连接
conn=DriverManager.getConnection(“jdbc:oracle:thin:@192.168.0.23:1521:tarena”, 
” User”,” Pasword”);


Connection连接是通过DriverManager的静态方法getConnection(....)来得到的,这个方法的实质是把参数传到实际的Driver中的connect()方法中来获得数据库连接的。
Oracle URL的格式:
jdbc:oracle:thin:(协议)@XXX.XXX.X.XXX:XXXX(IP地址及端口号):XXXXXXX(所使用的库名)
MySql URL的写法        例: jdbc:mysql://192.168.8.21:3306/test
SQLServer URL的写法    例:jdbc:microsoft:sqlserver://192.168.8.21:1433

③ 获得一个Statement对象      stm = conn.createStatement();    

④ 通过Statement执行SQL语句    
    stm.excuteQuery(Sring sql);     //返回一个查询结果集
stm.excuteUpdate(String sql);    //返回值为int型,表示影响记录的条数。
Stm.excute(String sql);         //返回true,表示查询;返回false,表示其它操作。
将sql语句通过连接发送到数据库中执行,以实现对数据库的操作。
⑤ 处理结果集
使用Connection对象获得一个Statement,Statement中的executeQuery(String sql) 方法可以使用select语句查询,并且返回一个结果集 ResultSet,通过遍历这个结果集,可以获得select语句的查寻结果,ResultSet的next()方法会操作一个游标从第一条记录的前面开始读取,直到最后一条记录。executeUpdate(String sql) 方法用于执行DDL和DML语句,主要还是DML,包括insert, delete,update操作。
只有执行select语句才有结果集返回。
例: Statement str=con.createStatement();  //创建Statement
String sql=”insert into test(id,name) values(1,”+”’”+”test”+”’”+”)”;
str. executeUpdate(sql);//执行Sql语句
String sql=”select * from test”;
ResultSet rs=str. executeQuery(String sql);//执行Sql语句,执行select语句后有结果集
//遍历处理结果集信息
      while(rs.next()){
          System.out.println(rs.getInt(“id”));
         System.out.println(rs.getString(“name”))
}  

⑥ 关闭数据库连接(释放资源)  调用.close()
rs.close();            stm.close();              conn.close();
ResultSet  Statement  Connection是依次依赖的。
注意:要按先ResultSet结果集,后Statement,最后Connection的顺序关闭资源,因为Statement和ResultSet是需要连接时才可以使用的,所以在使用结束之后有可能其它的Statement还需要连接,所以不能先关闭Connection。

图形演绎编写JDBC程序的一般过程:












三、几个重要接口:
(1) Statement  —— SQL语句执行接口
Statement接口代表了一个数据库的状态,在向数据库发送相应的SQL语句时,都需要创建Statement接口或者PreparedStatement接口。在具体应用中,Statement主要用于操作不带参数(可以直接运行)的SQL语句,比如删除语句、添加或更新。
Stagement   a. 某些情况下,效率不高;
            b. 语法结构不够清晰,造成对类型的支持不太好;

(2) PreparedStatement —— 预编译的Statement   (重点)
Step1: 通过连接获得PreparedStatement对象,用带占位符(?)的sql语句构造。
     PreparedStatement pstm = con.preparedStatement(“select * from test where id=?”);
Step2: 设置参数
     Pstm.setString(1, “08868”);
Step3: 执行sql语句
     rs = pstm.excuteQuery();
Statemnet发送完整的SQL语句到数据库不是直接执行而是由数据库先编译,再运行。
而PreparedStatement是先发送带参数的SQL语句,再发送一组参数值。如果是同构的SQL语句,PreparedStatement的效率要比Statement高。而对于异构的SQL则两者效率差不多。
同构:两个SQL语句可编译部分是相同的,只有参数值不同。
异构:整个SQL语句的格式是不同的。
注意点: ① 使用预编译的Statement编译多条SQL语句一次执行
         ② 可以跨数据库使用,编写通用程序
         ③ 能用预编译时尽量用预编译  
PreparedStatement也执行相应的SQL语句。它继承于Statement接口,除了具备Statement所有功能,还可以对SQL语句进行预处理。
主要方法:
① ResultSet executeQuery() throws SQLException
在此 PreparedStatement 对象中执行 SQL 查询,并返回该查询生成的 ResultSet 对象。从不返回 null;如果发生数据库访问错误或者 SQL 语句没有返回ResultSet 对象则抛出SQLException异常。

② int executeUpdate() throws SQLException
在此 PreparedStatement 对象中执行 SQL 语句,该语句必须是一个 SQL INSERT、UPDATE 或 DELETE 语句;或者是一个什么都不返回的 SQL 语句,比如 DDL 语句。
返回值int表示影响的记录条数,一条都没有则返回0;

③ boolean execute()throws SQLException
在此 PreparedStatement 对象中执行 SQL 语句,该语句可以是各种类型SQL 语句。这个方法的返回值是boolean类型,如果返回true就表示sql是一个select语句,可以通过getResultSet()获得结果集,如果是false,sql就是DML语句或者是DDL语句,可以通过getUpdateCount()获得所影响的记录条数。
④各种set方法   将指定位置的参数设置为指定的类型。比如ps.setString(3, “tarena”);

(3) ResultSet —— 结果集操作接口
   ResultSet接口是查询结果集接口,它对返回的结果集进行处理。ResultSet是程序员进行JDBC操作的必需接口。
若:ID  Int ; String str=rs.getString(“ID”);

(4) ResultSetMetaData —— 元数据操作接口
   ResultSetMetaData是对元数据进行操作的接口,可以实现很多高级功能。Hibernate运行数据库的操作,大部分都是通过此接口。可以认为,此接口是SQL查询语言的一种反射机制。ResultSetMetaData接口可以通过数组的形式,遍历数据库中表的各个字段的属性,对于我们开发者来说,此机制的意义重大。
  JDBC通过元数据(MetaData)来获得具体的表的相关信息,例如,可以查询数据库中有哪些表,表有哪些字段,以及字段的属性等。MetaData中通过一系列getXXX将这些信息返回给我们。     
                数据库元数据 Database MetaData     使用connection.getMetaData()获得
MetaData包括:                                  包含了关于数据库整体元数据信息。
                结果集元数据 Result Set MetaData    使用resultSet.getMetaData()获得
                                          比较重要的是获得表的列名、列数等信息。
结果集元数据对象:ResultSetMetaData meta = rs.getMetaData();
 字段个数:meta.getColomnCount();
 字段名字:meta.getColumnName();
 字段JDBC类型:meta.getColumnType();
 字段数据库类型名称:meta.getColumnTypeName();

数据库元数据对象:DatabaseMetaData dbmd = con.getMetaData();
 数据库名:dbmd.getDatabaseProductName();
 数据库版本号:dbmd.getDatabaseProductVersion();
 数据库驱动名:dbmd.getDriverName();
 数据库驱动版本号:dbmd.getDriverVersion();
 数据库URL:dbmd.getURL();
 该连接的登录名:dbmd.getUserName();

四、JDBC异常处理:
JDBC中,和异常相关的两个类是SQLException和SQLWarning。
1. SQLException类:用来处理较为严重的异常情况。
比如:① 传输的SQL语句语法的错误;
      ② JDBC程序连接断开;
      ③ SQL语句中使用了错误的函数。
   SQLException提供以下方法:
         getNextException() —— 用来返回异常栈中的下一个相关异常;
         getErrorCode() —— 用来返回代表异常的整数代码 (error code);
         getMessage() —— 用来返回异常的描述信息 (error message)。

2. SQLWarning类:用来处理不太严重的异常情况,也就是一些警告性的异常。其提供的方法和使用与SQLException基本相似。


结合异常的两种处理方式,明确何时采用哪种。
A. throws    处理不了,以及要让调用者知道,就throws;
B. try … catch   能自行处理,就进行异常处理。

五、JDBC中使用Transaction编程(事务编程)
1. 事务是具备以下特征(ACID)的工作单元:
 原子性(Atomicity)—— 如果因故障而中断,则所有结果均被撤消;
 一致性(Consistency)—— 事务的结果保留不变;
 孤立性(Isolation)—— 中间状态对其它事务是不可见的;
 持久性(Durability)—— 已完成的事务结果上持久的。
原子操作,也就是不可分割的操作,必须一起成功一起失败。

2. 事务处理三步曲:(事务是一个边界)
① connection.setAutoCommit(false);       //把自动提交关闭
② 正常的DB操作                     //若有一条SQL语句失败了,自动回滚
③ connection.commit()           //主动提交
或 connection.rollback()       //主动回滚

















3. JDBC事务并发产生的问题和事务隔离级别
JDBC事务并发产生的问题:
① 脏读(Dirty Reads) 一个事务读取了另一个并行事务还未提交的数据。
② 不可重复读(UnPrpeatable Read) 一个事务再次读取之前的数据时,得到的数据不一致,被另一个已提交的事务修改。
③ 幻读(Phantom Read) 一个事务重新执行一个查询,返回的记录中包含了因为其它最近提交的事务而产生的新记录。
为了避免以上三种情况的出现,则采用
事务隔离级别:









以上的五个事务隔离级别都是在Connection类中定义的静态常量,使用setTransactionIsolation(int level) 方法可以设置事务隔离级别。
比如:con.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
六、JavaBean定义
1、是一个普通的Java类;
2、在结构上没有预先的规定,不需要容器;
3、要求放在包中,要求实现java.io.Serializalbe接口
4、要求有一个无参的构造方法;
5、属性的类型必须保持唯一,返回值必须和set方法参数类型一致
6、对每个属性要有对应的get和set方法。注:隐藏属性可以没有。
另外,POJO与JavaBean的区别:
    POJO——Pure Old Java Object or Plain Ordinary Java Object (简单Java类对象)
POJO 原则上不鼓励在 JavaBean 里面写业务逻辑方法。简单说 POJO 除了能赋值,也就是提供get/set方法,别的什么也不能做,它就好比一个水杯, 不是个能烧水的水壶,因为它没有烧水()这个方法,因此只能盛水。POJO主要用来和数据库里的表进行对应。

七、JDBC2.0新特性:
1、Scrollability 可滚动结果集(可双向滚动),这种结果集不但可以双向滚动,相对定位,绝对定位,并且可以修改数据信息。
   滚动:可支持双向绝对与双向相对滚动,对结果集可进行多次迭代。
   con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
                          ResultSet.CONCUR_UPDATABLE);
TYPE_FORWARD_ONLY :
该常量指示指针只能向前移动的ResultSet对象的类型。
TYPE_SCROLL_INSENSITIVE:
该常量指示可滚动但通常不受其他的更改影响的ResultSet对象的类型。
TYPE_SCROLL_SENSITIVE:
该常量指示可以更新的ResultSet对象的并发模式。

滚动特性,对ResultSet调用:
绝对定位:boolean absolute(int row)
可以使用此方法跳到指定的记录位置。定位成功返回true,不成功返回false,若返回值为false,则游标不会移动。
void afterLast(),游标跳到最后一条记录之后。
        void beforeFirst(),游标跳到第一条记录之前。(跳到游标初始位)
相对定位:boolean first(),游标指向第一条记录。
          boolean last(),游标指向最后一条记录。
boolean next(),此方法是使游标向下一条记录移动。
boolean previous() ,可以使游标向上一条记录移动,前提是未到首条记录。
2、Updatability结果集可更新。(主要应用于桌面应用)
ResultSet结果集中,先使用moveToInsertRow(),然后可以使用updateXxx(int column, columnType value)方法来更新指定列数据,再使用insertRow() 方法插入记录,最后将游标指回原位:moveToCurrentRow() 。
  更新: rs.updateInt(1, ”13800”);     //修改
         rs.deleteRow();    //删除
         rs.updateRow();
    注:只有在必要的时候(如桌面应用)才用结果集更新数据库,因为使用结果集更新数据库效率低下。可更新结果集还要看数据库驱动程序是否支持,如Oracle支持,而MySQL就不支持。并且只能针对一张表做结果集更新。而且不能有join操作。必须有主键,必须把所有非空且没有默认值的字段查出。处理可更新结果集时不能用select * 来执行查询语句,必须指出具体要查询的字段。
能否使用可更新结果集,要看使用的数据库驱动是否支持,还有只能用于单表且表中有主键字段(可能会是联合主键),不能够有表连接,会取所有非空字段且没有默认值。
总之,能否使用JDBC2.0 ResultSet的新特性要看数据库驱动程序是否支持。

3、Batch updates 可批量更新
将一组对数据库的更新操作发送到数据库统一执行(数据库支持并发执行操作),以提高效率。主要是通过减少数据(SQL语句或参数)在网络上传输的次数来节省时间。
(1)对于Statement的批量更新处理:
stm.addBatch(String sql1);   方法会在批处理缓存中加入一条sql语句。
stm.addBatch(String sql2);
int[] results = stm.executeBatch() ,执行批处理缓存中的所有sql语句。
(2)对于PreparedStatement的批量更新处理:
pstm.setInt(1, 11);
    pstm.setString(2,”haha”); …
pstm.addBatch() 将一组参数添加到此 PreparedStatement 对象的批处理命令中。
int[] results = pstm.executeBatch() 将一批命令提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组。
注意:
int[]中每一个数表示该SQL语句所影响的记录条数。
PreparedStatement中使用批量更新时,要先设置好参数后使用addBatch()方法加入缓存。
批量更新中只能使用更新或插入语句


JAVA_HOME: JDK的安装路径
CLASSPATH: 去那里找需要的JAR包
PATH:%JAVA_HOME%/bin
环境变量的设置

Linux下: /etc/.profile这是所有用户的全局的文件


主目录下的vi .bashrc这是当前的用户

export JAVA_HOME=/opt/jdk1.5.0_06      指向java的安装目录
export PATH=$JAVA_HOME/bin:$PATH       指向安装目录下的bin子目录
export CLASSPATH=.:$JAVA_HOME/lib      类路径
          
用source .bashrc 生效或注销

主目录下vi .bash_profile

JAVA_HOME=/opt/jdk1.5.0_06      指向java的安装目录
PATH=$JAVA_HOME/bin:$PATH       指向安装目录下的bin子目录
CLASSPATH=.:$JAVA_HOME/lib      类路径
export JAVA_HOME CLASSSPATH PATH

在以上两个文件配置都可以,只配置一个文件就可以

配置完以后用source .bash_profile生效或注销

windows下:我的电脑(右键)--->属性---->高级----->环境变量

  用户变量针对的是当前用户
  系统变量针对所有的用户
          在用户变量和系统变量这两个里面只配一个

JAVA_HOME=c:\Program Files\Java\jdk1.5.0_09(不是JRE)
path=%JAVA_HOME%\bin (可执行文件)  (path里原有的内容不要改变,只在其后进行添加即可)
CLASSPATH=.;%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\lib\dt.jar;

验证是否配置成功:在cmd后,输入javac就可以了,看是否有帮助信息
Linux下:在命令行输入javac就行了


编译:

   编译命令  javac xxxx.java 源文件的名字,源文件中的一个类会对应编译生成一个.class文件
运行:
   运行命令  java xxxx 类的名字 --- 启动虚拟机

带包编译和运行:

包 --- 分类放置,减少命名空间
   包名.类名   表示一个类的全限定名
   java xxx.yyyy.zzzz.ClassA   --- 运行时要在包结构的上一层目录来运行。
   javac -d . xxxx.java  --- 编译的时候,按照包结构存放字节码文件,此命令生成的.class文件在当前目录
   package xxx.yyyy.zzzz,包的定义在一个程序中只能由一个

分享到:
评论

相关推荐

    Java—JDBC资料汇总

    综上所述,本资料汇总包含的内容广泛且深入,从JDBC的基本使用到高级特性,再到数据库连接池的实现和优化,对于理解和掌握Java数据库编程具有很高的价值。通过学习这些内容,开发者可以有效地在Java应用中实现与...

    java jdbc连接数据库【Mysql sqlsever orcl sqlite】汇总

    Java JDBC(Java Database Connectivity)是Java编程语言中用于与各种关系数据库进行通信的API。它为程序员提供了一组标准接口,使得在Java程序中访问数据库变得简单。本篇将详细介绍如何使用Java JDBC连接MySQL、...

    JAVA连接数据库 JDBC驱动汇总

    ### JAVA连接数据库JDBC驱动汇总 #### 一、概述 在Java开发中,与数据库进行交互是必不可少的一个环节。为了实现这一目标,Java提供了多种方式来连接不同的数据库系统,其中最为广泛使用的便是JDBC(Java Database...

    家庭理财管理系统(java swing+jdbc)

    利用Swing的JTable组件,展示月度或年度的收支汇总,通过JDBC查询出统计结果并填充到表格中。同时,通过JFreeChart等库,可以创建饼图或柱状图来可视化数据,帮助用户更直观地理解自己的财务状况。 总结而言,Java ...

    JDBC汇总 包含sql oracle的jdbc代码

    在标题和描述中提到的“JDBC汇总”指的是对不同数据库(如SQL Server、Oracle、MySQL和Sybase)的JDBC驱动程序及其连接代码的总结。以下是这些数据库的JDBC连接代码示例: 1. **Microsoft SQL Server**: - 对于...

    JDBC连接方法总汇

    ### JDBC连接方法总汇 #### 一、JDBC简介与作用 Java Database Connectivity(JDBC)是一种用于执行SQL语句的Java API,可以为多种关系型数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了与...

    各个DB的jdbc汇总

    JDBC(Java Database Connectivity)是Java编程语言中与各种数据库进行交互的标准接口。它允许Java应用程序通过编写Java代码来访问和处理存储在不同数据库中的数据。以下是对标题和描述中涉及的不同数据库JDBC驱动的...

    JDBC驱动下载汇总.txt

    ### JDBC驱动下载汇总知识点 #### 一、JDBC概述与驱动选择 JDBC(Java Database Connectivity)是一种用于执行 SQL 语句的 Java API,可以为多种关系数据库提供统一访问,它由一组用 Java 编程语言编写的类和接口...

    JDBC驱动下载汇总

    在Java开发领域,JDBC(Java Database Connectivity)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。本文将详细介绍不同数据库的JDBC驱动下载及连接...

    JDBC数据库连接串总汇

    在IT领域,特别是软件开发与数据管理中,Java Database Connectivity(JDBC)是连接Java应用程序与各种数据库管理系统的重要桥梁。JDBC提供了一种标准的API,使得开发者能够使用SQL语句来查询、更新和管理数据库中的...

    Java项目:在线新闻平台系统(java+jsp+jdbc+mysql)

    功能: 用户的登录注册,新闻的分类查询,评论留言,投稿,新闻的后台管理,发布,审核,投稿管理以及汇总统计等等。 二、项目运行 环境配置: Jdk1.8 + Tomcat8.5 + mysql + Eclispe (IntelliJ IDEA,Eclispe,...

    JAVA笔试面试资料JDBC HTTP、JSP、Servlet、Struts面试题汇总资料.zip

    JAVA笔试面试资料JDBC HTTP、JSP、Servlet、Struts面试题汇总资料: 2014年最新Java笔试题及答案.docx 225道Java面试题 学会了Java面试随你问.docx Ant和Maven的作用是什么?两者之间功能、特点有哪些区别?.docx ...

    各个类型数据库的JDBC驱动汇总

    JDBC(Java Database Connectivity)是Java编程语言中用于与各种数据库进行交互的一种标准接口。它允许Java应用程序通过Java代码来连接和操作数据库。在本文中,我们将深入探讨JDBC驱动的几个主要类型,包括适用于...

    jdbc(java的数据库连接方式总汇)

    Java的数据库连接(JDBC,Java Database Connectivity)是Java编程语言与各种数据库进行交互的一种标准接口。它由Java API组成,允许Java程序执行SQL语句并处理返回的结果。本篇文章将详细阐述JDBC的核心概念、步骤...

    JDBC技巧汇总.pdf

    ### JDBC技巧汇总知识点详解 #### 一、JDBC概述 **定义:** JDBC(Java Database Connectivity,Java数据库连接)是一种用于执行SQL语句的Java API,它为多种关系数据库提供统一访问方式。JDBC由一系列Java语言...

    100家大公司java笔试题汇总.docx

    Java编程语言笔试题汇总 本文档总结了Java编程语言的常见笔试题,涵盖了Java基础知识、面向对象编程、异常处理、Servlet、JDBC、J2EE等方面的知识点。 一、Java基础知识 1. Java中的abstract关键字可以修饰字段、...

    实现jdbc所需要的jar包汇总.zip

    Java Database Connectivity(JDBC)是Java编程语言中用于与关系数据库交互的标准应用编程接口(API)。在Java项目中,为了实现对数据库的操作,如查询、插入、更新和删除数据,我们需要引入特定的JDBC驱动程序。这...

    JDBC核心技术_汇总篇.pdf

    JDBC(Java Database Connectivity)是Java编程语言中用于执行SQL语句的API,它可以用来连接和操作数据库。JDBC是Java的一部分,提供了连接数据库的统一方法,使得开发者可以不必为特定的数据库编写不同的代码,极大...

    100家大公司java笔试题汇总

    Java笔试题汇总 Java是最流行的编程语言之一,在软件开发行业中非常常用。以下是Java笔试题汇总,涵盖了Java的多个方面,包括Java基础、Java面向对象编程、Java多线程、Java网络编程、Java数据库编程等。 Java...

    jdbc数据库驱动汇总

    ### JDBC数据库驱动汇总 在Java开发中,JDBC(Java Database Connectivity)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问。它由一组用Java语言编写的类和接口组成。JDBC提供了诸如查询执行...

Global site tag (gtag.js) - Google Analytics