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

Rails每周一题(十八): Association之魔法

阅读更多

     要写出优美的rails代码,必须理解和掌握association的机制。它能使代码编写更加简单和方便,更能使你的代码更加简洁和优美。除此之外,也必须了解association背后的实现。有时候,代码的优美不代表一切,甚至代表着背后的丑恶。

 

 

    Association魔法

 

    先来看看什么是association,以及association如何使你的代码更加简单和优美。

 

    举个rubyonrails guides上的例子。

 

    一个customer有很多orders,它们的模型是这样子的:

 

 

class Customer < ActiveRecord::Base 
end 

class Order < ActiveRecord::Base 
end 
 

    假如要创建一个属于一个customer的order,则需要:

 

 

@order = Order.create(:order_date => Time.now,  :customer_id => @customer.id) 
 

    或者删除一个customer,以及他的所有orders:

 

 

@orders = Order.find_by_customer_id(@customer.id) 
@orders.each do |order| 
  order.destroy 
end 

@customer.destroy 

 

    这样的代码是非常繁琐的,并且在语义上很不清晰。让我们为这两个模型声明association:

 

 

class Customer < ActiveRecord::Base 
  has_many :orders, :dependent => :destroy 
end 

class Order < ActiveRecord::Base 
  belongs_to :customer 
end 

 

    当我们为这两个模型声明了association之后,一切就变得简单和明了了:

 

 

@order = @customer.orders.create(:order_date => Time.now) 

@customer.destroy 

 

    这就是assciation的魔法。

 

 

    选择正确的association

 

    使用何种association,跟数据库schema的设计有关;但最重要的,是反之,在设计数据库schema的时候,要考虑到数据的真正含义。

 

    比如,一个company,它拥有一个account,这是明显的account从属于company的关系。

 

    那么,它们的模型和association声明最好应该是这样的,以便跟语义匹配:

 

 

class Company
  has_one :account
end

class Account
  belongs_to :company
end
 

    但association的声明,还得符合数据库的表结构(特别是建立在一个遗留数据库上的时候)。这时候的外键(foreign key)应该在accounts表上,而不是companies表上。

 

    又比如,一个teacher有很多students,同时一个students也有很多teachers。所以,它们应该是多对多的关系,那么,association的模型声明应该是这样的:

 

 

class Teacher
  has_and_belongs_to_many :students
end

class Student
  has_and_belongs_to_many :teachers
end
 

   这种多对多的映射,在数据库上需要有一个中间表:students_teachers。但有时候,我们希望这个中间表变得有意义起来,而不是纯粹起到关联作用,比如在它上面添加一些其它字段。并且希望这个中间表的名字更加有意义,以便映射到一个模型。这时候,需要使用另外一种association,来建立这三个模型之间的关联:

 

 

class Teacher
  has_many :relations
  has_many :students, :through => :relations
end

class Relation
  belongs_to :teacher
  belongs_to :student
end

class Student
  has_many :relations
  has_many :teachers, :through => :relations
end

 

   还有一种高阶用法,有时候,我们希望一个模型属于几种不同的其它模型。比如,image即属于一个lightbox,也属于一个shopping cart。那我们可以使用多态association(polymorphic association ):

 

 

class Image
  belongs_to :image_collection, :polymorphic => true
end

class Lightbox
  has_many :images, :as => :image_collection
end

class ShoppingCart
  has_many :images, :as => :image_collection
end
 

    同时,在做migration的时候,也要注意声明这是一种多态的关联:

 

 

create_table :images do |t| 
  t.string :name  
  t.references :image_collection, :polymorphic => true
  t.timestamps 
end 

 

    这个多态的reference,其实会生成两个字段:image_collection_id和:image_collection_type。

 

   随之而来的那些方法

 

    这是association带来的非常重要的东西,就是随着association的声明,会有一些相关的方法被创建出来。

 

     比如我们声明了belongs_to关联之后,有四个方法被自动创建:

 

 

association(force_reload = false)
association=(associate)
build_association(attributes = {})
create_association(attributes = {})

 

     又比如对于has_many关联,有如下方法被创建:

 

 

