`

morphia与spring的整合

 
阅读更多

转自: http://www.blogjava.net/watchzerg/archive/2012/09/21/388291.html

 

 

最近研究mongoDB的各种pojo-mapping框架,中意的就两个:morphia和spring-data-mongodb。
本来想着spring-data-mongodb与spring的结合更紧密些,但悲剧的是其要求spring3.0.x以上版本,与生产环境不符。查了查stackoverflow,大家评价morphia更老牌更稳定一些,于是就用这个了。
研究了一番,果然与spring整合起来很麻烦。
首先看stackoverflow上的帖子,提问者跟我的想法完全一样:在spring里,我没有现成的办法调用ensureIndexes()这样的方法啊,肿么办?
http://stackoverflow.com/questions/5365315/using-morphia-with-spring
回答者给出的两个链接我也看了,真心没啥收获。
后来又搜到一篇帖子:
http://topmanopensource.iteye.com/blog/1449889
很粗略的看了一下,还不错,总之都得自己实现那些工厂类,完成与spring的集成。
看来网上这方面的需求还不少,甚至在google-code上找到一个项目叫“spring-morphia”,专门来解决这个问题:
http://code.google.com/p/spring-morphia/
貌似荒废已久,没有完成的可供下载的jar包,但是在其svn上,可以看到一些可供我们参考的类:
http://code.google.com/p/spring-morphia/source/browse/trunk/spring-morphia/src/main/java/com/so/smorphia/
本文基本上就是根据上面两个连接的思路写的,自己总结一下而已,不做过多解释了,代码里有注释。

首先我们需要一个生成和配置mongodb的工厂类:

 1  public  class  MongoFactoryBean extends  AbstractFactoryBean<Mongo> {
 2  
 3      //  表示服务器列表(主从复制或者分片)的字符串数组
 4      private  String[] serverStrings;
 5      //  mongoDB配置对象
 6      private  MongoOptions mongoOptions;
 7      //  是否主从分离(读取从库),默认读写都在主库
 8      private  boolean  readSecondary = false ;
 9      //  设定写策略(出错时是否抛异常),默认采用SAFE模式(需要抛异常)
10      private  WriteConcern writeConcern = WriteConcern.SAFE;
11  
12      @Override
13      public  Class<?> getObjectType() {
14          return  Mongo.class ;
15      }
16  
17      @Override
18      protected  Mongo createInstance() throws  Exception {
19          Mongo mongo = initMongo();
20          
21          //  设定主从分离
22          if  (readSecondary) {
23              mongo.setReadPreference(ReadPreference.secondaryPreferred());
24          }
25  
26          //  设定写策略
27          mongo.setWriteConcern(writeConcern);
28          return  mongo;
29      }
30      
31      /**
32       * 初始化mongo实例
33       *  @return
34       *  @throws  Exception
35        */
36      private  Mongo initMongo() throws  Exception {
37          //  根据条件创建Mongo实例
38          Mongo mongo = null ;
39          List<ServerAddress> serverList = getServerList();
40  
41          if  (serverList.size() == 0) {
42              mongo = new  Mongo();
43          }else  if (serverList.size() == 1){
44              if  (mongoOptions != null ) {
45                  mongo = new  Mongo(serverList.get(0), mongoOptions);
46              }else {
47                  mongo = new  Mongo(serverList.get(0));
48              }
49          }else {
50              if  (mongoOptions != null ) {
51                  mongo = new  Mongo(serverList, mongoOptions);
52              }else {
53                  mongo = new  Mongo(serverList);
54              }
55          }
56          return  mongo;
57      }
58      
59      
60      /**
61       * 根据服务器字符串列表,解析出服务器对象列表
62       * <p>
63       * 
64       * @Title: getServerList
65       *         </p>
66       * 
67       *  @return
68       *  @throws  Exception
69        */
70      private  List<ServerAddress> getServerList() throws  Exception {
71          List<ServerAddress> serverList = new  ArrayList<ServerAddress>();
72          try  {
73              for  (String serverString : serverStrings) {
74                  String[] temp = serverString.split(":");
75                  String host = temp[0];
76                  if  (temp.length > 2) {
77                      throw  new  IllegalArgumentException(
78                              "Invalid server address string: " + serverString);
79                  }
80                  if  (temp.length == 2) {
81                      serverList.add(new  ServerAddress(host, Integer
82                              .parseInt(temp[1])));
83                  } else  {
84                      serverList.add(new  ServerAddress(host));
85                  }
86              }
87              return  serverList;
88          } catch  (Exception e) {
89              throw  new  Exception(
90                      "Error while converting serverString to ServerAddressList",
91                      e);
92          }
93      }
94  
95      /*  ------------------- setters ---------------------  */
96  }

其次我们需要一个产生和配置morphia对象的工厂类:

 1  public  class  MorphiaFactoryBean extends  AbstractFactoryBean<Morphia> {
 2      /**
 3       * 要扫描并映射的包
 4        */
 5      private  String[] mapPackages;  
 6      
 7      /**
 8       * 要映射的类
 9        */
10      private  String[] mapClasses;  
11      
12      /**
13       * 扫描包时,是否忽略不映射的类
14       * 这里按照Morphia的原始定义,默认设为false
15        */
16      private  boolean  ignoreInvalidClasses;
17      
18      @Override
19      protected  Morphia createInstance() throws  Exception {
20          Morphia m = new  Morphia();
21          if  (mapPackages != null ) {
22              for  (String packageName : mapPackages) {
23                  m.mapPackage(packageName, ignoreInvalidClasses);
24              }
25          }
26          if  (mapClasses != null ) {  
27              for  (String entityClass : mapClasses) {
28                  m.map(Class.forName(entityClass));
29              }
30          }
31          return  m;
32      }
33  
34      @Override
35      public  Class<?> getObjectType() {
36          return  Morphia.class ;
37      }
38      
39      /* ----------------------setters----------------------- */
40  }

最后我们还需要一个产生和配置Datastore的工厂类:

 1  public  class  DatastoreFactoryBean extends  AbstractFactoryBean<Datastore> {
 2      
 3      private  Morphia morphia;    // morphia实例,最好是单例
 4      private  Mongo mongo;    // mongo实例,最好是单例
 5      private  String dbName;    // 数据库名
 6      private  String username;    // 用户名,可为空
 7      private  String password;    // 密码,可为空
 8      private  boolean  toEnsureIndexes=false ;    // 是否确认索引存在,默认false
 9      private  boolean  toEnsureCaps=false ;    // 是否确认caps存在,默认false
10      
11  
12      @Override
13      protected  Datastore createInstance() throws  Exception {
14          // 这里的username和password可以为null,morphia对象会去处理
15          Datastore ds = morphia.createDatastore(mongo, dbName, username,
16                  password==null ?null :password.toCharArray());
17          if (toEnsureIndexes){
18              ds.ensureIndexes();
19          }
20          if (toEnsureCaps){
21              ds.ensureCaps();
22          }
23          return  ds;
24      }
25  
26      @Override
27      public  Class<?> getObjectType() {
28          return  Datastore.class ;
29      }
30  
31      @Override
32      public  void  afterPropertiesSet() throws  Exception {
33          super .afterPropertiesSet();
34          if  (mongo == null ) {
35              throw  new  IllegalStateException("mongo is not set");
36          }
37          if  (morphia == null ) {
38              throw  new  IllegalStateException("morphia is not set");
39          }
40      }
41      
42      /* ----------------------setters----------------------- */
43  }

我们来仿照morphia文档,写两个测试的POJO:

 1  @Entity
 2  public  class  Hotel {
 3      @Id private  ObjectId id;
 4      
 5      private  String name;
 6      private  int  stars;
 7      
 8      @Embedded
 9      private  Address address;    
10      
11      /* -----------gettters & setters---------- */
12  }

1  @Embedded
2  public  class  Address {
3      private  String street;
4      private  String city;
5      private  String postCode;
6      private  String country;
7      /* -----------gettters & setters---------- */
8  }

还需要一个为测试POJO专门服务的DAO,这里继承morphia里的BasicDAO:

1  public  class  HotelDAO extends  BasicDAO<Hotel, ObjectId> {
2  
3      protected  HotelDAO(Datastore ds) {
4          super (ds);
5      }
6      
7      /*  ----------------以下是自定义的数据查询方法(finder)-----------------  */
8  }

最后是spring的XML文件:

  1  <? xml version="1.0" encoding="UTF-8" ?>   
  2  < beans  xmlns ="http://www.springframework.org/schema/beans"   
  3      xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"  xmlns:p ="http://www.springframework.org/schema/p"  xmlns:context ="http://www.springframework.org/schema/context"  
  4      xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
  5      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd" >   
  6      
  7      <!--  配置文件  -->
  8      < context:property-placeholder  location ="classpath:config.properties"   />
  9      
 10      <!--  mongoDB的配置对象  -->
 11      < bean  id ="mongoOptions"  class ="com.mongodb.MongoOptions" >
 12          <!--  服务器是否自动重连,默认为false  -->
 13          < property  name ="autoConnectRetry"  value ="false"   />
 14          <!--  对同一个服务器尝试重连的时间(毫秒),设为0时默认使用15秒  -->
 15          < property  name ="maxAutoConnectRetryTime"  value ="0"   />
 16          <!--  与每个主机的连接数,默认为10  -->
 17          < property  name ="connectionsPerHost"  value ="10"   />
 18          <!--  连接超时时间(毫秒),默认为10000  -->
 19          < property  name ="connectTimeout"  value ="10000"   />
 20          <!--  是否创建一个finalize方法,以便在客户端没有关闭DBCursor的实例时,清理掉它。默认为true  -->
 21          < property  name ="cursorFinalizerEnabled"  value ="true"   />
 22          <!--  线程等待连接可用的最大时间(毫秒),默认为120000  -->
 23          < property  name ="maxWaitTime"  value ="120000"   />
 24          <!--  可等待线程倍数,默认为5.例如connectionsPerHost最大允许10个连接,则10*5=50个线程可以等待,更多的线程将直接抛异常  -->
 25          < property  name ="threadsAllowedToBlockForConnectionMultiplier"  value ="5"   />
 26          <!--  socket读写时超时时间(毫秒),默认为0,不超时  -->
 27          < property  name ="socketTimeout"  value ="0"   />
 28          <!--  是socket连接在防火墙上保持活动的特性,默认为false  -->
 29          < property  name ="socketKeepAlive"  value ="false"   />
 30          <!--  对应全局的WriteConcern.SAFE,默认为false  -->
 31          < property  name ="safe"  value ="true"   />
 32          <!--  对应全局的WriteConcern中的w,默认为0  -->
 33          < property  name ="w"  value ="0"   />
 34          <!--  对应全局的WriteConcern中的wtimeout,默认为0  -->
 35          < property  name ="wtimeout"  value ="0"   />
 36          <!--  对应全局的WriteConcern.FSYNC_SAFE,如果为真,每次写入要等待写入磁盘,默认为false  -->
 37          < property  name ="fsync"  value ="false"   />
 38          <!--  对应全局的WriteConcern.JOURNAL_SAFE,如果为真,每次写入要等待日志文件写入磁盘,默认为false  -->
 39          < property  name ="j"  value ="false"   />
 40      </ bean >
 41      
 42      <!--  使用工厂创建mongo实例  -->
 43      < bean  id ="mongo"  class ="me.watchzerg.test.morphia.spring.MongoFactoryBean" >
 44          <!--  mongoDB的配置对象  -->
 45          < property  name ="mongoOptions"  ref ="mongoOptions" />
 46          
 47          <!--  是否主从分离(读取从库),默认为false,读写都在主库  -->
 48          < property  name ="readSecondary"  value ="false" />
 49          
 50          <!--  设定写策略,默认为WriteConcern.SAFE,优先级高于mongoOptions中的safe  -->
 51          < property  name ="writeConcern"  value ="SAFE" />
 52          
 53          <!--  设定服务器列表,默认为localhost:27017  -->
 54          < property  name ="serverStrings" >
 55              < array >
 56                  < value > ${mongoDB.server}</ value >
 57              </ array >
 58          </ property >
 59      </ bean >
 60      
 61      
 62      <!--  使用工厂创建morphia实例,同时完成类映射操作  -->
 63      < bean  id ="morphia"  class ="me.watchzerg.test.morphia.spring.MorphiaFactoryBean"   >
 64          <!--  指定要扫描的POJO包路径  -->
 65          < property  name ="mapPackages" >
 66              < array >
 67                  < value > me.watchzerg.test.morphia.pojo</ value >
 68              </ array >
 69          </ property >
 70          
 71          <!--  指定要映射的类  -->
 72          <!--  <property name="mapClasses">
 73              <array>
 74                  <value>me.watchzerg.test.morphia.pojo.Hotel</value>
 75                  <value>me.watchzerg.test.morphia.pojo.Address</value>
 76              </array>
 77          </property>  -->
 78          
 79          <!--  扫描包时是否忽略不可用的类,默认为false  -->
 80          <!--  <property name="ignoreInvalidClasses" value="false"/>  -->
 81      </ bean >
 82      
 83      <!--  使用工厂创建datastore,同时完成index和caps的确认操作  -->
 84      < bean  id ="datastore"  class ="me.watchzerg.test.morphia.spring.DatastoreFactoryBean"   >
 85          < property  name ="morphia"  ref ="morphia" />
 86          < property  name ="mongo"  ref ="mongo" />
 87          
 88          <!--  collection的名称  -->
 89          < property  name ="dbName"  value ="${mongoDB.dbName}" />
 90          
 91          <!--  用户名和密码可以为空  -->
 92          <!--  <property name="username" value="my_username"/>
 93          <property name="password" value="my_password"/>  -->
 94          
 95          <!--  是否进行index和caps的确认操作,默认为flase  -->
 96          < property  name ="toEnsureIndexes"  value ="true" />
 97          < property  name ="toEnsureCaps"  value ="true" />
 98      </ bean >
 99      
100      <!--  ===============以下是具体DAO的实现=====================  -->
101      
102      < bean  id ="hotelDAO"  class ="me.watchzerg.test.morphia.dao.impl.HotelDAO" >
103          < constructor-arg  ref ="datastore" />
104      </ bean >
105      
106  </ beans >  

最后写一个测试类看看我们的成果:

  1  public  class  MorphiaTest {
  2      private  static  HotelDAO hotelDAO;
  3  
  4      /**
  5       * 测试Morphia的DAO层
  6       * 
  7       *  @param  args
  8       *  @throws  Exception
  9        */
 10      public  static  void  main(String[] args) throws  Exception {
 11          //  初始化DAO
 12          initDAO();
 13  
 14          //  插入测试
 15          saveTest();
 16  
 17          //  更新测试
 18           //  updateTest();
 19  
 20           //  删除测试
 21           //  deleteTest();
 22  
 23           //  查询测试
 24           //  queryHotel();
 25  
 26          System.out.println("done!");
 27      }
 28  
 29      /**
 30       * 初始化DAO
 31       * <p>
 32       * @Title: initDAO
 33       * </p>
 34        */
 35      private  static  void  initDAO() {
 36          ApplicationContext context = new  ClassPathXmlApplicationContext(
 37                  "config.xml");
 38          hotelDAO = (HotelDAO) context.getBean("hotelDAO");
 39      }
 40  
 41      /**
 42       * 生成指定个数的hotelList
 43       * <p>
 44       * @Title: getHotelList
 45       * </p>
 46       * 
 47       *  @param  num
 48       *  @return
 49        */
 50      private  static  List<Hotel> getHotelList(int  num) {
 51          List<Hotel> list = new  ArrayList<Hotel>();
 52          for  (int  i = 0; i < num; i++) {
 53              Hotel hotel = new  Hotel();
 54              hotel.setName("编号为[" + i + "]的旅店");
 55              hotel.setStars(i % 10);
 56              Address address = new  Address();
 57              address.setCountry("中国");
 58              address.setCity("北京");
 59              address.setStreet("上帝南路");
 60              address.setPostCode("10000" + (i % 10));
 61              hotel.setAddress(address);
 62              list.add(hotel);
 63          }
 64          return  list;
 65      }
 66  
 67      /**
 68       * 将hotelList插入数据库
 69       * <p>
 70       * @Title: saveHotelList
 71       * </p>
 72       * 
 73       *  @param  hotelDAO
 74       *  @param  hotelList
 75        */
 76      private  static  void  saveTest() {
 77          List<Hotel> hotelList = getHotelList(100);
 78          for  (Hotel hotel : hotelList) {
 79              //  Key<Hotel> key=hotelDAO.save(hotel,WriteConcern.SAFE);
 80              Key<Hotel> key = hotelDAO.save(hotel);
 81              System.out.println("id为[" + key.getId() + "]的记录已被插入");
 82          }
 83      }
 84  
 85      /**
 86       * 更新操作测试
 87       * <p>
 88       * @Title: updateTest
 89       * </p>
 90       * 
 91       *  @throws  Exception
 92        */
 93      private  static  void  updateTest() throws  Exception {
 94          // 生成查询条件
 95          Query<Hotel> q = hotelDAO.createQuery().field("stars")
 96                  .greaterThanOrEq(9);
 97          // 生成更新操作
 98          UpdateOperations<Hotel> ops = hotelDAO.createUpdateOperations()
 99                  .set("address.city", "shanghai").inc("stars");
100          //  UpdateResults<Hotel> ur=hotelDAO.update(q, ops);
101          UpdateResults<Hotel> ur = hotelDAO.updateFirst(q, ops);
102          if  (ur.getHadError()) {
103              System.out.println(ur.getError());
104              throw  new  Exception("更新时发生错误");
105          }
106          if  (ur.getUpdatedExisting()) {
107              System.out.println("更新成功,更新条数为[" + ur.getUpdatedCount()
108                      + "],插入条数为[" + ur.getInsertedCount() + "]");
109          } else  {
110              System.out.println("没有记录符合更新条件");
111          }
112      }
113  
114      /**
115       * 删除操作测试
116       * <p>
117       * @Title: deleteTest
118       * </p>
119        */
120      private  static  void  deleteTest() {
121          ObjectId id = hotelDAO.findIds().get(0);
122          hotelDAO.deleteById(id);
123  
124          Query<Hotel> q = hotelDAO.createQuery().field("stars")
125                  .greaterThanOrEq(100);
126          hotelDAO.deleteByQuery(q);
127      }
128  
129      /**
130       * 查询测试
131       * <p>
132       * @Title: queryHotel
133       * </p>
134        */
135      private  static  void  queryHotel() {
136          //  显示所有记录
137          System.out.println("\nhotelDAO.find()=");
138          for  (Hotel hotel : hotelDAO.find()) {
139              System.out.println(hotel);
140          }
141  
142          //  统计star大于等于9的数目
143          System.out
144                  .println("\nhotelDAO.count(hotelDAO.createQuery().field(\"stars\").greaterThanOrEq(9))="
145                          + hotelDAO.count(hotelDAO.createQuery().field("stars")
146                                  .greaterThanOrEq(9)));
147  
148          //  显示符合条件的记录ID
149          List<ObjectId> ids = hotelDAO.findIds("stars", 8);
150          System.out.println("\nhotelDAO.findIds(\"stars\", 8)=");
151          for  (ObjectId id : ids) {
152              System.out.println(id);
153          }
154      }
155  
156  }

大功告成~
分享到:
评论

相关推荐

    spring MVC morphia mongo 整合的例子

    6. **Spring整合**:Spring MVC和Morphia可以通过Spring的依赖注入(DI)进行整合,使你能在控制器中直接注入`Datastore`,简化代码并提高可测试性。 7. **RESTful API设计**:示例可能展示了如何使用Spring MVC...

    spring-mongodb-morphia:springdata-mongo morphia mongodb 学习

    Spring Data的Repository抽象层可以与Morphia的实体映射相结合,提供强大的功能。例如,通过实现自定义的Repository接口,我们可以利用Morphia的查询能力,同时享受Spring Data的自动化和灵活性。 在`spring-...

    Spring整合MongoDB基于Maven

    通过以上步骤,我们已经完成了Spring与MongoDB的集成,以及基于Maven的依赖管理和JUnit测试的配置。这只是一个基础的示例,实际应用中可能需要处理更多复杂场景,如分页、聚合查询、自定义查询等。在开发过程中,...

    morphia-example-1:Java中使用Morphia ODM和Spring的示例项目

    5. **Spring与Morphia的集成**:在项目中,Morphia可能被配置为Spring的数据访问层,利用Spring的IoC容器管理和初始化Morphia的数据访问对象(DAO),同时结合Spring的数据访问支持,如数据源配置、事务管理等。...

    使用Morphia框架操作mongodb

    在Java开发中,Morphia是一个优秀的对象数据映射(ODM)框架,它简化了与MongoDB之间的交互,使得开发者可以像操作传统关系型数据库那样操作文档数据库。本文将深入探讨如何使用Morphia框架来操作MongoDB。 首先,...

    morphia-0.99.jar

    morphia-0.99.jar 最新版本。

    Mongo的ORM框架的学习Morphia

    在本文中,我们将探讨MongoDB的ORM框架Morphia,并结合Spring Data MongoDB的使用来创建一个集成应用。ORM(对象关系映射)框架是将数据库操作转化为对象模型的方法,简化了开发过程,使得开发者可以专注于业务逻辑...

    morphia所用到的jar包

    MongoDB是一个流行的开源、分布式文档数据库,而Morphia是一个Java对象数据映射(Object-Document Mapping, ODM)框架,用于简化与MongoDB的交互。在这个压缩文件中,我们有两个重要的jar包:mongo-2.7.3.jar和...

    morphia.jar和mongo.jar

    标题中的“morphia.jar”和“mongo.jar”指的是两个与MongoDB数据库操作相关的Java库文件。MongoDB是一个流行的开源、分布式文档型数据库,而Morphia则是Java开发人员用来操作MongoDB的一个对象数据映射(ODM)框架...

    使用 Morphia 和 MongoDB 实现域模型持久性(ZZ)

    Morphia 是一个 Java 框架,它为 MongoDB 提供了对象数据映射(Object Data Mapping,简称 ODM)功能,使得开发者可以更方便地在 Java 对象与 MongoDB 文档之间进行转换。本篇文章将详细介绍如何使用 Morphia 和 ...

    morphia基于mongodb的基本开发

    morphia基于mongodb的基本开发

    Morphia和MongoDB学习总结<三>

    在本文中,我们将深入探讨Morphia,一个用于Java的MongoDB对象数据映射(ODM)库,以及它如何与MongoDB数据库进行交互。Morphia简化了在MongoDB中存储和检索Java对象的过程,使得开发工作更为高效。 **一、Morphia...

    morphia-1.3.2.jar

    morphia-1.3.2.jar

    Morphia操作MongoDB

    - Morphia 提供了将对象转换为 JSON 和从 JSON 反序列化的功能,可以方便地与 JSON 文档交互。 - 使用 `JSON.serialize()` 方法将 `DBObject` 或 Java 对象序列化为 JSON 字符串。 7. **异常处理** - 在实际操作...

    Mongo集成Spring

    6. **Morphia**: `Mongo_Morthia_Spring` 可能指的是使用 Morphia 框架集成 MongoDB 和 Spring。Morphia 是一个针对 MongoDB 的对象数据映射库,它简化了将 Java 对象与 MongoDB 文档之间的转换。虽然 Spring Data ...

    Morphia开发简介.pdf

    6. **与其他框架的兼容性**:可与Guice、Spring等依赖注入框架无缝集成。 7. **扩展性**:提供多种扩展点,如新注解、转换器、行为映射和日志处理。 8. **不存储空值**:默认情况下,不存储Null或Empty值。 9. **GWT...

    Morphia 操作 MongoDB.docx

    【MongoDB与Morphia简介】 MongoDB是一款流行的NoSQL数据库系统,以其灵活的数据模型、高可用性和可扩展性而受到广泛欢迎。Morphia是针对MongoDB的一个Java持久层框架,它提供了简单易用的API,使得开发者能够方便地...

    morphia-1.00-SNAPSHOT.jar

    morphia mongo db OR-mapping mongo db再带的CRUD 太麻烦了, 一个不错的框架 类似 Hibernate

    Morphia 操作 MongoDB.pdf

    Morphia 是一个 Java ORM(对象关系映射)库,用于简化 MongoDB 的数据操作,将数据模型与数据库文档之间的转换自动化。在本文中,我们将深入探讨如何使用 Morphia 进行 MongoDB 的操作。 首先,为了在 Java 项目中...

    mongodb+springmvc+morphia

    后续提交放在https://github.com/zdsiyan/watermelon 上, 用eclipse导入该工程需安装m2eclipse,jetty等查件. 另外.settings下的org.eclipse.wst.common.component文件如下: ...&lt;/project-modules&gt;

Global site tag (gtag.js) - Google Analytics