`
shaobo
  • 浏览: 52654 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Java 通用数据库连接类[支持存储过程 参数自动识别]

    博客分类:
  • J2SE
阅读更多

package com.hospital.dao.tools;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import org.apache.log4j.Logger;

/**
 * 数据库操作管理类
 *
 * @author Harlyhood
 *
 */
public class DBManager {

 // --------------------------------------------------------- Instance
 private static Logger logger = Logger.getLogger(DBManager.class);
 // --------------------------------------------------------- Methods

 // 数据库连接对象
 private Connection con;
 // SQL语句对象
 private Statement stmt;
 // 带参数的Sql语句对象
 private PreparedStatement pstmt;
 // 记录集对象
 private ResultSet rs;
 // 数据连接管理(连接池对象)
 private DBConnectionManager dcm = null;

 /** ***********************手动设置的连接参数********************************* */
 @SuppressWarnings("unused")
 private static String _DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
 @SuppressWarnings("unused")
 private static String _URL = "jdbc:sqlserver://localhost:1433;database=Hospital_AI_DB;characterEncoding=gb2312";
 @SuppressWarnings("unused")
 private static String _USER_NA = "sa";
 @SuppressWarnings("unused")
 private static String _PASSWORD = "";

 /** ********************************************************************** */

 // 默认构造
 public DBManager() {
 }

 /** ****************************************************************************************** */
 /**
  * **************************************** 数据库连接初始化
  * ***********************************
  */
 /** ****************************************************************************************** */

 /**
  * 得到一个默认的数据库连接[从 com.hospital.dao.tools.db.properties文件初始化]
  *
  * @throws Exception
  */
 private void getConnection() {
  logger.info("###############open:::::从默认的配置文件得到一个数据库连接");
  // 获取一个连接池管理类的实例
  dcm = DBConnectionManager.getInstance();
  // 得到一个数据库连接
  con = dcm.getConnection("mysql");

  try {
   con.setAutoCommit(false);
  } catch (SQLException e) {

   e.printStackTrace();
  }
 }

 /**
  * 从指定参数得到一个连接对象
  *
  * @param driver
  * @param url
  * @param user_na
  * @param password
  * @throws Exception
  */
 public void getConnection(String driver, String url, String user_na,
   String password) throws Exception {
  try {
   logger.info("###############open:::::从指定配置中得到一个数据库连接");
   Class.forName(driver);
   con = DriverManager.getConnection(url, user_na, password);
  } catch (ClassNotFoundException ex) {
   logger
     .info("###############Error[com.hospital.dao.tools.DBManager^^^Method:getConnection^^^Line:81]找不到类驱动类: "
       + driver);
   throw ex;
  } catch (SQLException ex) {
   logger
     .info("###############Error[com.hospital.dao.tools.DBManager^^^Method:getConnection^^^Line:81]加载类: "
       + driver + " 时出现 SQLException 异常");
   throw ex;
  }
 }

 /** ****************************************************************************************** */
 /**
  * **************************************** 数据库操作方法
  * ***********************************
  */
 /** ****************************************************************************************** */

 /**
  * 执行SQL语句操作(更新数据 无参数)
  *
  * @param strSql
  *            SQL语句
  * @throws Exception
  */
 public boolean executeUpdate(String strSql) throws SQLException {
  getConnection();
  // getConnection(_DRIVER,_URL,_USER_NA,_PASSWORD);
  boolean flag = false;
  stmt = con.createStatement();
  logger.info("###############::执行SQL语句操作(更新数据 无参数):" + strSql);
  try {
   if (0 < stmt.executeUpdate(strSql)) {
    close_DB_Object();
    flag = true;
    con.commit();
   }
  } catch (SQLException ex) {
   logger
     .info("###############Error DBManager Line126::执行SQL语句操作(更新数据 无参数):"
       + strSql + "失败!");
   flag = false;
   con.rollback();
   throw ex;
  }
  return flag;

 }

 /**
  * 执行SQL语句操作(更新数据 有参数)
  *
  * @param strSql
  *            sql指令
  * @param prams
  *            参数列表
  * @return
  * @throws SQLException
  */
 public boolean executeUpdate(String strSql, HashMap<Integer, Object> prams)
   throws SQLException, ClassNotFoundException {
  getConnection();
  // getConnection(_DRIVER,_URL,_USER_NA,_PASSWORD);
  boolean flag = false;
  try {
   pstmt = con.prepareStatement(strSql);
   setParamet(pstmt, prams);
   logger.info("###############::执行SQL语句操作(更新数据 有参数):" + strSql);

   if (0 < pstmt.executeUpdate()) {
    close_DB_Object();
    flag = true;
    con.commit();
   }
  } catch (SQLException ex) {
   logger
     .info("###############Error DBManager Line121::执行SQL语句操作(更新数据 无参数):"
       + strSql + "失败!");
   flag = false;
   con.rollback();
   throw ex;
  } catch (ClassNotFoundException ex) {
   logger
     .info("###############Error DBManager Line152::执行SQL语句操作(更新数据 无参数):"
       + strSql + "失败! 参数设置类型错误!");
   con.rollback();
   throw ex;
  }
  return flag;

 }

 /**
  * 执行SQL语句操作(查询数据 无参数)
  *
  * @param strSql
  *            SQL语句
  * @return 数组对象列表
  * @throws Exception
  */
 public ArrayList<HashMap<Object, Object>> executeSql(String strSql)
   throws Exception {
  getConnection();
  // getConnection(_DRIVER,_URL,_USER_NA,_PASSWORD);
  stmt = con.createStatement();
  logger.info("###############::执行SQL语句操作(查询数据):" + strSql);
  rs = stmt.executeQuery(strSql);
  con.commit();
  if (null != rs) {
   return convertResultSetToArrayList(rs);
  }
  close_DB_Object();
  return null;
 }

 /**
  * 执行SQL语句操作(查询数据 有参数)
  *
  * @param strSql
  *            SQL语句
  * @param prams
  *            参数列表
  * @return 数组对象列表
  * @throws Exception
  */
 public ArrayList<HashMap<Object, Object>> executeSql(String strSql,
   HashMap<Integer, Object> prams) throws Exception {
  getConnection();
  // getConnection(_DRIVER,_URL,_USER_NA,_PASSWORD);
  pstmt = con.prepareStatement(strSql);
  setParamet(pstmt, prams);
  logger.info("###############::执行SQL语句操作(查询数据):" + strSql);
  rs = pstmt.executeQuery();
  con.commit();
  if (null != rs) {
   return convertResultSetToArrayList(rs);
  }
  return null;
 }

 /**
  * 执行存储过程(查询数据 无参数)
  *
  * @param procName
  *            存储过程名称
  * @return 数组列表对象
  * @throws Exception
  */
 public ArrayList<HashMap<Object, Object>> executeProcedureQuery(
   String procName) throws Exception {
  getConnection();// 获取连接
  String callStr = "{call " + procName + "}";// 构造执行存储过程的sql指令
  CallableStatement cs = con.prepareCall(callStr);
  logger.info("###############::执行存储过程(查询数据):" + procName);
  rs = cs.executeQuery();
  con.commit();
  cs.close();
  close_DB_Object();
  return convertResultSetToArrayList(rs);
 }

 /**
  * 执行存储过程(查询数据,带参数)返回结果集合
  *
  * @param procName
  *            存储过程名称
  * @param parameters
  *            参数对象数组
  * @param al
  *            数组列表对象
  * @return 数组列表对象
  * @throws Exception
  */
 public ArrayList<HashMap<Object, Object>> executeProcedureQuery(
   String procName, Object[] parameters) throws Exception {
  int parameterPoint = 0;
  // 获取存储过程信息列表集合
  ArrayList<HashMap<Object, Object>> procedureInfo = getProcedureInfo(procName);
  // 获取存储过程的完全名称
  String procedureCallName = getProcedureCallName(procName,parameters.length);
  // 获取连接对象
  getConnection();
  // 初始化 存储过程 执行对象
  CallableStatement cs = con.prepareCall(procedureCallName);
  // 参数下标变量
  int index = 0;
  // 获取 存储过程信息列表集合的 迭代器 对象
  Iterator<HashMap<Object, Object>> iter = procedureInfo.iterator();
  // 遍历存储过程信息列表集合
  while (iter.hasNext()) {
   HashMap<Object, Object> hm = iter.next();

   parameterPoint++;
   // 如果参数是输入参数 way = 0
   if (hm.get("WAY").equals("0")) {
    // 设置参数到cs
    cs.setObject(parameterPoint, parameters[index]);
    // 参数下标+1
    index++;
   }
  }
  // 释放这个对象,做为第二次使用
  procedureInfo = null;
  logger.info("###############::执行存储过程(查询数据):::::" + procedureCallName);
  rs = cs.executeQuery();
  con.commit();
  procedureInfo = convertResultSetToArrayList(rs);
  cs.close();
  close_DB_Object();
  return procedureInfo;

 }

 /**
  * 执行存储过程(更新,查询数据[简单查询、非纪录集],返回输出参数[非纪录集])
  *
  * @param procName
  *            存储过程名称
  * @param parameters
  *            参数对象数组
  * @param os
  *            输出参数对象数组
  * @return 输出参数对象数组
  * @throws Exception
  */
 public Object[] executeProcedureUpdate(String procName, Object[] parameters)
   throws Exception {
  logger.info("------------------------------------------------------------------------------------------------------");
  logger.info(" Run --> executeProcedureUpdate ##############   正在执行 存储过程: " + procName +"   ##############");
  CallableStatement cs = null;
  Object []returnVal = null;
  try {
  // 获取 存储过程 调用全名
  String fullPCallName = getProcedureCallName(procName,parameters.length);
  logger.info(" Run --> executeProcedureUpdate #   存储过程命令: " + fullPCallName +"   #");
  //获取存储过程参数信息
  ArrayList<HashMap<Object, Object>> p_Call_Info_List = getProcedureInfo(procName);
  //获取连接
  getConnection();
  //创建 存储过程 执行对象
  cs = con.prepareCall(fullPCallName);
  //数组下标
  int index = 1;
  //输出参数下标 纪录
        ArrayList<Integer> outPutIndexList = new ArrayList<Integer>();
        logger.info(" Run --> executeProcedureUpdate #   参数个数是: " + parameters.length +"   #");
  for(HashMap<Object,Object> tempHash:p_Call_Info_List)
  {
   if("0".equals(tempHash.get("WAY")))
      {
    //设置输入参数
    cs.setObject(index, parameters[index-1]);
    logger.info(" Run --> executeProcedureUpdate #   输入 Input: 编号:" + index +" 值: "+parameters[index-1]+" 类型: "+parameters[index-1].getClass()+"   #");
   }
   else
   {
    //注册输出参数
    cs.registerOutParameter(index, getDataType(tempHash.get("TYPENAME").toString()));
    //纪录输出参数的下标
    outPutIndexList.add(index);
    logger.info(" Run --> executeProcedureUpdate #   输出 OutPut: 编号:" + index +" 值: "+parameters[index-1]+" 类型: "+parameters[index-1].getClass()+"   #");
   }
   index++;
  }
  logger.info(" Run --> executeProcedureUpdate #   参数设置完毕,正在执行中 ... :   #");
  
  //-------------------- 执行 -----------------
  if(!cs.execute())
  {
   returnVal = new Object[outPutIndexList.size()];
   logger.info(" Run --> executeProcedureUpdate #   执行成功! :   #");
   //取输 出参数的 返回值
   for(int i = 0 ;i<outPutIndexList.size();i++)
   {
    returnVal[i] = cs.getObject(outPutIndexList.get(i));
    logger.info(" Run --> executeProcedureUpdate #   返回值 "+(i+1)+" "+returnVal[i]+"   #");
   }
   con.commit();//提交
  }
  } catch (Exception e) {
   logger.info(" Run --> executeProcedureUpdate #   执行失败!事务回滚中... :   #");
   con.rollback();
   throw e;
  }
  logger.info("------------------------------------------------------------------------------------------------------");
  return returnVal;
 }

 /** ****************************************************************************************** */
 /**
  * ********************************* 小工具
  * ************************************************
  */
 /** ****************************************************************************************** */

 /**
  * 关闭数据对象
  */
 public void close_DB_Object() {
  logger.info("###############close:::::关闭连接对象,语句对象,记录集对象");
  if (null != rs) {
   try {
    rs.close();
   } catch (SQLException ex) {
    rs = null;
   }
  }
  if (null != stmt) {
   try {
    stmt.close();
   } catch (SQLException ex) {
    stmt = null;
   }
  }
  if (null != pstmt) {
   try {
    pstmt.close();
   } catch (SQLException ex) {
    pstmt = null;
   }
  }
  if (con != null) {
   dcm.freeConnection("mysql", con);
  }
 }


 /**
  * 设置Sql 指令参数
  *
  * @param p_stmt
  *            PreparedStatement
  * @param pramets
  *            HashMap
  */
 private PreparedStatement setParamet(PreparedStatement p_stmt,
   HashMap<Integer, Object> pramets) throws ClassNotFoundException,
   SQLException {
  // 如果参数为空
  if (null != pramets) {
   // 如果参数个数为0
   if (0 <= pramets.size()) {
    for (int i = 1; i <= pramets.size(); i++) {
     try {
      // 字符类型 String
      if (pramets.get(i).getClass() == Class
        .forName("java.lang.String")) {
       p_stmt.setString(i, pramets.get(i).toString());
      }
      // 日期类型 Date
      if (pramets.get(i).getClass() == Class
        .forName("java.sql.Date")) {
       p_stmt.setDate(i, java.sql.Date.valueOf(pramets
         .get(i).toString()));
      }
      // 布尔类型 Boolean
      if (pramets.get(i).getClass() == Class
        .forName("java.lang.Boolean")) {
       p_stmt.setBoolean(i, (Boolean) (pramets.get(i)));
      }
      // 整型 int
      if (pramets.get(i).getClass() == Class
        .forName("java.lang.Integer")) {
       p_stmt.setInt(i, (Integer) pramets.get(i));
      }
      // 浮点 float
      if (pramets.get(i).getClass() == Class
        .forName("java.lang.Float")) {
       p_stmt.setFloat(i, (Float) pramets.get(i));
      }
      // 双精度型 double
      if (pramets.get(i).getClass() == Class
        .forName("java.lang.Double")) {
       p_stmt.setDouble(i, (Double) pramets.get(i));
      }

     } catch (ClassNotFoundException ex) {
      throw ex;
     } catch (SQLException ex) {
      throw ex;
     }
    }
   }
  }
  return p_stmt;
 }

 /**
  * 转换记录集对象为数组列表对象
  *
  * @param rs
  *            纪录集合对象
  * @return 数组列表对象
  * @throws Exception
  */
 private ArrayList<HashMap<Object, Object>> convertResultSetToArrayList(
   ResultSet rs) throws Exception {
  logger.info("###############::转换记录集对象为数组列表对象");
  // 获取rs 集合信息对象
  ResultSetMetaData rsmd = rs.getMetaData();
  // 创建数组列表集合对象
  ArrayList<HashMap<Object, Object>> tempList = new ArrayList<HashMap<Object, Object>>();
  HashMap<Object, Object> tempHash = null;
  // 填充数组列表集合
  while (rs.next()) {
   // 创建键值对集合对象
   tempHash = new HashMap<Object, Object>();
   for (int i = 0; i < rsmd.getColumnCount(); i++) {
    // 遍历每列数据,以键值形式存在对象tempHash中
    tempHash.put(rsmd.getColumnName(i + 1).toUpperCase(), rs
      .getString(rsmd.getColumnName(i + 1)));
   }
   // 第一个键值对,存储在tempList列表集合对象中
   tempList.add(tempHash);
  }
  close_DB_Object();// 关闭相关链接
  return tempList;// 返回填充完毕的数组列表集合对象
 }

 /**
  * 从数据库得到存储过程信息
  *
  * @param procName
  *            存储过程名称
  * @return 数组列表对象
  * @throws Exception
  */
 private ArrayList<HashMap<Object, Object>> getProcedureInfo(String procName)
   throws Exception {
  return this.executeSql("select Syscolumns.isoutparam as Way,systypes.name as TypeName from sysobjects,syscolumns,systypes where systypes.xtype=syscolumns.xtype and syscolumns.id=sysobjects.id and sysobjects.name='"
    + procName + "' order by Syscolumns.isoutparam");
 }

 /**
  * 从数据库得到存储过程参数个数
  *
  * @param procName
  *            存储过程名称
  * @return 数组列表对象
  * @throws Exception
  */
 @SuppressWarnings("unused")
 private int getParametersCount(String procName) throws Exception {
  int returnVal = 0;
  for (HashMap<Object, Object> tempHas : this
    .executeSql("select count(*) as RowsCount from sysobjects,syscolumns,systypes where systypes.xtype=syscolumns.xtype and syscolumns.id=sysobjects.id and sysobjects.name='"
      + procName + "'")) {
   returnVal = Integer.parseInt(tempHas.get("ROWSCOUNT").toString());
  }
  return returnVal;
 }

 /**
  * 得到调用存储过程的全名
  *
  * @param procName
  *            存储过程名称
  * @return 调用存储过程的全名
  * @throws Exception
  */
 private String getProcedureCallName(String procName, int prametCount)
   throws Exception {
  String procedureCallName = "{call " + procName;
  for (int i = 0; i < prametCount; i++) {
   if (0 == i) {
    procedureCallName = procedureCallName + "(?";
   }
   if (0 != i) {
    procedureCallName = procedureCallName + ",?";
   }
  }
  procedureCallName = procedureCallName + ")}";
  return procedureCallName;
 }

 /**
  * 得到数据类型的整型值
  *
  * @param typeName
  *            类型名称
  * @return 数据类型的整型值
  */
 private int getDataType(String typeName) {
  if (typeName.equals("varchar"))
   return Types.VARCHAR;
  if (typeName.equals("int"))
   return Types.INTEGER;
  if (typeName.equals("bit"))
   return Types.BIT;
  if (typeName.equals("float"))
   return Types.FLOAT;
  return 0;
 }

 // 设置驱动路径
 @SuppressWarnings("static-access")
 public void set_DRIVER(String _DRIVER) {
  this._DRIVER = _DRIVER;
 }

 // 设置数据库密码
 @SuppressWarnings("static-access")
 public void set_PASSWORD(String _PASSWORD) {
  this._PASSWORD = _PASSWORD;
 }

 // 设置数据库连接字符串
 @SuppressWarnings("static-access")
 public void set_URL(String _URL) {
  this._URL = _URL;
 }

 // 设置数据库用户名
 @SuppressWarnings("static-access")
 public void set_USER_NA(String _USER_NA) {
  this._USER_NA = _USER_NA;
 }

}

分享到:
评论

相关推荐

    南大通用数据库驱动 jar 包

    南大通用数据库驱动的jar包内包含了所有必要的类和方法,使得Java程序能够识别并通信于南大通用数据库系统。 南大通用数据库,全称是南京大学通用数据库管理系统(GBase),是由南京大学计算机科学与技术系开发的高...

    《java 数据库编程实例》 源代码

    源代码中的相关Java类文件可能包括数据库驱动程序,如mysql-connector-java.jar,它是连接MySQL数据库的必备驱动。安装这些驱动后,JDBC才能识别并连接到对应的数据库。 总而言之,这个资源包为学习和实践Java...

    Java与数据库的连接1.ppt

    【Java与数据库的连接】是Java开发中的重要一环,主要依赖于Java Database Connectivity(JDBC)技术。JDBC是Sun Microsystems(现为Oracle公司)为Java开发者设计的一套用于访问数据库的API,使得Java程序可以与...

    数据库连接的bean

    - `DBUtil.java`:可能是一个工具类,包含了一些静态方法,用于简化数据库操作,比如提供一个通用的连接获取方法。 - `pom.xml`或`build.gradle`:构建文件,定义了项目依赖,可能包括了JDBC驱动库。 - `README.md`...

    神通数据库帮助手册(SQL语言手册,数据库备份恢复,读写分离集群,数据库审计,数据库进程守护,数据库接口.zip

    神通数据库帮助手册是一份全面介绍神通数据库操作和管理的资源集合,涵盖了多个关键领域,包括SQL语言、数据库备份与恢复、读写分离集群、数据库审计、数据库进程守护以及数据库接口。下面将对这些主题进行详细阐述...

    IBM DB2通用数据库入门

    【IBM DB2通用数据库入门】是一本专为初学者设计的指南,旨在帮助读者快速掌握IBM的DB2数据库系统的基本概念、安装配置以及SQL查询语言的使用。这本书以全中文的形式,降低了学习门槛,使非英语背景的读者也能轻松...

    java 连接池

    在使用DBCP连接池时,我们需要将这个驱动添加到类路径中,以便DBCP能识别并建立与MySQL数据库的连接。 在配置DBCP连接池时,通常需要设置以下参数: - **driverClassName**:指定JDBC驱动类名,对于MySQL,这个值...

    jdbc连接数据库(oracle、sqlserver)代码

    在实际开发中,为了代码的复用性和健壮性,通常会封装一个通用的数据库连接方法,该方法接受数据库类型、连接参数等作为输入,返回一个连接对象。这样,无论是Oracle还是SQL Server,都可以通过调用同一个接口实现...

    java百度OCR文字识别名片信息补充

    百度AI开放平台提供了多种OCR服务,包括通用文字识别、表格文字识别、身份证识别等,其中名片识别是专门针对名片设计的,能够准确地识别名片上的姓名、职位、公司名、电话号码、邮箱地址等关键信息。 要实现这个...

    db2通用数据库自学教程

    - JDBC和ODBC:数据库连接接口,使得DB2能与其他编程语言(如Java、C++)交互。 - API和SDK:提供开发工具和文档,帮助开发者编写应用程序。 通过本教程的学习,你将能够熟练地运用DB2数据库解决实际问题,为你的...

    IBM DB2通用数据库Windows版快速入门

    **IBM DB2通用数据库Windows版快速入门** IBM DB2是一款由国际商业机器公司(IBM)开发的高性能关系型数据库管理系统,适用于多种操作系统平台,包括Windows。本快速入门指南将带领你了解如何在Windows环境中安装、...

    JAVA上百实例源码以及开源项目源代码

    [ConfigLine.java] 控制条类 [RoundBox.java] 限定选择控件 [MonthMaker.java] 月份表算法类 [Pallet.java] 调色板,统一配色类 Java扫雷源码 Java生成自定义控件源代码 2个目标文件 Java实现HTTP连接与浏览,...

    各类数据库的驱动(jdbc连接)

    在IT领域,数据库是存储和管理数据的核心工具,而JDBC(Java Database Connectivity)则是Java程序与各种数据库进行交互的标准接口。本资源包包含了不同类型的数据库驱动,它们是连接Java应用程序到特定数据库的关键...

    java开源包4

    BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K(运行时需要slf4j和guava的支持,这二者加...

    JDBC链接数据库

    2. `CallableStatement`:用于执行数据库存储过程。 创建`Statement`实例的代码示例: ```java Statement stmt = con.createStatement(); ``` 创建`PreparedStatement`实例的代码示例: ```java String sql = ...

    java开源包6

    BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K(运行时需要slf4j和guava的支持,这二者加...

    java开源包9

    BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K(运行时需要slf4j和guava的支持,这二者加...

    java开源包101

    BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K(运行时需要slf4j和guava的支持,这二者加...

    java开源包5

    BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K(运行时需要slf4j和guava的支持,这二者加...

Global site tag (gtag.js) - Google Analytics