collection(force_reload = false)
collection<<(object, …)
collection.delete(object, …)
collection=objects
collection_singular_ids
collection_singular_ids=ids
collection.clear
collection.empty?
collection.size
collection.find(…)
collection.exist?(…)
collection.build(attributes = {}, …)
collection.create(attributes = {})
 

    正是这些方法,让代码变得简单。

 

    举个简单的例子,还是上面那个customer、order模型:

 

 

@customer.order_ids = [1, 2, 3]
 

    这个方法会把customer原有的并且不在这个id list里的orders给清除掉,创建新的原来不存在的关联,同时,会自动save到数据库。曾经需要非常复杂的操作,现在变得如此简单。

 

    还有,rails并不限制你为association添加自己的方法,这就是Association Extensions

 

   何时保存到数据库

 

    经过观察,发现对于大部分的关联,rails都会在赋值时自动把关联对象以及新的关联关系保存到数据库(除非你特别使用build方法来告诉rails不要save)。但对于belongs_to关联,却是个例外。比如对于上面account和company的例子:

 

 

@account.company = @company

 

    上面这条语句,并不会自动把@company(如果是一个new record)以及这两个对象之间的关联关系保存到数据库。

 

    我觉得,这个例外的主要原因是:对于其它关联来讲,关联的key要么在一个中间表,要么在对方表上。比如:

 

 

@company.account = @account
 

    外键存在于accounts表上,而不是companies表上。

 

   而对于belongs_to的关联,外键存在于自己表上。你给一个对象设置一些property,在没有调用save之前,它是不应该保存到数据库的。在如下这个场景里,就比较好理解为什么belongs_to关联不会自动保存到数据库:

 

 

@account.name = "new account"
@account.company = @company
@account.value = 100.00

# @account.save

 

    在调用上面的save语句之前,@account肯定不应该保存任何数据到数据库。

 

    而对于其它类型的关联,自动保存是比较合理的。比如:

 

 

@company.account = @account

 

    外键在accounts表上,这时候会自动把外键设上,并保存到数据库。而如果需要显示save才能保存,那么代码就会变得难看并且不合理:

 

 

@company.account= @account
# @account.save   #需要显示保存account,而我们是在操作company

 

    当然,上面讨论的前提是:自身对象本身已经被save了,而不是一个new record。

 

   Association的弹性

 

    Rails很精妙的一点在于,它用convention来使你省却很多麻烦,但它从来不限制你做什么,你如果觉得它的方式不好或者不适用,你可以去改变它。

 

    对于association也一样,它提供了很多options。

 

    比如对于company和account,外键默认是company_id,但你可以通过设定:foreign_key选项,来指定你希望的外键名称。

 

    又比如,对于任何关联,rails都提供了默认的sql查询语句。但我们可以通过:finder_sql来改变它的查询语句。

 

    Association,小心!

 

    Association的使用和创建并不是随心所欲的,你还得小心以下几点:

 

    1. 不要随心所欲地使用名字,至少,不应该跟model本身的instance method冲突。

 

    2. 小心cache:所有的associaiton方法,都是在最近查询的cache上操作,如果你的程序的其它部分改变了数据,就需要reload这些数据。比如

 

 

customer.orders # retrieves orders from the database 
customer.orders.size # uses the cached copy of orders 
customer.orders(true).empty? # discards the cached copy of orders  
                                                # and goes back to the database 
 

 

    Association的罪恶

 

    Association是魔法,但如果滥用,它是罪恶。

 

    举一个例子。

 

    有两个模型:

 

 

class User
  has_many :lightboxes
end

class Lightbox
  belongs_to :user
end
 

    我们希望找出拥有lightbox,并且lightbox的images_count大于10的所有users。可能我们会使用下面这个查询语句:

 

 

User.all(:include => [:lightboxes], 
             :conditions => "lightboxes.id IS NOT NULL AND lightboxes.images_count > 10")

 

 

   这个查询想当然地include(eager loading)了lightboxes,导致了两个罪恶:

 

   1. 滥用include。:include在这里确实需要,如果没有:include了lightboxes,lightboxes表因为没有被bound,所以不可以在:conditions里面使用它的字段作为查询条件。但是,:include应该在需要eager loading的时候使用,而不是把它用作:joins的替代。

 

       在跟踪log的时候,你会看到这个查询导致了两个表之间的left join,而这在当前的查询来说不是最好的方式。使用left join和lightbox.id IS NOT NULL的方式去过滤没有lightbox的user是一种愚蠢的行为。正确并且快速的方式是使用inner join。并且在结果集里面会把lightbox的字段查询出来,而这并不是我们需要的。

 

   2. 很多人以为eager loading是用join来实现的,其实在Rails2.2之后(我知道的是这个版本),已经改变了(http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations )。比如对于下面的查询:

 

 

