`
cyber_Y
  • 浏览: 11937 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

The Java EE 6 Tutorial 第32章 JPA简介 (一) 实体

阅读更多
第六部分



持久化

第六部分讲述了Java持久化API (JPA),本部分主要包括下述各章:

■        第 32, “JPA简介”
■        第 33 章, “运行持久化的示例”
■        第 34 章, “Java持久化查询语言(Java Persistence Query Language)”
■        第 35 章, “使用Criteria API创建查询”
■        第 36 章, “创建及使用基于字符串的条件(Criteria)查询”
■        第 37 章, “用锁机制控制对实体数据的并发访问”
■        第 38 章, “设置二级缓存来改进JPA应用的性能”




第32章
JPA简介


JPA(Java Persistence API)为Java开发者提供了一个对象关系映射工具,来在Java应用中管理关系数据 。Java持久化由四个领域组成:

■        持久化API (Java Persistence API, JPA)
■        查询语言
■        Java持久化查询条件接口 (( Java Persistence Criteria API)
■        对象关系映射的元数据

下列内容可以在如下位置找到:

■   “实体”  on page 583
■    “实体继承” on page 595
■     “管理实体” on page 599
■     “查询实体” on page 604
■     “关于持久化的更多信息” on page 605



实体

一个实体是一个轻量级的持久化领域对象。一般来说,一个实体代表了关系数据库中的一个表,而每个实体对象对应表中的每一行。编程实体时,主要编写的就是实体类,不过实体也可以使用帮助类。

实体的持久化状态,通过持久化字段或者持久化特性(properties)来体现。这些字段或者特性(properties)使用对象关系映射注解,来将实体和实体间的关系,映射到底层数据存储的关系数据上。



实体类的要求

一个实体类必须满足如下要求:

■        类必须用javax.persistence.Entity注解
■ 类必须有public的或者protected的,无参的构造函数,类可以有其他的构造函数
■ The类必须未被定义为final.且方法或者持久化实体变量也都未被定义为final(???No methods or persistent instance variables must be declared final.)
■ If 如果一个实体实例以游离对象的形式传递(passed by value as a detached object),例如通过一个session bean的远程业务接口(remote business interface),则此类必须实现了Serializable接口
■ 实体可以继承实体或者非实体类,而且非实体类也可以继承实体类
■ Persistent instance持久化实体变量必须被定义为private, protected, 或者package-private (即默认访问权限) ,并且只可以被实体类的方法直接访问。客户端(此处client是相对host而言)必须通过访问器(accessor)或者业务方法(business method)访问实体类


实体类中的持久化字段和属性

一个实体的持久化状态可以通过实体实例的变量(variables)和属性(properties)来访问。字段或者属性必须是如下的Java语言类型:

■       Java 基本类型(primitive types)
■        java.lang.String
■        其他的serializable(可序列化)类型,包括:
■        Java基本类型的封装器 (Integer, Double之流)
■        java.math.BigInteger
■        java.math.BigDecimal
■        java.util.Date
■        java.util.Calendar
■        java.sql.Date
■        java.sql.Time
■        java.sql.TimeStamp
■        用户定义的可序列化(serializable)类型
■        byte[]
■        Byte[]
■        char[]
■        Character[]
■        枚举(Enumerated)类型



■        其他实体或者实体的集合
■        嵌入类(Embeddable classes)

实体可能使用持久字段,持久特性(properties),或者两者结合使用。如果映射注解放在实例变量上,实体就使用持久化字段。如果映射注解放在实体的getter方法上,来实现JavaBean式的特性(properties),实体就使用持久化特性(properties)。


持久化字段
如果实体类使用持久化字段,持久化运行时容器会直接访问实体类实例的变量(译注:即使用反射机制,绕过getter方法)所有未被注解为javax.persistence.Transient的字段且未使用Java关键字transient标记的字段,都会被持久化到数据存储中。此时对象关系映射注解必须应用在实例变量上。

持久化特性(Properties)
如果实体使用持久化特性,实体必须遵守JavaBean组件的方法命名约定。JavaBean风格的特性使用getter和setter方法,其通常是跟据实体类的实例变量的名称来命名的。对于实体类型Type的每项持久化特性property,都有一个getter方法叫做getProperty,一个setter方法叫做setProperty。如果一个特性是Boolean值,则你可以使用isProperty来代替getProperty。例如如果一个客户实体使用持久化特性,其有一个私有实例变量叫做firstName,则类中需定义getFirstName和setFirstName方法来取出及设定firstName实例变量的状态。


单值持久化特性(properties)方法的方法签名如下所示:

Type getProperty()
void  setProperty(Type type)

持久化特性的对象关系映射注解必须应用在getter方法上。映射注解不可以应用于被注解为@Transient或者被标注为transient的字段或者属性上。


在实体字段和特性(Properties)中使用集合
集合值类型的持久化字段和特性必须使用可支持的Java集合接口,不管实体使用的是持久化字段还是特性(properties)。下面是可以使用的集合接口:

■        java.util.Collection
■        java.util.Set



■        java.util.List
■        java.util.Map

如果实体类使用持久化字段,那么处理方法的方法签名中的类型就必须是以上集合类型。集合类型也可能使用泛型变量。例如,如果实体类有一个持久化特性包括一个电话号码的集合,那么Customer实体就得有以下方法:

Set<PhoneNumber>  getPhoneNumbers()  { ... }
void  setPhoneNumbers(Set<PhoneNumber>) { ... }

如果一个实体的字段或者特性(property)由基本不类型或者嵌入类型的集合组成,则使用javax.persistence.ElementCollection来注解该字段或特性(property)。

@ElementCollection的两个属性是targetClass和fetch. targetClass属性指定了基本类或嵌入类的名称,如果字段或特性(property)已经使用Java泛型定义了的话,该属性也是可省略的。可选属性fetch用来指定集合取出时,使用延迟加载模式(lazily)还是提前加载模式(eagerly)。javax.persistence.FetchType的常量LAZY、EAGER分别设定。默认情况下,集合都是延迟加载模式的。

下面的Person实体有一个持久化字段nicknames,该字段是一个String类型的集合,取出方式为提前加载。其中targetClass元素不是必需的,因为该实体已经使用泛型定义字段了。

@Entity
public class Person  {
...
@ElementCollection(fetch=EAGER)
protected  Set<String> nickname = new HashSet();
...
}

实体元素及其关系的集合可以使用java.util.Map来展现。一个Map由一个key和一个value组成。

当使用Map元素或者关系时,要满足以下要求:

■ Map的key和value必须是基础的Java语言类型(不是指primitive类型)、嵌入类、或者实体。
■        当Map的值value是内嵌类或基础的类型是,使用@ElementCollection注解
■        当Map的值value是实体时,使用@OneToMany或@ManyToMany注解
■       只在双向关系的一方中使用Map类型

如果Map的key类型是Java语言的基础类型,则使用注解javax.persistence.MapKeyColumn来将列映射到key。默认情况下,@MapKeyColumn的name属性是如下形式:



RELATIONSHIP-FIELD/PROPERTY-NAME_KEY。例如,如果引用的是一个叫做image的关系字段,那么默认的name属性就是IMAGE_KEY。

If the key type of a Map is an entity, use the javax.persistence.MapKeyJoinColumn annotation. If the multiple columns are needed to set the mapping, use the annotation javax.persistence.MapKeyJoinColumns to include multiple @MapKeyJoinColumn
annotations. If no @MapKeyJoinColumn is present, the mapping column name is by default set to RELATIONSHIP-FIELD/PROPERTY-NAME_KEY. For example, if the relationship field name is employee, the default name attribute is EMPLOYEE_KEY.

If Java programming language generic types are not used in the relationship field or property, the key class must be explicitly set using the javax.persistence.MapKeyClass annotation.

If the Map key  is the primary key or a persistent field or property of the entity that is the Map value, use the javax.persistence.MapKey annotation. The @MapKeyClass and @MapKey annotations cannot be used on the same field or property.

If the Map value is a Java programming language basic type or an embeddable class, it will be mapped as a collection table in the underlying database. If generic types are not used, the
@ElementCollection annotation’s targetClass attribute must be set to the type of the Map
value.

If the Map value is an entity and part of a many-to-many or one-to-many unidirectional relationship, it will be mapped as a join table in the underlying database. A unidirectional one-to-many relationship that uses a Map may also be mapped using the @JoinColumn annotation.

If the entity is part of a one-to-many/many-to-one bidirectional relationship, it will be mapped in the table of the entity that represents the value of the Map. If generic types are not used, the targetEntity attribute of the @OneToMany and @ManyToMany annotations must be set to the type of the Map value.

校验持久化字段(Field)和特性(Property)
Java Bean校验(Bean Validation)的Java API提供了一个校验应用数据的机制。Java EE容器已经整合了Bean校验器,这样就可以将相同的校验逻辑应用到企业应用中的任何一层中。

Bean 校验约束可以应用到持久实体类、嵌入类及参与映射的父类上。默认情况下,在PrePersist、PreUpdate、PreRemove生命周期事件之后,持久化实现提供者(Persistence provider)会立刻自动在有Bean校验约束注解的实体上进行校验。

校验约束是应用于Java类字段或特性的注解。Bean校验提供了一套约束及使用自定义约束的API。自定义约束可以是默认约束的某种组合,也可以是不使用默认约束的新约

束。每个约束至少和一个校验类关联,校验类负责校验受约束字段或特性。自定义约束的开发者也必须为期约束提供一个校验类。

Bean校验约束应用于持久类的持久化字段或者特性。添加Bean校验约束时,要在持久类上使用同样的访问策略。即如果持久类使用字段访问,则Bean校验约束注解在类的字段上,如果类使用特性访问,则约束应用于getter方法上。

表 9–2 javax.validation.constraints内置Bean校验器列表


表 9–2 中所有的内置约束都有相应的注解;ConstraintName.List, 用来将一个字段或特性上的多个同类注解组织起来。例如,下面的持久字段有两个@Pattern约束:

@Pattern.List({
@Pattern(regexp="..."),
@Pattern(regexp="...")
})

下面的实体类,Contact,有应用于其持久字段上的Bean校验约束。

@Entity
public class Contact  implements  Serializable {
private static final long  serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
protected String firstName;
@NotNull
protected  String  lastName;
@Pattern(regexp="[a-z0-9!#$%&’*+/=?^_‘{|}~-]+(?:\\."
+"[a-z0-9!#$%&’*+/=?^_‘{|}~-]+)*@"
+"(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", message="{invalid.email}")
protected String email;
@Pattern(regexp="^\\(?(\\d{3})\\)?[- ]?(\\d{3})[-  ]?(\\d{4})$", message="{invalid.phonenumber}")
protected  String  mobilePhone;
@Pattern(regexp="^\\(?(\\d{3})\\)?[- ]?(\\d{3})[-  ]?(\\d{4})$", message="{invalid.phonenumber}")
protected String homePhone;
@Temporal(javax.persistence.TemporalType.DATE)
@Past
protected Date birthday;
...
}

在firstName和lastName字段上的@NotNull注解指明了这些字段是必填项。如果一个新的Contact实例被创建,但firstName和lastName并没有被初始化,则bean校验器会抛出一个校验错误。类似的,如果以前创建的Contact实例已经被修改,firstName和lastName被置为null,也会导致抛出校验错误。




email字段有一个@Pattern约束,其有一个正则表达式来匹配大多数有效的email地址。如果
email的值和正则表达式不匹配,就会抛出一个校验错误。


homePhone和mobilePhone字段有相同的@Pattern约束。正则表达式匹配美国加拿大的10位电话号码:(xxx) xxx–xxxx。

birthday字段被注解为@Past约束,即要保证birthday的值一定是过去的日期。



实体的主键

每个实体都有一个唯一对象表示。例如,一个自定义实体可能用客户编号标示。这种唯一标示,或者说主键(primary key) 使客户端可以找到特定的实体实例。每个实体都必须有一个主键。一个实体可能有一个简单的或者复合的主键。

简单主键使用javax.persistence.Id注解来标示主键特性或字段。

当构成主键的属性不止一个时,就使用复合主键,其为单个持久特性或字段的集合。复合主键必须被定义在主键类中。用javax.persistence.EmbeddedId和 javax.persistence.IdClass注解标示复合主键。

主键或者复合主键的特性、字段,必须是以下Java类型之一:

■       Java 基本类型(primitive types)
■       Java基本类型封装类(primitive wrapper types)
■        java.lang.String
■        java.util.Date (the temporal type should be DATE)
■        java.sql.Date
■        java.math.BigDecimal
■        java.math.BigInteger

绝不应该使用浮点(Floating-point)类型作为主键。如果你使用生成主键,只有整型(integral types)是可移植的(portable)。




(译注:这里integral types不知道是否包含Integer)



一个主键类必须满足如下要求:

■        类的访问控制修饰符必须是 public
■ 主键类的特性必须是public或protected的,如果使用基于特性的访问方式
■        类必须有一个public的默认构造器
■        类必须实现hashCode() and equals(Object other)方法
■        类必须是serializable的
■ 一个复合主键要么用实体类的多个字段或特性来表示、映射,要么就用嵌入类来表示、映射
■ 如果主键类映射到实体的多个字段或者特性,其字段或者特性的名字和类型必须和其实体类的相应字段匹配

下面的主键类是一个复合主键,其orderId和itemId字段一起唯一标示一个实体:

public final  class LineItemKey implements  Serializable {
public Integer orderId;
public int  itemId;

public LineItemKey()  {}

public  LineItemKey(Integer orderId, int itemId) {
this.orderId = orderId;
this.itemId = itemId;
}

public boolean  equals(Object otherOb)  {
if (this == otherOb)  {
return true;
}
if (!(otherOb instanceof  LineItemKey))  {
return false;
}
LineItemKey other = (LineItemKey)  otherOb;
return (
(orderId==null?other.orderId==null:orderId.equals
(other.orderId)
)
&&
(itemId == other.itemId)
);
}

public int hashCode()  {
return (
(orderId==null?0:orderId.hashCode())
^
((int) itemId)
);



}

public String toString()  {
return "" + orderId + "-"  + itemId;
}
}


实体关系中的多重关系(Multiplicity)

多重关系有以下几种类型:one-to-one, one-to-many, many-to-one, and many-to-many:

■ One-to-one: 每个实体实例都只和另一个实体的一个实例关联。例如要对一个物理仓库建模,仓库中每个贮存箱(storage bin)都配有一套设备(widget),那StorageBin和Widget就是one-to-one关系。One-to-one关系需要在相应的持久化特性或字段上使用javax.persistence.OneToOne注解。
■ One-to-many: 一个实体实例可能关联到另一个实体的多个实例。例如,一个销售订单可以有多个条目。每个订单申请中,Order就会和LineItem形成one-to-many的关系。One-to-many关系需要在相应的持久化特性或字段上使用javax.persistence.OneToMany注解。
■ Many-to-one: 一个实体的多个实例可能和另一个实体的一个实例关联。这种多重关系是one-to-many的反面。在上述提到的例子中,从上述的LineItem到Order的关系就是many-to-one的关系。Many-to-one关系需要在相应的持久化特性或字段上使用javax.persistence.ManyToOne注解。
■ Many-to-many: 实体实例可以被互相关联到多个实例。例如,每个门课程(Course)都有多个学生(Student),而每个学生也都可以选多门课程。因此,在一个登记表中,Course 和Student之前就有many-to-many关系。Many-to-many关系需要在相应的持久化特性或字段上使用javax.persistence.ManyToMany注解。


实体关系中的方向

关系的方向可以是双向(bidirectional)的或单向的(unidirectional)。一个双向的关系中,只有一个所有方(owning side)。这个所有方决定了持久运行时容器如何在数据库中进行更新双方。


双向关系
在双向(bidirectional)关系中,每个实体都有一个关系字段或特性,用来指向另一个实体。通过关系字段或特性,一个实体类的代码可以访问它所关联的对象。如果一个

实体。如果实体有一个关联字段,那就称作该实体“知道 (know)”它的关联对象。例如,如果一个Order知道他有什么LineItem实例且LineItem能知道他所属于的Order,那他们之前就是一个双向关系。

双向关系必须满足以下规则:

■ 双向关系的对手方(inverse side)必须使用mappedBy元素注解来指明其所有方。mappedBy元素可以是@OneToOne, @OneToMany, @ManyToMany。mappedBy元素指定了主导方实体中的字段或特性。
■ 对于many-to-one双向关系中的many一方,不可以定义mappedBy元素 。many方通常是关系中的所有方。
■ 对于one-to-one双向关系,关系的所有方是带有外键的那方。
■        对于many-to-many双向关系,两次都可以做为所有方。


单向关系

在单向(unidirectional)关系中,只有一个实体拥有指向其他实体的字段或特性。例如LineItem可能有一个关系字段用来标示一个Product(产品),但是Product却没有关系字段或属性来确定LineItem。换句话说,LineItem知道Product,但是Product却不知道引用它的LineItem。


查询和关系方向

JPQL(Java Persistence query language)和Criteria API查询常通过关系来导航(译注:即引用)。关系的方向决定了查询是否可以从一个实体导航到另一个实体。例如,一个查询可以从LineItem导航到Product,但是却不能反向导航。而对Order和LineItem来说,查询则按两种方向都可以导航,因为两种实体间有一个双向关系。


级联(cascade)操作和关系

使用关系的实体通常都会依赖关系中的其他实体。例如一个条目是订单的一部分,如果订单删除了,则条目应该也被删除。这就叫做级联删除关系。

枚举类型javax.persistence.CascadeType定义了关系注解的级联(cascade)元素中可用级联操作。表32–1列出了实体可用的级联操作。




表 32–1    实体的级联操作

级联操作                                         Description描述

ALL 所有的级联操作都被应用于此父实体的关联实体。ALL等价于这么指定级联类型:cascade={DETACH, MERGE, PERSIST, REFRESH, REMOVE}

DETACH 若父实体从持久上下中游离(detached)出来,则关联实体也会游离。

MERGE 若父实体合并(merge)到持久化上下文,那关联实体也会被合并。

PERSIST 若父实体被持久化(persist)到持久化上下文,那关联实体也会被持久化。

REFRESH 若当前持久化上下文中父实体刷新(refresh)了,则关联实体也会刷新。

REMOVE 若复试题从当前持久化上下文中移除(remove),则关联实体也会移除。


级联删除关系通过cascade=REMOVE元素指定,可供@OneToOne和@OneToMany关系使用:

@OneToMany(cascade=REMOVE,  mappedBy="customer")
public Set<Order>  getOrders() { return orders; }


关系中的孤儿删除

对于处于一对一(one-to-one)或一对多(one-to-many)关系中的实体,其从所处的关系中移除时,通常需要将删除动作的级联到目标实体上。(译注:即当关系不存在时,实体失去意义,也需要被删除)。这种目标实体被认为是“孤儿(orphan)”(译注:孤儿通常指与其他对象无关系的对象),而 (orphanRemoval)属性可以用来指定应被删除的孤儿实体。例如,如果一个订单有多个条目,而其中一个从订单中删除了,那被删除的条目就被认为是一个孤儿。如果orphanRemoval被设置为true,则条目被从订单中移除时,该条目的实体就会被删除。

@OneToMany和@oneToOne中的orphanRemoval属性使用Boolean值作为参数,其默认值为false。

下面的例子中,当客户实体被从关系中移除时,删除操作会级联到这个孤儿实体上:

@OneToMany(mappedBy="customer",  orphanRemoval="true")
public List<Order>  getOrders() { ... }



实体中的嵌入类

嵌入类可用来反映实体的状态,但和实体类不一样的是,他们没有自己的持久化标识(ID)。嵌入类的示例和它所属的实体类共享一个标识。嵌入类只作为其他实体的状态而存在。一个实体可以有单值(single-valued)或集合的(collection-valued)嵌入类属性。

嵌入类和实体类有相同的限定条件,区别是使用注解javax.persistence.Embeddable来代替@Entity。

下面的嵌入类ZipCode有两个字段zip和plusFour:

@Embeddable
public class ZipCode {
String zip;
String plusFour;
...
}

这个嵌入类被用于Address实体:

@Entity
public class Address  {
@Id
protected long  id
String street1;
String street2;
String city;
String province;
@Embedded
ZipCode zipCode;
String country;
...
}

用嵌入类作为其持久化状态一部分的实体,而将(使用嵌入类的)字段或特性注解为javax.persistence.Embedded,但是这不是必须的。

嵌入类也可以使用其他嵌入类来描绘自己的特性。它们也可以包括 基本Java类型或其他嵌入类 的集合。如果嵌入类有一个关系(译注:这里指双向或单向关系),那关系两方则是从目标实体(或实体的集合)到拥有该嵌入类的实体(译注:即嵌入类上注解的关系属于所属实体)。
分享到:
评论

相关推荐

    The Java EE 6 Tutorial

    《Java EE 6 教程》是Oracle公司于2010年发布的一部详尽指导书籍,旨在帮助开发者深入理解和掌握Java Platform, Enterprise Edition(Java EE)平台的应用开发技术。Java EE 6作为企业级应用开发的标准框架,提供了...

    The Java EE 5Tutorial

    《Java EE 5 Tutorial》是Java企业版5(Java Platform, Enterprise Edition 5)的一份官方教程,旨在帮助开发者深入理解和掌握Java EE平台的核心技术。这份文档详细介绍了如何使用Java EE 5进行分布式应用程序的开发...

    The Java EE 6 Tutorial: Basic Concepts, 4th Edition

    《Java EE 6 Tutorial: Basic Concepts, Fourth Edition》是一本面向新手及中级Java开发者的指南书籍,旨在帮助他们深入理解Java平台企业版6(Java EE 6)的各项特性与技术。本书由Oracle公司Java EE 6文档团队成员...

    Java EE 6 tutorial

    Java EE 6(Java Platform, Enterprise Edition 6)是Java技术在企业级开发中的一个里程碑,它为构建可扩展、高性能、分布式的企业级应用程序提供了全面的框架和规范。本教程由Sun Microsystems(后被Oracle收购)...

    The Java EE 5 Tutorial

    The Java EE 5 Tutorial为学习Java EE 5的经典教材!一直由sun公司维护,不过为全英文版.

    The Java EE 6 Tutorial Basic Concepts 4th Edition

    Java EE 6 APIs in the Java Platform, Standard Edition 6.0 31 GlassFish Server Tools 34 Chapter 2: Using the Tutorial Examples 37 Required Software 37 Starting and Stopping the GlassFish Server ...

    The Java EE 7 tutorial

    根据给定的文件信息,我们将深入探讨与Java EE 7教程相关的关键知识点,这将包括对Java EE平台的理解、其核心组件以及版本7所引入的新特性。 ### Java EE(Java Platform, Enterprise Edition)概述 Java EE是Java...

    The Java EE 7 Tutorial

    《The Java EE 7 Tutorial》是一份针对Java EE 7平台的官方教程,旨在帮助开发者掌握Java EE 7的最新技术。Java EE(Java Platform, Enterprise Edition)是一个用来开发企业级应用的平台,它包括了一系列的标准和...

    The Java EE 6 Tutorial,Volume I.pdf

    《Java EE 6 教程,第一卷》是Sun Microsystems为开发者提供的一份详尽的指南,旨在深入解析Java EE 6平台的基本概念、架构和关键特性。此教程由Sun Microsystems编写,出版于2009年,是理解Java企业版(Java EE)第...

    Java EE 7 tutorial 中的examples代码

    Java EE 7(Java Platform, Enterprise Edition 7)是Java平台的一个版本,专注于为企业级应用程序开发提供服务。这个教程中的examples代码是学习Java EE 7技术的重要资源,它与官方教程紧密配合,帮助开发者深入...

    Java EE 5 Tutorial (CHM文件)

    《Java EE 5 Tutorial》是Java企业版5(Java Platform, Enterprise Edition 5,简称Java EE 5)的官方教程,旨在帮助开发者理解和掌握在Java平台上构建分布式、多层的企业级应用程序。本教程以CHM(Compiled ...

    Java EE 6 tutorial 配套example代码

    Java EE 6是Java平台企业版的一个重要版本,它提供了许多用于构建分布式、多层的企业级应用程序的框架和API。这个教程的配套example代码是为了帮助开发者深入理解和实践Java EE 6的各种技术特性而设计的。 Java EE ...

    The Java EE 6 Tutorial.Volume I.pdf

    ### Java EE 6 教程(第一卷):基础知识概览 #### 一、概述 《Java EE 6 教程》(第一卷)是一本详细介绍Java Platform, Enterprise Edition (Java EE) 6版本的基础概念和技术细节的专业书籍。本书由Sun ...

Global site tag (gtag.js) - Google Analytics