-
Notifications
You must be signed in to change notification settings - Fork 3
/
chapter14.html
1983 lines (1720 loc) · 188 KB
/
chapter14.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<title>Ruby on Rails 教程 - 第 14 章 关注用户</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="最好的 Ruby on Rails 入门教程"/>
<meta name="keywords" content="ruby, rails, tutorial"/>
<meta name="author" content="Michael Hartl"/>
<meta name="translator" content="安道"/>
<meta name="generator" content="persie 0.0.5.1"/>
<link rel="stylesheet" type="text/css" href="//railstutorial-china.org/assets/css/main.css"/>
<link rel="stylesheet" type="text/css" href="book.css"/>
<script type="text/javascript" src="//railstutorial-china.org/assets/js/global.js"></script>
</head>
<body class="book-page">
<nav class="navbar">
<div class="container">
<div class="clearfix">
<a class="navbar-brand hidden-sm-up" href="//railstutorial-china.org/" title="Ruby on Rails 教程">Ruby on Rails 教程</a>
<button class="navbar-toggler hidden-sm-up pull-xs-right" type="button" data-toggle="collapse" data-target="#main-nav">☰</button>
</div>
<a class="navbar-brand hidden-xs-down" href="//railstutorial-china.org/" title="Ruby on Rails 教程">Ruby on Rails 教程</a>
<div class="collapse navbar-toggleable-xs pull-sm-right" id="main-nav">
<ul class="nav navbar-nav">
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/" title="首页">首页</a></li>
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/blog/" title="博客">博客</a></li>
<li class="nav-item active"><a class="nav-link" href="//railstutorial-china.org/book/" title="阅读">阅读</a></li>
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/#ebook" title="电子书">电子书</a></li>
</ul>
</div>
</div>
</nav>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<div class="book-versions">
选择版本:
<a class="btn btn-primary" href="//railstutorial-china.org/book/" title="Ruby on Rails 教程(原书第 4 版,针对 Rails 5)">Rails 5</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails42/" title="Ruby on Rails 教程(原书第 3 版,针对 Rails 4.2)">Rails 4.2</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails4/" title="Ruby on Rails 教程(原书第 3 版,针对 Rails 4.0)">Rails 4.0</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails3/" title="Ruby on Rails 教程(原书第 2 版,针对 Rails 3.2)">Rails 3.2</a>
</div>
<div class="alert alert-warning">
<p>在线版的内容可能落后于电子书,如果想及时获得更新,请<a href="//railstutorial-china.org/#ebook" title="购买电子书">购买电子书</a>。</p>
</div>
<article class="article">
<section data-type="chapter" id="following-users">
<h1><span class="title-label">第 14 章</span> 关注用户</h1>
<p>这一章,我们要为演示应用添加社交功能,允许用户关注(及取消关注)其他人,并在主页显示被关注用户发布的微博(动态流)。我们将在 <a class="xref-link" href="#the-relationship-model">14.1 节</a>学习如何建立用户之间的关系,然后在 <a class="xref-link" href="#a-web-interface-for-following-users">14.2 节</a>编写相应的 Web 界面(还会介绍 Ajax)。最后,在 <a class="xref-link" href="#the-status-feed">14.3 节</a>实现功能完善的动态流。</p>
<p>这是本书最后一章,有些内容具有挑战性。比如说,为了实现动态流,我们会使用一些 Ruby 和 SQL 技巧。通过这些示例,你将了解到 Rails 是如何处理更加复杂的数据模型的,这些知识也会在你日后开发其他应用时发挥作用。 为了帮助你平稳地从学习过渡到独立开发,<a class="xref-link" href="#following-users-conclusion">14.4 节</a>会列出一些进阶学习资源。</p>
<p>因为本章的内容比较有挑战性,所以在开始编写代码之前,我们先来讨论一下界面。和之前的章节一样,在开发之前,我们将使用构思图。<sup>[<a id="fn-ref-1" href="#fn-1">1</a>]</sup>完整的页面流程是这样的:一个用户 (John Calvin) 从他的资料页面(<a class="xref-link" href="#fig-page-flow-profile-mockup">图 14.1</a>)浏览到用户列表页面(<a class="xref-link" href="#fig-page-flow-user-index-mockup">图 14.2</a>),寻找想关注的用户;然后他打开另一个用户 Thomas Hobbes 的资料页面(<a class="xref-link" href="#fig-page-flow-other-profile-follow-button">图 14.3</a>),点击“Follow”(关注)按钮关注了他,这时“Follow”按钮会变为“Unfollow”(取消关注),而且关注 Hobbes 的人数增加了一个(<a class="xref-link" href="#fig-page-flow-other-profile-unfollow-button-mockup">图 14.4</a>);接着,Calvin 回到主页,看到他关注的人数也增加了一个,而且在动态流中能看到 Hobbes 发布的微博(<a class="xref-link" href="#fig-page-flow-home-page-feed-mockup">图 14.5</a>)。本章接下来的内容就是要实现这样的页面流程。</p>
<div id="fig-page-flow-profile-mockup" class="figure"><img src="images/chapter14/page_flow_profile_mockup_3rd_edition.png" alt="page flow profile mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 14.1</span>:当前用户的资料页面</div></div>
<div id="fig-page-flow-user-index-mockup" class="figure"><img src="images/chapter14/page_flow_user_index_mockup_bootstrap.png" alt="page flow user index mockup bootstrap" /><div class="figcaption"><span class="title-label">图 14.2</span>:找一个想关注的用户</div></div>
<div id="fig-page-flow-other-profile-follow-button" class="figure"><img src="images/chapter14/page_flow_other_profile_follow_button_mockup_3rd_edition.png" alt="page flow other profile follow button mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 14.3</span>:想关注的那个用户的资料页面,有一个“Follow”(关注)按钮</div></div>
<div id="fig-page-flow-other-profile-unfollow-button-mockup" class="figure"><img src="images/chapter14/page_flow_other_profile_unfollow_button_mockup_3rd_edition.png" alt="page flow other profile unfollow button mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 14.4</span>:资料页面中显示了“Unfollow”(取消关注)按钮,而且关注他的人数增加了一个</div></div>
<div id="fig-page-flow-home-page-feed-mockup" class="figure"><img src="images/chapter14/page_flow_home_page_feed_mockup_3rd_edition.png" alt="page flow home page feed mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 14.5</span>:首页,显示了动态流,而且关注的人数增加了一个</div></div>
<section data-type="sect1" id="the-relationship-model">
<h1><span class="title-label">14.1</span> <code>Relationship</code> 模型</h1>
<p>为了实现用户关注功能,首先要创建一个看上去并不是那么直观的数据模型。一开始我们可能以为 <code>has_many</code> 关联能满足我们的要求:一个用户关注多个用户,而且也被多个用户关注。但实际上这种实现方式有问题,下面我们将学习如何使用 <code>has_many :through</code> 解决。</p>
<p>和之前一样,如果使用 Git,现在应该新建一个主题分支:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>git checkout <span class="nt">-b</span> following-users
</code></pre></div>
</div>
<section data-type="sect2" id="a-problem-with-the-data-model-and-a-solution">
<h2><span class="title-label">14.1.1</span> 数据模型带来的问题(以及解决方法)</h2>
<p>在构建关注用户所需的数据模型之前,我们先来分析一个典型的案例。假如一个用户关注了另外一个用户,比如 Calvin 关注了 Hobbes,也就是 Hobbes 被 Calvin 关注了,那么 Calvin 就是“关注人”(follower),Hobbes 则是“被关注人”(followed)。按照 Rails 默认的复数命名约定, 我们称关注了某个用户的所有用户为这个用户的“followers”,因此,<code>hobbes.followers</code> 是一个数组,包含所有关注了 Hobbes 的用户。不过,如果反过来,这种表述就说不通了:默认情况下,所有被关注的用户应该叫“followeds”,但是这样说并不符合英语语法。所以,参照 Twitter 的叫法,我们把被关注的用户叫做“following”(例如,“50 following, 75 followers”)。因此,Calvin 关注的人可以通过 <code>calvin.following</code> 数组获取。</p>
<p>经过上述讨论,我们可以按照<a class="xref-link" href="#fig-naive-user-has-many-following">图 14.6</a> 中的方式构建被关注用户的模型——一个 <code>following</code> 表和 <code>has_many</code> 关联。由于 <code>user.following</code> 应该是一个用户对象组成的数组,所以 <code>following</code> 表中的每一行都应该是一个用户,通过 <code>followed_id</code> 列标识,然后再通过 <code>follower_id</code> 列建立关联。<sup>[<a id="fn-ref-2" href="#fn-2">2</a>]</sup>除此之外,由于每一行都是一个用户,所以还要在表中加入用户的其他属性,例如名字、电子邮件地址和密码等。</p>
<div id="fig-naive-user-has-many-following" class="figure"><img src="images/chapter14/naive_user_has_many_following.png" alt="naive user has many following" /><div class="figcaption"><span class="title-label">图 14.6</span>:一个用户关注的人(天真方式)</div></div>
<p><a class="xref-link" href="#fig-naive-user-has-many-following">图 14.6</a> 中的数据模型有个问题:存在非常多的冗余,每一行不仅包括被关注用户的 ID,还包括他们的其他信息,而这些信息在 <code>users</code> 表中都有。 更糟的是,为了保存关注我的人,还需要另一个同样冗余的 <code>followers</code> 表。这么做会导致数据模型极难维护:用户修改名字时,不仅要修改 <code>users</code> 表中的数据,还要修改 <code>following</code> 和 <code>followers</code> 表中包含这个用户的每一条记录。</p>
<p>造成这个问题的原因是缺少底层抽象。找到合适的抽象有一种方法:思考在 Web 应用中如何实现关注用户的操作。<a class="xref-link" href="chapter7.html#a-users-resource">7.1.2 节</a>介绍过,REST 架构涉及到资源的创建和销毁两个操作。由此引出了两个问题:用户关注另一个用户时,创建的是什么?用户取消关注另一个用户时,销毁的是什么?按照这样的方式思考,我们会发现,在关注用户的过程中,创建和销毁的是两个用户之间的“关系”。因此,一个用户有多个“关系”,从而通过这个“关系”得到很多我关注的人(<code>following</code>)和关注我的人(<code>followers</code>)。</p>
<p>在实现应用的数据模型时还有一个细节要注意:Facebook 实现的关系是对称的(至少在数据模型层是),而我们要实现的关系和 Twitter 类似,是不对称的,Calvin 可以关注 Hobbes,但 Hobbes 并不需要关注 Calvin。为了区分这两种情况,我们要使用专业的术语:如果 Calvin 关注了 Hobbes,但 Hobbes 没有关注 Calvin,那么 Calvin 和 Hobbes 之间建立的是主动关系(active relationship),而 Hobbes 和 Calvin 之间是被动关系(passive relationship)。<sup>[<a id="fn-ref-3" href="#fn-3">3</a>]</sup></p>
<p>现在我们集中精力实现主动关系,即获取我关注的用户。<a class="xref-link" href="#followers">14.1.5 节</a>再实现被动关系。从<a class="xref-link" href="#fig-naive-user-has-many-following">图 14.6</a> 中可以看出实现的方式:既然我关注的每一个用户都由 <code>followed_id</code> 独一无二地标识出来了,我们就可以把 <code>following</code> 表转化成 <code>active_relationships</code> 表,删掉用户的属性,然后使用 <code>followed_id</code> 从 <code>users</code> 表中检索我关注的用户的信息。这个数据模型如<a class="xref-link" href="#fig-user-has-many-following">图 14.7</a> 所示。</p>
<div id="fig-user-has-many-following" class="figure"><img src="images/chapter14/user_has_many_following_3rd_edition.png" alt="user has many following 3rd edition" /><div class="figcaption"><span class="title-label">图 14.7</span>:通过主动关系获取我关注的用户</div></div>
<p>因为主动关系和被动关系最终会存储在同一个表中,所以我们把这个表命名为“relationships”。这个表对应的模型是 <code>Relationship</code>,如<a class="xref-link" href="#fig-relationship-model">图 14.8</a> 所示。从 <a class="xref-link" href="#followed-users">14.1.4 节</a>开始,我们将介绍如何使用这个模型同时实现主动关系和被动关系。</p>
<div id="fig-relationship-model" class="figure"><img src="images/chapter14/relationship_model.png" alt="relationship model" /><div class="figcaption"><span class="title-label">图 14.8</span>:<code>Relationship</code> 数据模型</div></div>
<p>为此,我们要生成一个迁移,对应于<a class="xref-link" href="#fig-relationship-model">图 14.8</a> 中的模型:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails generate model Relationship follower_id:integer followed_id:integer
</code></pre></div>
</div>
<p>因为我们将通过 <code>follower_id</code> 和 <code>followed_id</code> 查找关系,所以还要为这两个列建立索引,提高查询的效率,如<a class="xref-link" href="#listing-relationships-migration">代码清单 14.1</a> 所示。</p>
<div id="listing-relationships-migration" data-type="listing">
<h5><span class="title-label">代码清单 14.1</span>:在 <code>relationships</code> 表中添加索引</h5>
<div class="source-file">db/migrate/[timestamp]_create_relationships.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">CreateRelationships</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">5.0</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:relationships</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:follower_id</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:followed_id</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="k">end</span>
<span class="hll"> <span class="n">add_index</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="ss">:follower_id</span></span>
<span class="hll"> <span class="n">add_index</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="ss">:followed_id</span></span>
<span class="hll"> <span class="n">add_index</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="p">[</span><span class="ss">:follower_id</span><span class="p">,</span> <span class="ss">:followed_id</span><span class="p">],</span> <span class="ss">unique: </span><span class="kp">true</span></span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>在<a class="xref-link" href="#listing-relationships-migration">代码清单 14.1</a> 中,我们还设置了一个多键索引,确保 (<code>follower_id, followed_id</code>) 组合是唯一的,避免多次关注同一个用户。(可以和<a class="xref-link" href="chapter6.html#listing-email-uniqueness-index">代码清单 6.29</a> 中保持电子邮件地址唯一的索引,以及<a class="xref-link" href="chapter13.html#listing-micropost-migration">代码清单 13.3</a> 中的多键索引比较一下。)从 <a class="xref-link" href="#followed-users">14.1.4 节</a>起会看到,用户界面不会允许这样的事发生,但添加索引后,如果用户试图创建重复的关系(例如使用 <code>curl</code> 这样的命令行工具),应用会抛出异常。</p>
<p>为了创建 <code>relationships</code> 表,和之前一样,我们要迁移数据库:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails db:migrate
</code></pre></div>
</div>
<h5 id="exercises-a-problem-with-the-data-model-and-a-solution" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>对<a class="xref-link" href="#fig-user-has-many-following">图 14.7</a> 中 ID 为 1 的用户来说,<code>user.following.map(&:id)</code> 的值是什么?(<a class="xref-link" href="chapter4.html#blocks">4.3.2 节</a> 介绍过 <code>map(&:method_name)</code> 这种句法;<code>user.following.map(&:id)</code> 返回的是 ID 组成的数组。)</p>
</li>
<li>
<p>查看<a class="xref-link" href="#fig-user-has-many-following">图 14.7</a>,对 ID 为 2 的用户来说,<code>user.following</code> 的值是什么?<code>user.following.map(&:id)</code> 呢?</p>
</li>
</ol>
</section>
<section data-type="sect2" id="user-relationship-associations">
<h2><span class="title-label">14.1.2</span> <code>User</code> 模型和 <code>Relationship</code> 模型之间的关联</h2>
<p>在获取我关注的人和关注我的人之前,我们要先建立 <code>User</code> 模型和 <code>Relationship</code> 模型之间的关联。一个用户有多个“关系”(<code>has_many</code>),因为一个“关系”涉及到两个用户,所以“关系”同时属于(<code>belongs_to</code>)该用户和被关注的用户。</p>
<p>和 <a class="xref-link" href="chapter13.html#user-micropost-associations">13.1.3 节</a>创建微博的方式一样,我们要通过关联创建“关系”,如下面的代码所示:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">user</span><span class="p">.</span><span class="nf">active_relationships</span><span class="p">.</span><span class="nf">build</span><span class="p">(</span><span class="ss">followed_id: </span><span class="o">...</span><span class="p">)</span>
</code></pre></div>
</div>
<p>此时,你可能想在应用中加入类似于 <a class="xref-link" href="chapter13.html#user-micropost-associations">13.1.3 节</a>使用的代码。我们要添加的代码确实很像,但有两处不同。</p>
<p>首先,把用户和微博关联起来时我们是这么写的:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="ss">:microposts</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>之所以可以这么写,是因为 Rails 会寻找 <code>:microposts</code> 符号对应的模型,即 <code>Micropost</code>。<sup>[<a id="fn-ref-4" href="#fn-4">4</a>]</sup>可是现在这个模型名为 <code>Relationship</code>,而我们想写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">has_many</span> <span class="ss">:active_relationships</span>
</code></pre></div>
</div>
<p>所以要告诉 Rails 模型的类名。</p>
<p>其次,前面在 <code>Micropost</code> 模型中是这么写的:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">Micropost</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>之所以可以这么写,是因为 <code>microposts</code> 表中有识别用户的 <code>user_id</code> 列(<a class="xref-link" href="chapter13.html#the-basic-model">13.1.1 节</a>)。这种连接两个表的列,我们称之为外键(foreign key)。当指向 <code>User</code> 模型的外键为 <code>user_id</code> 时,Rails 会自动获知关联,因为默认情况下,Rails 会寻找名为 <code><class>_id</code> 的外键,其中 <code><class></code> 是模型类名的小写形式。<sup>[<a id="fn-ref-5" href="#fn-5">5</a>]</sup>现在,尽管我们处理的还是用户,但识别用户使用的外键是 <code>follower_id</code>,所以要告诉 Rails 这一变化。</p>
<p>综上所述,<code>User</code> 和 <code>Relationship</code> 模型之间的关联如<a class="xref-link" href="#listing-user-relationships-association">代码清单 14.2</a> 和<a class="xref-link" href="#listing-relationship-belongs-to">代码清单 14.3</a> 所示。</p>
<div id="listing-user-relationships-association" data-type="listing">
<h5><span class="title-label">代码清单 14.2</span>:实现主动关系中的 <code>has_many</code> 关联</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Relationship"</span><span class="p">,</span></span>
<span class="hll"> <span class="ss">foreign_key: </span><span class="s2">"follower_id"</span><span class="p">,</span></span>
<span class="hll"> <span class="ss">dependent: :destroy</span></span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>(因为删除用户时也要删除涉及这个用户的“关系”,所以我们在关联中加入了 <code>dependent: :destroy</code>。)</p>
<div id="listing-relationship-belongs-to" data-type="listing">
<h5><span class="title-label">代码清单 14.3</span>:在 <code>Relationship</code> 模型中添加 <code>belongs_to</code> 关联</h5>
<div class="source-file">app/models/relationship.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">Relationship</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="hll"> <span class="n">belongs_to</span> <span class="ss">:follower</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"User"</span></span>
<span class="hll"> <span class="n">belongs_to</span> <span class="ss">:followed</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"User"</span></span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>尽管 <a class="xref-link" href="#followers">14.1.5 节</a>才会用到 <code>followed</code> 关联,但同时添加易于理解。</p>
<p>建立上述关联后,会得到一系列类似于<a class="xref-link" href="chapter13.html#table-association-methods">表 13.1</a> 中的方法,如<a class="xref-link" href="#table-association-methods-relationships">表 14.1</a> 所示。</p>
<table id="table-association-methods-relationships" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 14.1</span>:<code>User</code> 模型与 <code>Relationship</code> 模型建立主动关系之后得到的方法简介</caption>
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">方法</th>
<th class="tableblock halign-left valign-top">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>active_relationship.follower</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">获取关注我的用户</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>active_relationship.followed</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">获取我关注的用户</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.active_relationships.create(followed_id: other_user.id)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建 <code>user</code> 发起的主动关系</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.active_relationships.create!(followed_id: other_user.id)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建 <code>user</code> 发起的主动关系(失败时抛出异常)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.active_relationships.build(followed_id: other_user.id)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">构建 <code>user</code> 发起的主动关系对象</p></td>
</tr>
</tbody>
</table>
<h5 id="exercises-relationship-user-associations" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>打开 Rails 控制台,使用<a class="xref-link" href="#table-association-methods-relationships">表 14.1</a> 中的 <code>create</code> 方法为数据库中的第一个用户和第二个用户建立主动关系。</p>
</li>
<li>
<p>确认 <code>active_relationship.followed</code> 和 <code>active_relationship.follower</code> 返回的值是正确的。</p>
</li>
</ol>
</section>
<section data-type="sect2" id="relationship-validations">
<h2><span class="title-label">14.1.3</span> 数据验证</h2>
<p>在继续之前,我们要在 <code>Relationship</code> 模型中添加一些验证。测试(<a class="xref-link" href="#listing-relationship-validation-tests">代码清单 14.4</a>)和应用代码(<a class="xref-link" href="#listing-relationship-validations">代码清单 14.5</a>)都非常直观。与生成的用户固件一样(<a class="xref-link" href="chapter6.html#listing-default-fixtures">代码清单 6.30</a>),生成的“关系”固件也违背了迁移中的唯一性约束(<a class="xref-link" href="#listing-relationships-migration">代码清单 14.1</a>)。这个问题的解决方法也和之前一样(<a class="xref-link" href="chapter6.html#listing-empty-fixtures">代码清单 6.31</a>)——删除自动生成的固件,如<a class="xref-link" href="#listing-empty-relationship-fixture">代码清单 14.6</a> 所示。</p>
<div id="listing-relationship-validation-tests" data-type="listing">
<h5><span class="title-label">代码清单 14.4</span>:测试 <code>Relationship</code> 模型中的验证</h5>
<div class="source-file">test/models/relationship_test.rb</div>
<div class="highlight language-ruby"><pre><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">RelationshipTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@relationship</span> <span class="o">=</span> <span class="no">Relationship</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">follower_id: </span><span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">).</span><span class="nf">id</span><span class="p">,</span>
<span class="ss">followed_id: </span><span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">).</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@relationship</span><span class="p">.</span><span class="nf">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should require a follower_id"</span> <span class="k">do</span>
<span class="vi">@relationship</span><span class="p">.</span><span class="nf">follower_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@relationship</span><span class="p">.</span><span class="nf">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should require a followed_id"</span> <span class="k">do</span>
<span class="vi">@relationship</span><span class="p">.</span><span class="nf">followed_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@relationship</span><span class="p">.</span><span class="nf">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<div id="listing-relationship-validations" data-type="listing">
<h5><span class="title-label">代码清单 14.5</span>:在 <code>Relationship</code> 模型中添加验证</h5>
<div class="source-file">app/models/relationship.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">Relationship</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:follower</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"User"</span>
<span class="n">belongs_to</span> <span class="ss">:followed</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"User"</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:follower_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span></span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:followed_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span></span>
<span class="k">end</span>
</code></pre></div>
</div>
<div id="listing-empty-relationship-fixture" data-type="listing">
<h5><span class="title-label">代码清单 14.6</span>:删除“关系”固件</h5>
<div class="source-file">test/fixtures/relationships.yml</div>
<div class="highlight language-yaml"><pre><code><span class="c1"># empty</span>
</code></pre></div>
</div>
<p>现在,测试应该可以通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 14.7</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<h5 id="exercises-relationship-validations" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>把<a class="xref-link" href="#listing-relationship-validations">代码清单 14.5</a> 中的验证注释掉,确认测试仍能通过。(这是 Rails 5 的变化,在之前的 Rails 版本中,必须添加那两个验证。为了明确表明意图,我们会留着验证。不过你要知道这一点,以防其他人编写的代码中没有这两个验证。)</p>
</li>
</ol>
</section>
<section data-type="sect2" id="followed-users">
<h2><span class="title-label">14.1.4</span> 我关注的用户</h2>
<p>现在到“关系”的核心部分了——获取我关注的用户(<code>following</code>)和关注我的用户(<code>followers</code>)。这里我们要首次用到 <code>has_many :through</code> 关联:用户通过 <code>Relationship</code> 模型关注多个用户,如<a class="xref-link" href="#fig-user-has-many-following">图 14.7</a> 所示。默认情况下,在 <code>has_many :through</code> 关联中,Rails 会寻找关联名单数形式对应的外键。例如:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">has_many</span> <span class="ss">:followeds</span><span class="p">,</span> <span class="ss">through: :active_relationships</span>
</code></pre></div>
</div>
<p>Rails 发现关联名是“followeds”,先把它变成单数形式“followed”,然后在 <code>relationships</code> 表中获取一个由 <code>followed_id</code> 组成的集合。不过,<a class="xref-link" href="#a-problem-with-the-data-model-and-a-solution">14.1.1 节</a>说过,写成 <code>user.followeds</code> 有点说不通,所以我们会使用 <code>user.following</code>。Rails 允许定制默认生成的关联方法:使用 <code>source</code> 参数指定 <code>following</code> 数组由 <code>followed_id</code> 组成,如<a class="xref-link" href="#listing-has-many-following-through-active-relationships">代码清单 14.8</a> 所示。</p>
<div id="listing-has-many-following-through-active-relationships" data-type="listing">
<h5><span class="title-label">代码清单 14.8</span>:在 <code>User</code> 模型中添加 <code>following</code> 关联</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
<span class="n">has_many</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Relationship"</span><span class="p">,</span>
<span class="ss">foreign_key: </span><span class="s2">"follower_id"</span><span class="p">,</span>
<span class="ss">dependent: :destroy</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">through: :active_relationships</span><span class="p">,</span> <span class="ss">source: :followed</span></span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>定义这个关联后,我们可以充分利用 Active Record 和数组的功能。例如,可以使用 <code>include?</code> 方法(<a class="xref-link" href="chapter4.html#arrays-and-ranges">4.3.1 节</a>)检查我关注的用户中有没有某个用户,或者通过关联查找一个用户:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">user</span><span class="p">.</span><span class="nf">following</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="n">user</span><span class="p">.</span><span class="nf">following</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
</code></pre></div>
</div>
<p>还可以像数组那样添加和删除元素:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">user</span><span class="p">.</span><span class="nf">following</span> <span class="o"><<</span> <span class="n">other_user</span>
<span class="n">user</span><span class="p">.</span><span class="nf">following</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
</code></pre></div>
</div>
<p>(<a class="xref-link" href="chapter4.html#arrays-and-ranges">4.3.1 节</a>说过,<code><<</code> 运算符把元素添加到数组末尾。)</p>
<p>很多情况下都可以把 <code>following</code> 当成数组来用,Rails 会使用特定的方式处理 <code>following</code>,所以这么做很高效。例如:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">following</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
</code></pre></div>
</div>
<p>这看起来好像是要把我关注的所有用户都从数据库中读取出来,然后再调用 <code>include?</code>。其实不然,为了提高效率,Rails 会直接在数据库层执行相关的操作。(和 <a class="xref-link" href="chapter13.html#rendering-microposts">13.2.1 节</a>使用 <code>user.microposts.count</code> 获取数量一样,都直接在数据库中操作。)</p>
<p>为了处理关注用户的操作,我们要定义两个辅助方法:<code>follow</code> 和 <code>unfollow</code>。这样我们就可以写 <code>user.follow(other_user)</code>。我们还要定义 <code>following?</code> 布尔值方法,检查一个用户是否关注了另一个用户。<sup>[<a id="fn-ref-6" href="#fn-6">6</a>]</sup></p>
<p>现在是编写测试的好时机,因为我们还要等很久才会开发关注用户的网页界面,如果一直没人监管,很难向前推进。我们可以为 <code>User</code> 模型编写一个简短的测试,先调用 <code>following?</code> 方法确认某个用户没有关注另一个用户,然后调用 <code>follow</code> 方法关注那个用户,再使用 <code>following?</code> 方法确认关注成功了,最后调用 <code>unfollow</code> 方法取消关注,并确认操作成功,如<a class="xref-link" href="#listing-utility-method-tests">代码清单 14.9</a> 所示。</p>
<div id="listing-utility-method-tests" data-type="listing">
<h5><span class="title-label">代码清单 14.9</span>:测试关注用户相关的几个辅助方法 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">test</span> <span class="s2">"should follow and unfollow a user"</span> <span class="k">do</span>
<span class="n">michael</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="n">archer</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">)</span>
<span class="n">assert_not</span> <span class="n">michael</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">michael</span><span class="p">.</span><span class="nf">follow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">assert</span> <span class="n">michael</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">michael</span><span class="p">.</span><span class="nf">unfollow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">assert_not</span> <span class="n">michael</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>把 <code>following</code> 关联视作对象,可以像<a class="xref-link" href="#listing-follow-unfollow-following">代码清单 14.10</a> 那样定义 <code>follow</code>、<code>unfollow</code> 和 <code>following?</code> 三个方法。(注意,只要可能,我们就省略 <code>self</code>。)</p>
<div id="listing-follow-unfollow-following" data-type="listing">
<h5><span class="title-label">代码清单 14.10</span>:定义关注用户相关的几个辅助方法 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">feed</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="c1"># 关注另一个用户</span>
<span class="k">def</span> <span class="nf">follow</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="hll"> <span class="n">following</span> <span class="o"><<</span> <span class="n">other_user</span></span>
<span class="k">end</span>
<span class="c1"># 取消关注另一个用户</span>
<span class="k">def</span> <span class="nf">unfollow</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="hll"> <span class="n">following</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span></span>
<span class="k">end</span>
<span class="c1"># 如果当前用户关注了指定的用户,返回 true</span>
<span class="k">def</span> <span class="nf">following?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="hll"> <span class="n">following</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span></span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>现在,测试能通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 14.11</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<h5 id="exercises-following" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>在 Rails 控制台中重现<a class="xref-link" href="#listing-utility-method-tests">代码清单 14.9</a> 中的步骤。</p>
</li>
<li>
<p>前一题中各个操作对应的 SQL 语句是什么?</p>
</li>
</ol>
</section>
<section data-type="sect2" id="followers">
<h2><span class="title-label">14.1.5</span> 关注我的人</h2>
<p>“关系”的最后一部分是定义与 <code>user.following</code> 对应的 <code>user.followers</code> 方法。从<a class="xref-link" href="#fig-user-has-many-following">图 14.7</a> 中得知,获取关注我的人所需的数据都已经存在于 <code>relationships</code> 表中(我们要参照<a class="xref-link" href="#listing-user-relationships-association">代码清单 14.2</a> 中实现 <code>active_relationships</code> 表的方式)。其实我们要使用的方法和实现我关注的人一样,只要对调 <code>follower_id</code> 和 <code>followed_id</code> 的位置,并把 <code>active_relationships</code> 换成 <code>passive_relationships</code> 即可,如<a class="xref-link" href="#fig-user-has-many-followers">图 14.9</a> 所示。</p>
<div id="fig-user-has-many-followers" class="figure"><img src="images/chapter14/user_has_many_followers_3rd_edition.png" alt="user has many followers 3rd edition" /><div class="figcaption"><span class="title-label">图 14.9</span>:通过被动关系获取关注我的用户</div></div>
<p>参照<a class="xref-link" href="#listing-has-many-following-through-active-relationships">代码清单 14.8</a>,我们可以使用<a class="xref-link" href="#listing-has-many-following-through-passive-relationships">代码清单 14.12</a> 中的代码实现<a class="xref-link" href="#fig-user-has-many-followers">图 14.9</a> 中的模型。</p>
<div id="listing-has-many-following-through-passive-relationships" data-type="listing">
<h5><span class="title-label">代码清单 14.12</span>:使用被动关系实现 <code>user.followers</code></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
<span class="n">has_many</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Relationship"</span><span class="p">,</span>
<span class="ss">foreign_key: </span><span class="s2">"follower_id"</span><span class="p">,</span>
<span class="ss">dependent: :destroy</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:passive_relationships</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Relationship"</span><span class="p">,</span></span>
<span class="hll"> <span class="ss">foreign_key: </span><span class="s2">"followed_id"</span><span class="p">,</span></span>
<span class="hll"> <span class="ss">dependent: :destroy</span></span>
<span class="n">has_many</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">through: :active_relationships</span><span class="p">,</span> <span class="ss">source: :followed</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:followers</span><span class="p">,</span> <span class="ss">through: :passive_relationships</span><span class="p">,</span> <span class="ss">source: :follower</span></span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>值得注意的是,其实我们可以省略 <code>followers</code> 关联中的 <code>source</code> 参数,直接写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">has_many</span> <span class="ss">:followers</span><span class="p">,</span> <span class="ss">through: :passive_relationships</span>
</code></pre></div>
</div>
<p>因为 Rails 会把“followers”转换成单数“follower”,然后查找名为 <code>follower_id</code> 的外键。<a class="xref-link" href="#listing-has-many-following-through-passive-relationships">代码清单 14.12</a> 之所以保留了 <code>source</code> 参数,是为了和 <code>has_many :following</code> 关联的结构保持一致。</p>
<p>我们可以使用 <code>followers.include?</code> 测试这个数据模型,如<a class="xref-link" href="#listing-followers-test">代码清单 14.13</a> 所示。(这段测试本可以使用与 <code>following?</code> 方法对应的 <code>followed_by?</code> 方法,但应用中用不到,所以我们没这么做。)</p>
<div id="listing-followers-test" data-type="listing">
<h5><span class="title-label">代码清单 14.13</span>:测试 <code>followers</code> 关联 <span class="green">GREEN</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">test</span> <span class="s2">"should follow and unfollow a user"</span> <span class="k">do</span>
<span class="n">michael</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="n">archer</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">)</span>
<span class="n">assert_not</span> <span class="n">michael</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">michael</span><span class="p">.</span><span class="nf">follow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">assert</span> <span class="n">michael</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="hll"> <span class="n">assert</span> <span class="n">archer</span><span class="p">.</span><span class="nf">followers</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">michael</span><span class="p">)</span></span>
<span class="n">michael</span><span class="p">.</span><span class="nf">unfollow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">assert_not</span> <span class="n">michael</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>我们只在<a class="xref-link" href="#listing-utility-method-tests">代码清单 14.9</a> 的基础上增加了一行代码,但若想让这个测试通过,很多事情都要正确处理才行,这足以测试<a class="xref-link" href="#listing-has-many-following-through-passive-relationships">代码清单 14.12</a> 中的关联。</p>
<p>现在,整个测试组件都能通过:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<h5 id="exercises-followers" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>在 Rails 控制台中为数据库中的第一个用户(赋值给 <code>user</code> 变量)添加几个关注者,<code>user.followers.map(:id)</code> 的值是什么?</p>
</li>
<li>
<p>确认 <code>user.followers.count</code> 的值与你在前一题中添加的关注者数量一样。</p>
</li>
<li>
<p><code>user.followers.count</code> 对应的 SQL 语句是什么?与 <code>user.followers.to_a.count</code> 有什么区别?提示:假设这个用户有一百万个关注者。</p>
</li>
</ol>
</section>
</section>
<section data-type="sect1" id="a-web-interface-for-following-users">
<h1><span class="title-label">14.2</span> 关注用户的网页界面</h1>
<p><a class="xref-link" href="#the-relationship-model">14.1 节</a>用到了很多数据模型技术,可能要花些时间才能完全理解。其实,理解这些关联最好的方式是在网页界面中使用。</p>
<p>在本章的导言中,我们介绍了关注用户的操作流程。本节,我们要实现这些构思的页面,以及关注和取消关注功能。我们还会创建两个页面,分别列出我关注的用户和关注我的用户。在 <a class="xref-link" href="#the-status-feed">14.3 节</a>,我们会实现用户的动态流,届时,这个演示应用才算完成。</p>
<section data-type="sect2" id="sample-following-data">
<h2><span class="title-label">14.2.1</span> 示例数据</h2>
<p>和之前的几章一样,我们要使用 <code>rails db:seed</code> 命令把“关系”相关的种子数据加载到数据库中。有了示例数据,我们就可以先实现网页界面,本节末尾再实现后端功能。</p>
<p>“关系”相关的种子数据如<a class="xref-link" href="#listing-sample-relationships">代码清单 14.14</a> 所示。我们让第一个用户关注第 3-51 个用户,让第 4-41 个用户关注第一个用户。这样的数据足够用来开发应用的界面了。</p>
<div id="listing-sample-relationships" data-type="listing">
<h5><span class="title-label">代码清单 14.14</span>:在种子数据中添加“关系”相关的数据</h5>
<div class="source-file">db/seeds.rb</div>
<div class="highlight language-yaml"><pre><code><span class="c1"># Users</span>
<span class="s">User.create!(name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Example</span><span class="nv"> </span><span class="s">User"</span><span class="err">,</span>
<span class="na">email</span><span class="pi">:</span> <span class="s2">"</span><span class="s">example@railstutorial.org"</span><span class="err">,</span>
<span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">foobar"</span><span class="err">,</span>
<span class="na">password_confirmation</span><span class="pi">:</span> <span class="s2">"</span><span class="s">foobar"</span><span class="err">,</span>
<span class="na">admin</span><span class="pi">:</span> <span class="no">true</span><span class="s">,</span>
<span class="na">activated</span><span class="pi">:</span> <span class="no">true</span><span class="s">,</span>
<span class="na">activated_at</span><span class="pi">:</span> <span class="s">Time.zone.now)</span>
<span class="s">99.times do |n|</span>
<span class="s">name = Faker::Name.name</span>
<span class="s">email = "example-#{n+1}@railstutorial.org"</span>
<span class="s">password = "password"</span>
<span class="s">User.create!(name</span><span class="pi">:</span> <span class="s">name,</span>
<span class="s">email</span><span class="pi">:</span> <span class="s">email,</span>
<span class="s">password</span><span class="pi">:</span> <span class="s">password,</span>
<span class="s">password_confirmation</span><span class="pi">:</span> <span class="s">password,</span>
<span class="s">activated</span><span class="pi">:</span> <span class="no">true</span><span class="s">,</span>
<span class="s">activated_at</span><span class="pi">:</span> <span class="s">Time.zone.now)</span>
<span class="s">end</span>
<span class="c1"># Microposts</span>
<span class="s">users = User.order(:created_at).take(6)</span>
<span class="s">50.times do</span>
<span class="s">content = Faker::Lorem.sentence(5)</span>
<span class="s">users.each { |user| user.microposts.create!(content</span><span class="pi">:</span> <span class="s">content) }</span>
<span class="s">end</span>
<span class="hll"><span class="c1"># Following relationships</span></span>
<span class="hll"><span class="s">users = User.all</span></span>
<span class="hll"><span class="s">user = users.first</span></span>
<span class="hll"><span class="s">following = users[2..50]</span></span>
<span class="hll"><span class="s">followers = users[3..40]</span></span>
<span class="hll"><span class="s">following.each { |followed| user.follow(followed) }</span></span>
<span class="hll"><span class="s">followers.each { |follower| follower.follow(user) }</span></span>
</code></pre></div>
</div>
<p>然后像之前一样,执行下面的命令,重置数据库之后重新加载种子数据:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails db:migrate:reset
<span class="nv">$ </span>rails db:seed
</code></pre></div>
</div>
<h5 id="exercises-sample-following-data" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>在 Rails 控制台中确认 <code>User.first.followers.count</code> 的值与<a class="xref-link" href="#listing-sample-relationships">代码清单 14.14</a> 设定的一样。</p>
</li>
<li>
<p>确认 <code>User.first.following.count</code> 的值也正确。</p>
</li>
</ol>
</section>
<section data-type="sect2" id="stats-and-a-follow-form">
<h2><span class="title-label">14.2.2</span> 数量统计和关注表单</h2>
<p>现在示例用户已经关注了其他用户,也被其他用户关注了,我们要更新一下用户资料页面和首页,把这些变动显示出来。首先,我们要创建一个局部视图,在资料页面和首页显示我关注的人和关注我的人的数量。然后再添加关注和取消关注表单,并且在专门的页面中列出我关注的用户和关注我的用户。</p>
<p><a class="xref-link" href="#a-problem-with-the-data-model-and-a-solution">14.1.1 节</a>说过,我们参照了 Twitter 的叫法,在我关注的用户数量后使用“following”作标注,例如“50 following”。<a class="xref-link" href="#fig-page-flow-profile-mockup">图 14.1</a> 中的构思图就使用了这种表述方式,现在把这部分单独摘出来,如<a class="xref-link" href="#fig-stats-partial-mockup">图 14.10</a> 所示。</p>
<div id="fig-stats-partial-mockup" class="figure"><img src="images/chapter14/stats_partial_mockup.png" alt="stats partial mockup" /><div class="figcaption"><span class="title-label">图 14.10</span>:数量统计局部视图的构思图</div></div>
<p><a class="xref-link" href="#fig-stats-partial-mockup">图 14.10</a> 中显示的数量统计包含当前用户关注的人数和关注当前用户的人数,而且分别链接到专门的用户列表页面。在<a class="xref-link" href="chapter5.html#filling-in-the-layout">第 5 章</a>,我们使用 <code>#</code> 占位符代替真实的网址,因为那时我们还没怎么接触路由。现在,虽然 <a class="xref-link" href="#following-and-followers-pages">14.2.3 节</a>才会创建所需的页面,不过可以先设置路由,如<a class="xref-link" href="#listing-following-followers-actions-routes">代码清单 14.15</a> 所示。这段代码在 <code>resources</code> 块中使用了 <code>:member</code> 方法。我们以前没用过这个方法,你可以猜一下这个方法的作用是什么。</p>
<div id="listing-following-followers-actions-routes" data-type="listing">
<h5><span class="title-label">代码清单 14.15</span>:在 <code>Users</code> 控制器中添加 <code>following</code> 和 <code>followers</code> 两个动作</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'/help'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'/about'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'/contact'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'/signup'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'users#new'</span>
<span class="n">get</span> <span class="s1">'/login'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#new'</span>
<span class="n">post</span> <span class="s1">'/login'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#create'</span>
<span class="n">delete</span> <span class="s1">'/logout'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#destroy'</span>
<span class="hll"> <span class="n">resources</span> <span class="ss">:users</span> <span class="k">do</span></span>
<span class="hll"> <span class="n">member</span> <span class="k">do</span></span>
<span class="hll"> <span class="n">get</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">:followers</span></span>
<span class="hll"> <span class="k">end</span></span>
<span class="hll"> <span class="k">end</span></span>
<span class="n">resources</span> <span class="ss">:account_activations</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">]</span>
<span class="n">resources</span> <span class="ss">:password_resets</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="n">resources</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>你可能猜到了,设定上述路由后,得到的 URL 地址类似 /users/1/following 和 /users/1/followers 这种形式。不错,<a class="xref-link" href="#listing-following-followers-actions-routes">代码清单 14.15</a> 的作用确实如此。因为这两个页面都是用来显示数据的,所以我们使用了 <code>get</code> 方法,指定这两个地址响应的是 GET 请求。而且,使用 <code>member</code> 方法后,这两个动作对应的 URL 地址中都会包含用户的 ID。除此之外,我们还可以使用 <code>collection</code> 方法,但这样 URL 中就没有用户 ID 了。所以,如下的代码</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">resources</span> <span class="ss">:users</span> <span class="k">do</span>
<span class="n">collection</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:tigers</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>得到的 URL 是 /users/tigers(或许可以用来显示应用中所有的老虎)。<sup>[<a id="fn-ref-7" href="#fn-7">7</a>]</sup></p>
<p><a class="xref-link" href="#listing-following-followers-actions-routes">代码清单 14.15</a> 生成的路由如<a class="xref-link" href="#table-following-routes">表 14.2</a> 所示。留意一下我关注的用户页面和关注我的用户页面的具名路由是什么,稍后会用到。</p>
<table id="table-following-routes" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 14.2</span>:<a class="xref-link" href="#listing-following-followers-actions-routes">代码清单 14.15</a> 中设置的规则生成的 REST 路由</caption>
<colgroup>
<col style="width: 15%;" />
<col style="width: 20%;" />
<col style="width: 15%;" />
<col style="width: 50%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">HTTP 请求</th>
<th class="tableblock halign-left valign-top">URL</th>
<th class="tableblock halign-left valign-top">动作</th>
<th class="tableblock halign-left valign-top">具名路由</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">GET</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1/following</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>following</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>following_user_path(1)</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">GET</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1/followers</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>followers</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>followers_user_path(1)</code></p></td>
</tr>
</tbody>
</table>
<p>设好了路由后,我们来编写数量统计局部视图。我们要在一个 <code>div</code> 元素中显示几个链接,如<a class="xref-link" href="#listing-stats-partial">代码清单 14.16</a> 所示。</p>
<div id="listing-stats-partial" data-type="listing">
<h5><span class="title-label">代码清单 14.16</span>:显示数量统计的局部视图</h5>
<div class="source-file">app/views/shared/_stats.html.erb</div>
<div class="highlight language-erb"><pre><code><span class="cp"><%</span> <span class="vi">@user</span> <span class="o">||=</span> <span class="n">current_user</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"stats"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"</span><span class="cp"><%=</span> <span class="n">following_user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span><span class="s">"</span><span class="nt">></span>
<span class="nt"><strong</span> <span class="na">id=</span><span class="s">"following"</span> <span class="na">class=</span><span class="s">"stat"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">following</span><span class="p">.</span><span class="nf">count</span> <span class="cp">%></span>
<span class="nt"></strong></span>
following
<span class="nt"></a></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"</span><span class="cp"><%=</span> <span class="n">followers_user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span><span class="s">"</span><span class="nt">></span>
<span class="nt"><strong</span> <span class="na">id=</span><span class="s">"followers"</span> <span class="na">class=</span><span class="s">"stat"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">followers</span><span class="p">.</span><span class="nf">count</span> <span class="cp">%></span>
<span class="nt"></strong></span>
followers
<span class="nt"></a></span>
<span class="nt"></div></span>
</code></pre></div>
</div>
<p>因为用户资料页面和首页都要使用这个局部视图,所以在<a class="xref-link" href="#listing-stats-partial">代码清单 14.16</a> 的第一行,我们要获取正确的用户对象:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><code><span class="cp"><%</span> <span class="vi">@user</span> <span class="o">||=</span> <span class="n">current_user</span> <span class="cp">%></span>
</code></pre></div>
</div>
<p>我们在<a class="xref-link" href="chapter8.html#aside-or-equals">旁注 8.1</a>中介绍过这种用法,如果 <code>@user</code> 不是 <code>nil</code>(在用户资料页面),这行代码没什么效果;如果是 <code>nil</code>(在首页),就会把当前用户赋值给 <code>@user</code>。还有一处要注意,我关注的人数和关注我的人数是通过关联获取的,分别使用 <code>@user.following.count</code> 和 <code>@user.followers.count</code>。</p>
<p>我们可以和<a class="xref-link" href="chapter13.html#listing-user-show-microposts">代码清单 13.24</a> 中获取微博数量的代码对比一下,微博的数量通过 <code>@user.microposts.count</code> 获取。为了提高效率,Rails 会直接在数据库层统计数量。</p>
<p>最后还有一个细节需要注意,我们为某些元素指定了 CSS ID,例如:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><span class="nt"><strong</span> <span class="na">id=</span><span class="s">"following"</span> <span class="na">class=</span><span class="s">"stat"</span><span class="nt">></span>
...
<span class="nt"></strong></span>
</code></pre></div>
</div>
<p>这些 ID 是为 <a class="xref-link" href="#a-working-follow-button-with-ajax">14.2.5 节</a>中的 Ajax 准备的,因为 Ajax 要通过独一无二的 ID 获取页面中的元素。</p>
<p>编写好局部视图,把它放入首页就很简单了,如<a class="xref-link" href="#listing-home-page-stats">代码清单 14.17</a> 所示。</p>
<div id="listing-home-page-stats" data-type="listing">
<h5><span class="title-label">代码清单 14.17</span>:在首页显示数量统计</h5>
<div class="source-file">app/views/static_pages/home.html.erb</div>
<div class="highlight language-erb"><pre><code><span class="cp"><%</span> <span class="k">if</span> <span class="n">logged_in?</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><aside</span> <span class="na">class=</span><span class="s">"col-md-4"</span><span class="nt">></span>
<span class="nt"><section</span> <span class="na">class=</span><span class="s">"user_info"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/user_info'</span> <span class="cp">%></span>
<span class="nt"></section></span>
<span class="hll"> <span class="nt"><section</span> <span class="na">class=</span><span class="s">"stats"</span><span class="nt">></span></span>
<span class="hll"> <span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</span> <span class="cp">%></span></span>
<span class="hll"> <span class="nt"></section></span></span>
<span class="nt"><section</span> <span class="na">class=</span><span class="s">"micropost_form"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/micropost_form'</span> <span class="cp">%></span>
<span class="nt"></section></span>
<span class="nt"></aside></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"col-md-8"</span><span class="nt">></span>
<span class="nt"><h3></span>Micropost Feed<span class="nt"></h3></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/feed'</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
.
.
.
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre></div>
</div>
<p>我们要添加一些 SCSS 代码,美化数量统计,如<a class="xref-link" href="#listing-stats-css">代码清单 14.18</a> 所示(包含本章用到的所有样式)。添加样式后,首页如<a class="xref-link" href="#fig-home-page-follow-stats">图 14.11</a> 所示。</p>
<div id="listing-stats-css" data-type="listing">
<h5><span class="title-label">代码清单 14.18</span>:首页侧边栏的 SCSS 样式</h5>
<div class="source-file">app/assets/stylesheets/custom.scss</div>
<div class="highlight language-scss"><pre><code><span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">/*</span> <span class="nt">sidebar</span> <span class="o">*/</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.gravatar</span> <span class="p">{</span>
<span class="nl">float</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.gravatar_edit</span> <span class="p">{</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="hll"><span class="nc">.stats</span> <span class="p">{</span></span>
<span class="nl">overflow</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nt">a</span> <span class="p">{</span>
<span class="nl">float</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">border-left</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nv">$gray-lighter</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">gray</span><span class="p">;</span>
<span class="k">&</span><span class="nd">:first-child</span> <span class="p">{</span>
<span class="nl">padding-left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">&</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">blue</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nt">strong</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="hll"><span class="nc">.user_avatars</span> <span class="p">{</span></span>
<span class="nl">overflow</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nc">.gravatar</span> <span class="p">{</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">1px</span> <span class="m">1px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">a</span> <span class="p">{</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="hll"><span class="nc">.users.follow</span> <span class="p">{</span></span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="cm">/* forms */</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
</code></pre></div>
</div>
<div id="fig-home-page-follow-stats" class="figure"><img src="images/chapter14/home_page_follow_stats_3rd_edition.png" alt="home page follow stats 3rd edition" /><div class="figcaption"><span class="title-label">图 14.11</span>:显示有数量统计的首页</div></div>
<p>稍后再把数量统计局部视图添加到用户资料页面中,现在先来编写关注和取消关注按钮的局部视图,如<a class="xref-link" href="#listing-follow-form-partial">代码清单 14.19</a> 所示。</p>
<div id="listing-follow-form-partial" data-type="listing">
<h5><span class="title-label">代码清单 14.19</span>:显示关注或取消关注表单的局部视图</h5>
<div class="source-file">app/views/users/_follow_form.html.erb</div>
<div class="highlight language-erb"><pre><code><span class="cp"><%</span> <span class="k">unless</span> <span class="n">current_user?</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">id=</span><span class="s">"follow_form"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">following?</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'unfollow'</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'follow'</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre></div>
</div>
<p>这段代码其实也没做什么,只是把具体的工作委托给 <code>follow</code> 和 <code>unfollow</code> 局部视图了。我们要再次设置路由,加入 <code>Relationships</code> 资源,如<a class="xref-link" href="#listing-relationships-resource">代码清单 14.20</a> 所示,这与 <code>Microposts</code> 资源的设置类似(<a class="xref-link" href="chapter13.html#listing-microposts-resource">代码清单 13.30</a>)。</p>
<div id="listing-relationships-resource" data-type="listing">
<h5><span class="title-label">代码清单 14.20</span>:添加 <code>Relationships</code> 资源的路由规则</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'help'</span> <span class="o">=></span> <span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'about'</span> <span class="o">=></span> <span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'contact'</span> <span class="o">=></span> <span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'signup'</span> <span class="o">=></span> <span class="s1">'users#new'</span>
<span class="n">get</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#new'</span>
<span class="n">post</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#create'</span>
<span class="n">delete</span> <span class="s1">'logout'</span> <span class="o">=></span> <span class="s1">'sessions#destroy'</span>
<span class="n">resources</span> <span class="ss">:users</span> <span class="k">do</span>
<span class="n">member</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">:followers</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">resources</span> <span class="ss">:account_activations</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">]</span>
<span class="n">resources</span> <span class="ss">:password_resets</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="n">resources</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="p">]</span>
<span class="hll"> <span class="n">resources</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="p">]</span></span>
<span class="k">end</span>
</code></pre></div>
</div>
<p><code>follow</code> 和 <code>unfollow</code> 局部视图的代码分别如<a class="xref-link" href="#listing-follow-form">代码清单 14.21</a> 和<a class="xref-link" href="#listing-unfollow-form">代码清单 14.22</a> 所示。</p>
<div id="listing-follow-form" data-type="listing">
<h5><span class="title-label">代码清单 14.21</span>:关注用户的表单</h5>
<div class="source-file">app/views/users/_follow.html.erb</div>
<div class="highlight language-erb"><pre><code><span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="n">current_user</span><span class="p">.</span><span class="nf">active_relationships</span><span class="p">.</span><span class="nf">build</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><div></span><span class="cp"><%=</span> <span class="n">hidden_field_tag</span> <span class="ss">:followed_id</span><span class="p">,</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">id</span> <span class="cp">%></span><span class="nt"></div></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Follow"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn btn-primary"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre></div>
</div>
<div id="listing-unfollow-form" data-type="listing">
<h5><span class="title-label">代码清单 14.22</span>:取消关注用户的表单</h5>
<div class="source-file">app/views/users/_unfollow.html.erb</div>
<div class="highlight language-erb"><pre><code><span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="n">current_user</span><span class="p">.</span><span class="nf">active_relationships</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">followed_id: </span><span class="vi">@user</span><span class="p">.</span><span class="nf">id</span><span class="p">),</span>
<span class="ss">html: </span><span class="p">{</span> <span class="ss">method: :delete</span> <span class="p">})</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Unfollow"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre></div>
</div>
<p>这两个表单都使用 <code>form_for</code> 处理 <code>Relationship</code> 模型对象,二者之间主要的不同是,<a class="xref-link" href="#listing-follow-form">代码清单 14.21</a> 用于构建一个新“关系”,而<a class="xref-link" href="#listing-unfollow-form">代码清单 14.22</a> 查找现有的“关系”。很显然,第一个表单会向 <code>Relationships</code> 控制器的 <code>create</code> 动作发送 <code>POST</code> 请求,创建“关系”;而第二个表单向 <code>destroy</code> 动作发送 <code>DELETE</code> 请求,销毁“关系”。(这两个动作在 <a class="xref-link" href="#a-working-follow-button-the-standard-way">14.2.4 节</a>编写。)你可能还注意到了,关注用户的表单中除了按钮之外什么内容也没有,但是仍然要把 <code>followed_id</code> 发送给控制器。在<a class="xref-link" href="#listing-follow-form">代码清单 14.21</a> 中,我们使用 <code>hidden_field_tag</code> 方法把 <code>followed_id</code> 添加到表单中,生成的 HTML 如下:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><code><span class="nt"><input</span> <span class="na">id=</span><span class="s">"followed_id"</span> <span class="na">name=</span><span class="s">"followed_id"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"3"</span> <span class="nt">/></span>
</code></pre></div>
</div>
<p><a class="xref-link" href="chapter12.html#resetting-the-password">12.3 节</a>说过,隐藏的 <code>input</code> 标签会把所需的信息包含在表单中,但在浏览器中不显示。</p>
<p>现在我们可以在资料页面中加入关注表单和数量统计了,如<a class="xref-link" href="#listing-user-follow-form-profile-stats">代码清单 14.23</a> 所示,只需渲染相应的局部视图即可。显示有关注按钮和取消关注按钮的用户资料页面分别如<a class="xref-link" href="#fig-profile-follow-button">图 14.12</a> 和<a class="xref-link" href="#fig-profile-unfollow-button">图 14.13</a> 所示。</p>
<div id="listing-user-follow-form-profile-stats" data-type="listing">
<h5><span class="title-label">代码清单 14.23</span>:在用户资料页面加入关注表单和数量统计</h5>
<div class="source-file">app/views/users/show.html.erb</div>
<div class="highlight language-erb"><pre><code><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><aside</span> <span class="na">class=</span><span class="s">"col-md-4"</span><span class="nt">></span>
<span class="nt"><section></span>
<span class="nt"><h1></span>
<span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span>
<span class="nt"></h1></span>
<span class="nt"></section></span>
<span class="hll"> <span class="nt"><section</span> <span class="na">class=</span><span class="s">"stats"</span><span class="nt">></span></span>
<span class="hll"> <span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</span> <span class="cp">%></span></span>
<span class="hll"> <span class="nt"></section></span></span>
<span class="nt"></aside></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"col-md-8"</span><span class="nt">></span>
<span class="hll"> <span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'follow_form'</span> <span class="k">if</span> <span class="n">logged_in?</span> <span class="cp">%></span></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">any?</span> <span class="cp">%></span>
<span class="nt"><h3></span>Microposts (<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">count</span> <span class="cp">%></span>)<span class="nt"></h3></span>
<span class="nt"><ol</span> <span class="na">class=</span><span class="s">"microposts"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@microposts</span> <span class="cp">%></span>
<span class="nt"></ol></span>
<span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="vi">@microposts</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div>
</div>
<p>稍后我们会让这些按钮起作用,而且将使用两种方式实现,一种是常规方式(<a class="xref-link" href="#a-working-follow-button-the-standard-way">14.2.4 节</a>),另一种使用 Ajax(<a class="xref-link" href="#a-working-follow-button-with-ajax">14.2.5 节</a>)。不过在此之前,我们要创建剩下的页面——我关注的用户列表页面和关注我的用户列表页面。</p>
<h5 id="exercises-stats-and-a-follow-form" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>确认 /users/2 页面有关注按钮,/users/5 页面有取消关注按钮。/users/1 页面有关注按钮吗?</p>
</li>
<li>
<p>在浏览器中确认首页和资料页面有数量统计。</p>
</li>
<li>
<p>为首页中的数量统计编写测试。提示:把测试添加到<a class="xref-link" href="chapter13.html#listing-user-profile-test">代码清单 13.28</a> 中。为什么不用再测试资料页面的数量统计了?</p>
</li>
</ol>
<div id="fig-profile-follow-button" class="figure"><img src="images/chapter14/profile_follow_button_3rd_edition.png" alt="profile follow button 3rd edition" /><div class="figcaption"><span class="title-label">图 14.12</span>:某个用户的资料页面(/users/2),显示有关注按钮</div></div>
<div id="fig-profile-unfollow-button" class="figure"><img src="images/chapter14/profile_unfollow_button_3rd_edition.png" alt="profile unfollow button 3rd edition" /><div class="figcaption"><span class="title-label">图 14.13</span>:某个用户的资料页面(/users/5),显示有取消关注按钮</div></div>
</section>
<section data-type="sect2" id="following-and-followers-pages">
<h2><span class="title-label">14.2.3</span> 我关注的用户列表页面和关注我的用户列表页面</h2>
<p>我关注的用户列表页面和关注我的用户列表页面是资料页面和用户列表页面的混合体,在侧边栏显示用户的信息(包括数量统计),再列出一系列用户。除此之外,还会在侧边栏中显示一个用户头像列表。构思图如<a class="xref-link" href="#fig-following-mockup">图 14.14</a>(我关注的用户)和<a class="xref-link" href="#fig-followers-mockup">图 14.15</a>(关注我的用户)所示。</p>
<div id="fig-following-mockup" class="figure"><img src="images/chapter14/following_mockup_bootstrap.png" alt="following mockup bootstrap" /><div class="figcaption"><span class="title-label">图 14.14</span>:我关注的用户列表页面构思图</div></div>
<div id="fig-followers-mockup" class="figure"><img src="images/chapter14/followers_mockup_bootstrap.png" alt="followers mockup bootstrap" /><div class="figcaption"><span class="title-label">图 14.15</span>:关注我的用户列表页面构思图</div></div>
<p>首先,我们要让这两个页面的地址可访问。按照 Twitter 的方式,访问这两个页面都需要先登录。我们要先编写测试,参照以前的访问限制测试,写出的测试如<a class="xref-link" href="#listing-following-followers-authorization-test">代码清单 14.24</a> 所示。注意,<a class="xref-link" href="#listing-following-followers-authorization-test">代码清单 14.24</a> 用到了<a class="xref-link" href="#table-following-routes">表 14.2</a> 中的具名路由。</p>
<div id="listing-following-followers-authorization-test" data-type="listing">
<h5><span class="title-label">代码清单 14.24</span>:测试我关注的用户列表页面和关注我的用户列表页面的访问限制</h5>
<div class="source-file">test/controllers/users_controller_test.rb</div>
<div class="highlight language-ruby"><pre><code><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UsersControllerTest</span> <span class="o"><</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">IntegrationTest</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="vi">@other_user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">test</span> <span class="s2">"should redirect following when not logged in"</span> <span class="k">do</span>
<span class="n">get</span> <span class="n">following_user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>
<span class="n">assert_redirected_to</span> <span class="n">login_url</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should redirect followers when not logged in"</span> <span class="k">do</span>
<span class="n">get</span> <span class="n">followers_user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>
<span class="n">assert_redirected_to</span> <span class="n">login_url</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>在实现这两个页面的过程中,唯一很难想到的是,我们要在 <code>Users</code> 控制器中添加相应的两个动作。按照<a class="xref-link" href="#listing-following-followers-actions-routes">代码清单 14.15</a> 中的路由规则,这两个动作应该命名为 <code>following</code> 和 <code>followers</code>。在这两个动作中,需要设置页面的标题、查找用户,获取 <code>@user.followed_users</code> 或 <code>@user.followers</code>(要分页显示),然后再渲染页面,如<a class="xref-link" href="#listing-following-followers-actions">代码清单 14.25</a> 所示。</p>
<div id="listing-following-followers-actions" data-type="listing">
<h5><span class="title-label">代码清单 14.25</span>:<code>following</code> 和 <code>followers</code> 动作 <span class="red">RED</span></h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:logged_in_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:index</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:destroy</span><span class="p">,</span>
<span class="ss">:following</span><span class="p">,</span> <span class="ss">:followers</span><span class="p">]</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">following</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="s2">"Following"</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">following</span><span class="p">.</span><span class="nf">paginate</span><span class="p">(</span><span class="ss">page: </span><span class="n">params</span><span class="p">[</span><span class="ss">:page</span><span class="p">])</span>
<span class="n">render</span> <span class="s1">'show_follow'</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">followers</span>