User.all(:include => :lightboxes, :conditions => "company_id = 20")
 

    数据库真正的查询是:

 

SELECT * FROM users WHERE company_id = 20
SELECT * FROM lightboxes WHERE lightboxes.user_id IN (....................)

 

    这种查询方式,比之于用left oin来查询,数据结果集更加少,而且更易于做缓存(评论里QuakeWang说道)。

 

    但是,有一种情况会改变这种默认的查询方式:

 

    在:conditions里面有其它表的字段出现。不管这个表是否出现在:include集里面(比如只出现在joins里面),rails的eager loading的查询方式都会变成用join的方式,来查出发起表自身和:include里面所有关联的结果集。

 

    分几种情况:

 

    如果是通过:include绑定了其它表,那么使用的是left outer join,并且结果集中会出现这个表。

 

    如果是通过:joins绑定了其他表,那么使用的join方式由自己指定,并且如果没有:include这个关联,结果集不会出现这个表的数据。

 

    如果同时被:include和:joins绑定,代表的是:需要eager loading这个关联,但使用由:joins指定的表关联方式。

 

    举两个例子吧:

 

 

Company.all(:include => :address, 
    :joins => "INNER JOIN users ON users.company_id = companies.id", 
    :conditions => "users.id in (1, 2)")

 

    引起的查询时:

 

 

SELECT companies.id AS t0_r0, ...., addresses.id as t1_r0, ... FROM companies LEFT OUTER JOIN 
addresses ON addresses.id = companies.address_id INNER JOIN users ON users.company_id = 
companies.id WHERE users.id in (1, 2)
 

    而如果稍微改变一下上面的查询语句:

 

 

Company.all(:include => :address, 
    :joins => "INNER JOIN users ON users.company_id = companies.id AND users.id IN (1, 2)")

 

    则引起的查询是:

 

 

SELECT companies.* FROM companies INNER JOIN users 
ON users.company_id = companies.id AND users.id in (1, 2)

SELECT * FROM addresses WHERE addresses.id in (...)
 

 

    通过上面的分析,我们了解了如何正确使用eager loading。

 

    1. Eager loading是需要在预先获取关联数据时使用的,它通过减少N+1查询,来提高效率。它不是:joins的替代,并且较之于:joins,:include会导致结果集的增加。

 

    2. 同时,最好在需要eager loading时,避免在:conditions里面涉及其它表的字段,而通过:joins来替代。这种方式的效率会更好,这也是Rails2.2改变查询方式的原因吧。

 

    所以,上面那个查询正确的方式应该是下面这样的,因为我们只是把lightboxes作为一个查询条件,并不需要它的结果集:

 

 

User.all(:joins=> "INNER JOIN lightboxes ON users.id = lightboxes.user_id", 
             :conditions => "lightboxes.images_count > 10")
 

   当然,有人会说,这也不是“最美”的查询方式,你可以利用模型之间的association来写出一个更漂亮的查询,但上面这个查询是建立在“我们需要性能”的基础上的。

 

 

   其它

 

    关于association,还有其它一些重要的东西,比如 Association Callbacks ,就不再一一赘述了。本文完毕。

 

 

Reference:

 

    http://guides.rubyonrails.org/association_basics.htm

分享到:
评论
17 楼 jerry 2009-08-26  
这一次比以前写的好:)
16 楼 andyhu1007 2009-08-19  
QuakeWang 写道
rainchen说的是一种方法,还有一种做法利用memcached的mutil get,这里的查询是通过ActiveRecord::Base.find_some生成的in语句,我们可以alias这个方法,先用mutil get这一系列的id,然后将已经获取的对象id从array中剔除,再用find_some_without_cache查询,这样在缓存命中率不高的情况下,也只会有一句in的查询。


Great!
15 楼 QuakeWang 2009-08-19  
rainchen说的是一种方法,还有一种做法利用memcached的mutil get,这里的查询是通过ActiveRecord::Base.find_some生成的in语句,我们可以alias这个方法,先用mutil get这一系列的id,然后将已经获取的对象id从array中剔除,再用find_some_without_cache查询,这样在缓存命中率不高的情况下,也只会有一句in的查询。
14 楼 rainchen 2009-08-18  
andyhu1007 写道
QuakeWang 写道
引用
   2. 很多人以为eager loading是用join来实现的,其实并不是,至少Rails2.2之后不再是。那么,上面那个查询导致了一个很庞大但我们并不需要的数据库查询,即:
SELECT * FROM lightboxes WHERE id IN (....................) 

2.2的这个改动我觉得是个优点,因为这种查询在大部分的情况下性能反而比join查询要好,配合缓存更是能够极大地减少数据库的负担,比如我给light_box加上按id作缓存,查询就会变成:
SELECT * FROM lightboxes WHERE id IN (ids not in cache) 
而查询完以后,还能够将未cache的light_box填充,这样其它代码在通过id获取light_box的时候也能够减少数据库查询。



请教一下,你这里所说的缓存怎么做?


我猜QuakeWang的意思是指利用memcached这样的mem hash db 来做db cache server,只做主键查询缓存,比如:select * from xx where primary_id = 123
那么一个类似:
SELECT * FROM lightboxes WHERE id IN (123,456,789, ...) 
的查询就可以分解为使用多个主键查询:
SELECT * FROM lightboxes WHERE id = 123;
SELECT * FROM lightboxes WHERE id = 456;
SELECT * FROM lightboxes WHERE id = 789;
...

这样所有查询都还是徘徊在db cache server层,不会过问db server,减轻db server的负担。

13 楼 andyhu1007 2009-08-17  
QuakeWang 写道
引用
   2. 很多人以为eager loading是用join来实现的,其实并不是,至少Rails2.2之后不再是。那么,上面那个查询导致了一个很庞大但我们并不需要的数据库查询,即:
SELECT * FROM lightboxes WHERE id IN (....................) 

2.2的这个改动我觉得是个优点,因为这种查询在大部分的情况下性能反而比join查询要好,配合缓存更是能够极大地减少数据库的负担,比如我给light_box加上按id作缓存,查询就会变成:
SELECT * FROM lightboxes WHERE id IN (ids not in cache) 
而查询完以后,还能够将未cache的light_box填充,这样其它代码在通过id获取light_box的时候也能够减少数据库查询。



请教一下,你这里所说的缓存怎么做?
12 楼 QuakeWang 2009-08-17  
引用
   2. 很多人以为eager loading是用join来实现的,其实并不是,至少Rails2.2之后不再是。那么,上面那个查询导致了一个很庞大但我们并不需要的数据库查询,即:
SELECT * FROM lightboxes WHERE id IN (....................) 

2.2的这个改动我觉得是个优点,因为这种查询在大部分的情况下性能反而比join查询要好,配合缓存更是能够极大地减少数据库的负担,比如我给light_box加上按id作缓存,查询就会变成:
SELECT * FROM lightboxes WHERE id IN (ids not in cache) 
而查询完以后,还能够将未cache的light_box填充,这样其它代码在通过id获取light_box的时候也能够减少数据库查询。
11 楼 fireflyman 2009-08-17  
這個更詳細
10 楼 fireflyman 2009-08-17  
http://guide.rails.info/association_basics.html
9 楼 下一站,火星 2009-08-17  
andyhu1007 写道
下一站,火星 写道
User.all, :conditions => "lightboxes_count > 10"

加了之后,你不就可以在users表里面直接根据lightboxes_count查询了?


不好意思,让你误解了。

我那里是images_count,它是lightboxes上的一个字段。


是我看错了,你的方法不错
引用
  User.all(:joins=> "INNER JOIN lightboxes ON users.id = lightboxes.user_id",               :conditions => "lightboxes.images_count > 10") 

8 楼 下一站,火星 2009-08-17  
kukuwuwu 写道
counter_cache 只是个count数缓存到一个字段上而已, 如果查询中有count查询数据库则少一步count查询,如果逻辑中没有count查询 counter_cache 对查询并不起任何作用


what is your point ?

7 楼 kukuwuwu 2009-08-17  
counter_cache 只是个count数缓存到一个字段上而已, 如果查询中有count查询数据库则少一步count查询,如果逻辑中没有count查询 counter_cache 对查询并不起任何作用
6 楼 wxmfly 2009-08-16  
写的很用心
5 楼 andyhu1007 2009-08-16  
下一站,火星 写道
User.all, :conditions => "lightboxes_count > 10"

加了之后,你不就可以在users表里面直接根据lightboxes_count查询了?


不好意思,让你误解了。

我那里是images_count,它是lightboxes上的一个字段。
4 楼 下一站,火星 2009-08-16  
User.all, :conditions => "lightboxes_count > 10"

加了之后,你不就可以在users表里面直接根据lightboxes_count查询了?
3 楼 andyhu1007 2009-08-16  
下一站,火星 写道
引用

class User 
  has_many :lightboxes 
end   
class Lightbox 
  belongs_to :user 
end

引用
我们希望找出拥有lightbox,并且lightbox的images_count大于10的所有users。

为什么不用
引用
class Lightbox 
  belongs_to :user , :counter_cache => true
end





这个例子本身是为了举例之用,所以肯定有不合理的地方。

不过,我不清楚加上:counter_cache对这个查询的作用是什么,请指教~~

2 楼 下一站,火星 2009-08-16  
引用
class User 
  has_many :lightboxes 
end   
class Lightbox 
  belongs_to :user 
end

引用
我们希望找出拥有lightbox,并且lightbox的images_count大于10的所有users。

为什么不用
引用
class Lightbox 
  belongs_to :user , :counter_cache => true
end



1 楼 机器人 2009-08-16  
呀,真不是错啊,谢谢分享。

你这么晚还在写帖子啊啊

相关推荐

    Rails的精简版本Rails::API.zip

    Rails::API 是 Rails 的精简版本,针对不需要使用完整 Rails 功能的开发者。 Rails::API 移除了 ActionView 和其他一些渲染功能,不关心Web前端的开发者可更容易、快速地开发应用程序,因此运行速度比正常的 Rails ...

    Rails上的API:使用Rails构建REST APIAPIs on Rails: Building REST APIs with Rails

    Rails以其简洁优雅的语法、高效的开发速度以及良好的社区支持而闻名,这使得它成为构建现代API的理想选择之一。 ### 一、什么是RESTful API REST(Representational State Transfer)是一种软件架构风格,用于定义...

    Ruby on Rails入门例子

    Ruby on Rails,简称Rails,是一种基于Ruby语言的开源Web应用程序框架,它遵循MVC(Model-View-Controller)架构模式,旨在使Web开发过程更加高效、简洁。本篇将通过一个入门实例,深入探讨Rails的基本概念和核心...

    rails-basic-template:基本 Rails 模板

    Rails 基本模板参考: : Ruby on Rails Gemfile:定义应用程序正在使用的库的文件bundle install:基于Gemfile,安装所有库每次修改 Gemfile 时都应该运行bundle install gem 是 Ruby 的库RubyGems.org 是一个查找和...

    rails-ansible-presentation:有关Rails + Ansible的Deckset演示

    [适合] Rails :red_heart: Ansible [适合] Rails :red_heart: Ansible (有一点帮助) Rails部署 简单吧? 将应用程序放在服务器上。 捆绑宝石。 应用迁移。 重新启动服务。 Easy Rails部署 git push master ...

    rails_console_toolkit:可配置的 Rails 控制台助手

    RailsConsole 工具包 :wrench: :toolbox: 可配置的 Rails 控制台助手更快地查找记录,添加自定义助手,将您的控制台寿命提高 100%。安装将此行添加到应用程序的 Gemfile 中: gem 'rails_console_toolkit' 然后生成...

    Ruby on Rails入门经典代码

    Ruby on Rails,简称Rails,是基于Ruby语言的一个开源Web应用程序框架,它遵循MVC(Model-View-Controller)架构模式,旨在使Web开发过程更加高效、简洁。本压缩包中的"Ruby on Rails入门经典代码"提供了新手学习...

    Ruby On Rails 面试系列七,一个面试练习题

    这个面试练习题可能是为了测试应聘者对于Rails应用的构建、自动化任务管理、测试驱动开发(TDD)以及文件组织结构的理解。让我们逐一探讨这些知识点。 首先,`Rakefile`是Ruby中的构建工具,类似于其他语言的...

    webpack-rails, 将 web pack与你的Ruby on Rails 应用程序集成.zip

    webpack-rails, 将 web pack与你的Ruby on Rails 应用程序集成 不再维护webpack-rails 不再被维护。 有关详细信息,请参阅 #90. web pack-railsweb pack 为你提供了将 web pack集成到现有的Ruby on Rails 应用程序中...

    rails_email_preview:在Rails中预览和编辑应用程序邮件模板

    安装加 到Gemfile: gem 'rails_email_preview' , '~&gt; 2.2.2' 添加一个初始化程序和路由: $ rails g rails_email_preview:install 在app / mailer_previews /中生成预览类和方法存根$ rails g rails_email_preview:...

    rails-developer-scanning:针对Rails开发人员的自动面试问题

    标题 "rails-developer-scanning" 指向的是一个针对Rails开发者设计的自动化面试工具,旨在帮助面试官或招聘者快速有效地评估应聘者的Rails技能。这个工具名为 "jyaasa_interviewer",通过RubyGem安装,使得面试过程...

    rails-dom-testing:从ActionView中提取DomAssertions和SelectorAssertions

    Rails :: Dom :: Testing 这个gem负责比较HTML DOM并断言Rails应用程序中存在DOM元素。 assert_dom_equal通过assert_dom_equal和assert_dom_not_equal进行比较。 元素通过assert_dom , assert_dom_encoded , ...

    Rails中应用Ext.tree:以中国的省市地区三级联动选择为例

    这篇博客文章“Rails中应用Ext.tree:以中国的省市地区三级联动选择为例”提供了一个实用的示例,教我们如何利用Ext.js库中的Tree组件来实现这种功能。 首先,让我们了解Rails和Ext.js的基本概念。Rails是基于Ruby...

    rails-controller-testing:将`assigns`和`assert_template`带回到您的Rails测试中

    Rails :: Controller :: Testing 这个gem将assigns给控制器测试的内容以及assert_template带回assigns控制器和集成测试的内容。 这些方法已中。 安装 将此行添加到您的应用程序的Gemfile中: gem 'rails-...

    rails-docker-compose:Ruby on Rails的Docker开发环境

    使用Docker开发Ruby on Rails的配置文件 docker-compose up 利润! 你得到什么 一线开发环境的设置和启动: docker-compose up 。 一个易于安装的依赖关系可在新计算机上进行编码:Docker。 (与使用Vagrant时的两...

    rails有用的命令

    在Ruby on Rails框架中,命令行工具是开发者日常工作中不可或缺的一部分。Rails命令允许我们快速地构建应用程序、管理数据库、生成代码以及执行各种自动化任务。以下是一些关键的Rails命令及其详细解释: 1. **创建...

    rails_stack-cookbook:使用 nginx、unicorn、redis 等设置 Rails 环境的 Chef 食谱

    rails_stack 食谱 TODO:在此处输入食谱说明。 例如,这本食谱使您最喜欢的早餐三明治。 要求 TODO:列出您的食谱要求。 确保包含本说明书对平台、库、其他说明书、软件包、操作系统等的任何要求。 例如 包裹 ...

    rails-html-sanitizer

    如果您在非Rails应用程序中需要类似的功能,请考虑直接使用(这是处理内幕消毒的原因)。 安装 将此行添加到您的应用程序的Gemfile中: gem 'rails-html-sanitizer' 然后执行: $ bundle 或将其自己安装为: $...

    rails-cache-extended:帮助程序和日志记录添加到 Rails 缓存

    Rails::Cache::Extended 这允许为记录集合生成自动过期的缓存键 安装 将此行添加到应用程序的 Gemfile 中: gem 'rails-cache-extended' 然后执行: $ bundle 或者自己安装: $ gem install rails-cache-...

    Rails相关电子书汇总

    Ruby on Rails,通常简称为Rails,是一个基于Ruby语言的开源Web应用程序框架,它遵循MVC(模型-视图-控制器)架构模式,以简洁、高效的代码和“约定优于配置”的理念著称。此压缩包中的"rubyonrails21-cn.pdf"可能是...

Global site tag (gtag.js) - Google Analytics