-
Notifications
You must be signed in to change notification settings - Fork 2
/
chapter11.html
1884 lines (1577 loc) · 193 KB
/
chapter11.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>第 11 章 关注用户</title>
<meta name="author" content="Andor Chen" />
<link rel="stylesheet" href="assets/styles/style.css" />
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript" src="assets/js/global.js"></script>
</head>
<body>
<div class="wrapper">
<div class="header">
<h1 class="logo"><a class="ir" href="http://railstutorial-china.org/rails4">Ruby on Rails 教程</a></h1>
<p class="subtitle">Ruby on Rails Tutorial 原书第 2 版(涵盖 Rails 4)</p>
</div>
<div class="content">
<div class="item chapter">
<h1 id="chapter-11"><span>第 11 章</span> 关注用户</h1>
<ol class="toc"> <li class="level-2">
<a href="#section-11-1">11.1 关系模型</a>
</li>
<li class="level-3">
<a href="#section-11-1-1">11.1.1 数据模型带来的问题以及解决方式</a>
</li>
<li class="level-3">
<a href="#section-11-1-2">11.1.2 User 和 Relationship 模型之间的关联</a>
</li>
<li class="level-3">
<a href="#section-11-1-3">11.1.3 数据验证</a>
</li>
<li class="level-3">
<a href="#section-11-1-4">11.1.4 被关注的用户</a>
</li>
<li class="level-3">
<a href="#section-11-1-5">11.1.5 粉丝</a>
</li>
<li class="level-2">
<a href="#section-11-2">11.2 关注用户功能的网页界面</a>
</li>
<li class="level-3">
<a href="#section-11-2-1">11.2.1 用户关注用到的示例数据</a>
</li>
<li class="level-3">
<a href="#section-11-2-2">11.2.2 数量统计和关注表单</a>
</li>
<li class="level-3">
<a href="#section-11-2-3">11.2.3 关注列表和粉丝列表页面</a>
</li>
<li class="level-3">
<a href="#section-11-2-4">11.2.4 关注按钮的常规实现方式</a>
</li>
<li class="level-3">
<a href="#section-11-2-5">11.2.5 关注按钮的 Ajax 实现方式</a>
</li>
<li class="level-2">
<a href="#section-11-3">11.3 动态列表</a>
</li>
<li class="level-3">
<a href="#section-11-3-1">11.3.1 目的和策略</a>
</li>
<li class="level-3">
<a href="#section-11-3-2">11.3.2 初步实现动态列表</a>
</li>
<li class="level-3">
<a href="#section-11-3-3">11.3.3 子查询(subselect)</a>
</li>
<li class="level-3">
<a href="#section-11-3-4">11.3.4 新的动态列表</a>
</li>
<li class="level-2">
<a href="#section-11-4">11.4 小结</a>
</li>
<li class="level-3">
<a href="#section-11-4-1">11.4.1 扩展示例程序的功能</a>
</li>
<li class="level-3">
<a href="#section-11-4-2">11.4.2 后续学习的资源</a>
</li>
<li class="level-2">
<a href="#section-11-5">11.5 练习</a>
</li>
</ol>
<div class="main">
<p>在这一章中,我们会在现有程序的基础上增加社交功能,允许用户关注(follow)或取消关注其他人,并在用户主页上显示其所关注用户的微博更新。我们还会创建两个页面用来显示关注的用户列表和粉丝列表。我们将会在 <a href="chapter11.html#section-11-1">11.1 节</a>学习如何构建用户之间的模型关系,随后在 <a href="chapter11.html#section-11-2">11.2 节</a>设计网页界面,同时还会介绍 Ajax。最后,我们会在 <a href="chapter11.html#section-11-3">11.3 节</a>实现一个完整的动态列表。</p>
<p>这是本书最后一章,其中会包含了一些本教程中最具有挑战性的内容,为了实现动态列表,我们会使用一些 Ruby/SQL 小技巧。 通过这些例子,你会了解到 Rails 是如何处理更加复杂的数据模型的,而这些知识会在你日后开发其他应用时发挥作用。 为了帮助你平稳地从教程学习过渡到独立开发,在 <a href="chapter11.html#section-11-4">11.4 节</a>我们推荐了几个可以在已有微博核心基础上开发的额外功能,以及一些进阶资料的链接。</p>
<p>和之前章节一样,Git 用户应该创建一个新的分支:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git checkout -b following-users
</pre></div>
</div>
<p>因为本章的内容比较有挑战性,在开始编写代码之前,我们先来思考一下网站的界面。 和之前的章节一样,在开发的早期阶段我们会通过构思图来呈现页面。<sup class="footnote" id="fnref-11-1"><a href="#fn-11-1" rel="footnote">1</a></sup>完整的页面流程是这样的:一名用户 (John Calvin) 从他的个人资料页面(如图 11.1 )浏览到用户索引页面(如图 11.2),关注了一个用户。Calvin 打开另一个用户 Thomas Hobbes 的个人主页(如图 11.3),点击“Follow(关注)”按钮 关注该用户,然后,这个“Follow”按钮会变为“Unfollow(取消关注)” ,而且 Hobbes 的关注人数会增加 1 个(如图 11.4)。接着,Calvin 回到自己的主页,看到关注人数增加了 1 个,在动态列表中也能看到 Hobbes 的状态更新(如图 11.5)。接下来的整章内容就是要实现这样的页面流程。</p>
<h2 id='section-11-1'><span>11.1</span> 关系模型</h2>
<p>为了实现关注用户这一功能,第一步我们要做的是创建一个看上去并不是那么直观的数据模型。一开始我们可能会认为一个 <code>has_many</code> 的数据关系能满足我们的要求:一个用户可以关注多个用户,同时一个用户还能被多个用户关注。但实际上这种关系是存在问题的,下面我们就来学习如何使用 <code>has_many through</code> 来解决这个问题。本节很多功能的实现初看起来都有点难以理解,你需要花一点时间思考,才能真正搞清楚这样做的原因。如果在某个地方卡住了,尝试着先往后读,然后再把本节读一遍,看一下刚才卡住的地方想明白了没。</p>
<h3 id='section-11-1-1'><span>11.1.1</span> 数据模型带来的问题以及解决方式</h3>
<p>构造数据模型的第一步,我们先来看一个典型的情况。假如一个用户关注了另外一个用户,比如 Calvin 关注了 Hobbes,也就是 Hobbes 被 Calvin 关注了,那么 Calvin 就是关注者(follower),而 Hobbes 则是被关注者( followed )。按照 Rails 默认的复数表示习惯, 我们称关注某一特定用户的用户集合为该用户的 followers,<code>user.followers</code> 就是这些用户组成的数组。不过,当我们颠倒一下顺序,上述关系则不成立了:默认情况下,所有被关注的用户称为 followeds,这样说在英语语法上并不通顺恰当。我们可以称被关注者为 following,但这个词有些歧义:在英语里,”following” 指关注你的人,和我们想表达的恰恰相反。考虑到上述两种情况,尽管我们将使用“following”作为标签,如“50 following, 75 followers”, 但在数据模型中会使用“followed users”表示我们关注的用户集合,以及一个对应的 <code>user.followed_users</code> 数组。<sup class="footnote" id="fnref-11-2"><a href="#fn-11-2" rel="footnote">2</a></sup></p>
<div class="figure" id="figure-11-1">
<img src="figures/page_flow_profile_mockup_bootstrap.png" alt="page flow profile mockup bootstrap" />
<p class="caption"><span>图 11.1:</span>当前登入用户的个人资料页面</p>
</div>
<p>经过上述的讨论,我们会按照图 11.6 的方式构建被关注用户的模型,使用 <code>followed_users</code> 表实现一对多(<code>has_many</code>)关联。由于 <code>user.followed_users</code> 应该是一个用户对象组成的数组,所以 <code>followed_users</code> 表中的每一行应该对应一个用户,并且指定 <code>followed_id</code> 列,和其他用户建立关联。<sup class="footnote" id="fnref-11-3"><a href="#fn-11-3" rel="footnote">3</a></sup>除此之外,由于每一行均对应一个用户,所以我们还要在表中加入用户的其他属性,包括名字,密码等。</p>
<p>图 11.6 中描述的数据模型仍存在一个问题,那就是存在非常多的冗余,每一行不仅包括了所关注用户的 id, 还包括了他们的其他信息,而这些信息在 <code>users</code> 表中都有。 更糟糕的是,为了建立关注用户的数据模型,我们还需要一个单独的,同样冗余的 <code>followers</code> 表。这最终将导致数据模型极难维护,每当用户修改姓名时,我们不仅要修改用户在 <code>users</code> 表中的数据,还要修改 <code>followed_users</code> 和 <code>followers</code> 表中对应该用户的每一个记录。</p>
<div class="figure" id="figure-11-2">
<img src="figures/page_flow_user_index_mockup_bootstrap.png" alt="page flow user index mockup bootstrap" />
<p class="caption"><span>图 11.2:</span>寻找一个用户来关注</p>
</div>
<p>造成这个问题的主要原因是,我们缺少了一层抽象。 找到合适抽象的一个方法是,思考我们会如何在应用程序中实现关注用户的操作。在 <a href="chapter7.html#section-7-1-2">7.1.2 节</a>中我们介绍过,REST 架构涉及到创建资源和销毁资源两个过程。 由此引出两个问题: 当用户关注另一个用户时,创建了什么? 当用户取消关注另一个用户是,销毁了什么?</p>
<p>按照 REST 架构的思路再次思考之后,我们会发现,在关注用户的过程中,被创建和被销毁的是两个用户之间的“关系”。在这种“关系”中,一个用户有多个“关系”(<code>has_many :relationships</code>),并有很多关注的用户(<code>followed_users</code> 或 <code>followers</code>)。其实,在图 11.6 中我们已经基本实现了这种“关系”: 由于每一个被关注的用户都是由 <code>followed_id</code> 独一无二的标识出来的,我们就可以将 <code>followed_users</code> 表转化成 <code>relationships</code> 表,删掉用户的详细资料,使用 <code>followed_id</code> 从 <code>users</code> 表中获得被关注用户的数据。 同样的,这种“关系”反过来,我们可以使用 <code>follower_id</code> 获取所有粉丝组成的数组。</p>
<p>为了得到一个由所有被关注用户组成的 <code>followed_users</code> 数组,我们可以先获取由 <code>followed_id</code> 属性组成的数组,再查找每个用户。不过,如你所想,Rails 为我们提供了一种更简单的方式,那就是 <code>has_many through</code>。我们将在 <a href="chapter11.html#section-11-1-4">11.1.4 节</a>介绍, Rails 允许我们使用下面这行清晰简洁的代码,通过 <code>relationships</code> 表来描述一个用户关注了很多其他用户:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">has_many</span> <span class="p">:</span><span class="n">followed_users</span><span class="p">,</span> <span class="ss">through: :relationships</span><span class="p">,</span> <span class="ss">source: </span><span class="s2">"followed_id"</span>
</pre></div>
</div>
<p>这行代码会自动获取被关注用户组成的数组,也就是 <code>user.followed_users</code>。 图11.7 描述了这个数据模型。</p>
<div class="figure" id="figure-11-3">
<img src="figures/page_flow_other_profile_follow_button_mockup_bootstrap.png" alt="page flow other profile follow button mockup bootstrap" />
<p class="caption"><span>图 11.3:</span>一个想要关注的用户资料页面,显示有关注按钮</p>
</div>
<p>下面让我们动手实现,首先我们通过下面的命令创建 Relationship 模型:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate model Relationship follower_id:integer followed_id:integer
</pre></div>
</div>
<p>这个命令可能会生成 Relationship 预构件,你应该将其删除:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rm -f spec/factories/relationship.rb
</pre></div>
</div>
<p>由于我们需通过 <code>follower_id</code> 和 <code>followed_id</code> 来查找用户之间的关系,考虑到性能,要为这两列加上索引, 如代码 11.1 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-1"><p class="caption"><span>代码 11.1:</span>在 <code>relationships</code> 表中设置索引</p><p class="file"><code>db/migrate/[timestamp]_create_relationships.rb</code></p><div class="highlight type-ruby"><pre><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="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="p">:</span><span class="n">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="n">add_index</span> <span class="p">:</span><span class="n">relationships</span><span class="p">,</span> <span class="ss">:follower_id</span>
<span class="n">add_index</span> <span class="p">:</span><span class="n">relationships</span><span class="p">,</span> <span class="ss">:followed_id</span>
<span class="n">add_index</span> <span class="p">:</span><span class="n">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 class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="figure" id="figure-11-4">
<img src="figures/page_flow_other_profile_unfollow_button_mockup_bootstrap.png" alt="page flow other profile unfollow button mockup bootstrap" />
<p class="caption"><span>图 11.4:</span>关注按钮变为取消关注的同时,关注人数增加了 1 个</p>
</div>
<p>在代码 11.1 中,我们还设置了一个组合索引(composite index),其目的是确保 (<code>follower_id, followed_id</code>) 组合是唯一的,这样用户就无法多次关注同一个用户了 (可以和代码 6.19 中为保持 Email 地址唯一的 index 做比较一下):</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">add_index</span> <span class="p">:</span><span class="n">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>
</pre></div>
</div>
<p>从 <a href="chapter11.html#section-11-1-4">11.1.4 节</a>开始,我们会发现,在用户界面中这样的事情是不会发生的,但是添加了组合索引后,如果用户试图二次关注时,程序会抛出异常(例如,使用像 <code>curl</code> 这样的命令行程序)。我们也可以在 Relationship 模型中添加唯一性数据验证,但因为每次尝试创建一个重复关系时都会触发错误,所以这个组合索引现在已经能满足我们的需求了。</p>
<div class="figure" id="figure-11-5">
<img src="figures/page_flow_home_page_feed_mockup_bootstrap.png" alt="page flow home page feed mockup bootstrap" />
<p class="caption"><span>图 11.5:</span>个人主页出现了新关注用户的微博,关注人数增加了 1 个</p>
</div>
<div class="figure" id="figure-11-6">
<img src="figures/naive_user_has_many_followed_users.png" alt="naive user has many followed users" />
<p class="caption"><span>图 11.6:</span>一个简单的用户互相关注实现</p>
</div>
<div class="figure" id="figure-11-7">
<img src="figures/user_has_many_followed_users.png" alt="user has many followed users" />
<p class="caption"><span>图 11.7:</span>通过 <code>relationships</code> 表建立的被关注用户数据模型</p>
</div>
<p>为了创建 <code>relationships</code> 表,和之前一样,我们要先执行数据库迁移,再准备好“测试数据库”:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rake db:migrate
<span class="gp">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:prepare
</pre></div>
</div>
<p>得到的 Relationship 数据模型如图 11.8 所示。</p>
<div class="figure" id="figure-11-8">
<img src="figures/relationship_model.png" alt="relationship model" />
<p class="caption"><span>图 11.8:</span>Relationship 数据模型</p>
</div>
<h3 id='section-11-1-2'><span>11.1.2</span> User 和 Relationship 模型之间的关联</h3>
<p>在着手实现已关注用户和关注者之前,我们先要建立 User 和 Relationship 模型之间的关联关系。一个用户可以有多个“关系”(<code>has_many</code> relationships), 因此一个“关系”涉及到两个用户,所以这个“关系”就同时属于(<code>belongs_to</code>)该用户和被关注者。</p>
<p>和 <a href="chapter10.html#section-10-1-3">10.1.3 节</a>创建微博一样,我们将通过 User 和 Relationship 模型之间的关联来创建这个“关系”,使用如下的代码实现:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">user</span><span class="p">.</span><span class="nf">relationships</span><span class="p">.</span><span class="nf">build</span><span class="p">(</span><span class="ss">followed_id: </span><span class="p">.</span><span class="nf">.</span><span class="o">.</span><span class="p">)</span>
</pre></div>
</div>
<p>首先,我们来编写测试,如代码 11.2 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-2"><p class="caption"><span>代码 11.2:</span>测试建立“关系”以及属性的可访问性</p><p class="file"><code>spec/models/relationship_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="no">Relationship</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:follower</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:followed</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:relationship</span><span class="p">)</span> <span class="p">{</span> <span class="n">follower</span><span class="p">.</span><span class="nf">relationships</span><span class="p">.</span><span class="nf">build</span><span class="p">(</span><span class="ss">followed_id: </span><span class="n">followed</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span> <span class="p">}</span>
<span class="n">subject</span> <span class="p">{</span> <span class="n">relationship</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">be_valid</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p>这里需要注意,与测试 User 和 Micropost 模型时使用 <code>@user</code> 和 <code>@micropost</code> 不同,代码 11.2 中使用 <code>let</code> 代替了实例变量。这两种方式之间几乎没有差别<sup class="footnote" id="fnref-11-4"><a href="#fn-11-4" rel="footnote">4</a></sup>,但我认为使用 <code>let</code> 相对于使用实例变量更易懂。测试 User 和 Micropost 时之所以使用实例变量,是希望读者早些接触这个重要的概念,而 <code>let</code> 则略显高深,所以我们放在这里才用。</p>
<p>同时,在 User 模型中我们还要测试用户对象是否可以响应 <code>relationships</code> 方法,如代码 11.3 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-3"><p class="caption"><span>代码 11.3:</span>测试 <code>user.relationships</code></p><p class="file"><code>spec/models/user_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="no">User</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:feed</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:relationships</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>此时,你可能会想在程序中中加入类似于 <a href="chapter10.html#section-10-1-3">10.1.3 节</a>中用到的代码,我们要添加的代码确实很像,但二者之间有一处很不一样:在 Micropost 模型中, 我们使用</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">Micropost</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">user</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>和</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">microposts</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>因为 <code>microposts</code> 表中存有 <code>user_id</code> 属性,可以标示用户(参见 <a href="chapter10.html#section-10-1-1">10.1.1 节</a>)。这种连接两个数据表的 id,我们称之为外键(foreign key),当指向 User 模型的外键为 <code>user_id</code> 时,Rails 就会自动的获知关联关系,因为默认情况下,Rails 会寻找 <code><class>_id</code> 形式的外键,其中 <code><class></code> 是模型类名的小写形式。<sup class="footnote" id="fnref-11-5"><a href="#fn-11-5" rel="footnote">5</a></sup>现在,尽管我们处理的还是用户,但外键是 <code>follower_id</code> 了,所以我们要告诉 Rails 这一变化,如代码 11.4 所示。<sup class="footnote" id="fnref-11-6"><a href="#fn-11-6" rel="footnote">6</a></sup></p>
<div class="codeblock has-caption" id="codeblock-11-4"><p class="caption"><span>代码 11.4:</span>实现 User 和 Relationship 模型之间 <code>has_many</code> 的关联关系</p><p class="file"><code>app/models/user.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">microposts</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">relationships</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="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>(由于删除用户后,也应该删除该用户的所有“关系”, 于是我们指定了 <code>dependent: :destroy</code> 参数;针对删除效果的测试会留作练习,参见 <a href="chapter11.html#section-11-5">11.5 节</a>。)</p>
<p>和 Micropost 模型一样,Relationship 模型和 User 模型之间也有一层 <code>belongs_to</code> 关系,此时,这种关系同时属于关注者和被关注者,针对这层“关系”的测试如代码 11.5 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-5"><p class="caption"><span>代码 11.5:</span>测试 User 和 Relationship 模型之间的 <code>belongs_to</code> 关系</p><p class="file"><code>spec/models/relationship_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="n">describe</span> <span class="no">Relationship</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"follower methods"</span> <span class="k">do</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:follower</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:followed</span><span class="p">)</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:follower</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="n">eq</span> <span class="n">follower</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:followed</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="n">eq</span> <span class="n">followed</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>下面我们开始写程序的代码,<code>belongs_to</code> 关系的建立和之前一样。Rails 会通过 Symbol 获知外键的名字(例如,<code>:follower</code> 对应的外键是 <code>follower_id</code>,<code>:followed</code> 对应的外键是 <code>followed_id</code>),但 Followed 或 Follower 模型是不存在的,因此这里就要使用 <code>User</code> 这个类名, 如代码 11.6 所示。注意,与默认生成的 Relationship 模型不同,这里只有 <code>followed_id</code> 是可以访问的。</p>
<div class="codeblock has-caption" id="codeblock-11-6"><p class="caption"><span>代码 11.6:</span>为 Relationship 模型添加 <code>belongs_to</code> 关系</p><p class="file"><code>app/models/relationship.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">Relationship</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">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="p">:</span><span class="n">followed</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"User"</span>
<span class="k">end</span>
</pre></div>
</div>
<p>尽管直到 <a href="chapter11.html#section-11-1-5">11.1.5 节</a>我们才会用到 <code>followed</code> 关联,但同时实现 follower 和 followed 关联会更容易理解。</p>
<p>此时,代码 11.2 和代码 11.3 中的测试应该可以通过了。</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h3 id='section-11-1-3'><span>11.1.3</span> 数据验证</h3>
<p>在结束这部分之前,我们将添加一些针对 Relationship 模型的数据验证,确保代码的完整性。测试(代码 11.7)和程序代码(代码 11.8)都非常易懂。</p>
<div class="codeblock has-caption" id="codeblock-11-7"><p class="caption"><span>代码 11.7:</span>测试 Relationship 模型的数据验证</p><p class="file"><code>spec/models/relationship_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="n">describe</span> <span class="no">Relationship</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"when followed id is not present"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">relationship</span><span class="p">.</span><span class="nf">followed_id</span> <span class="o">=</span> <span class="kp">nil</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should_not</span> <span class="n">be_valid</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"when follower id is not present"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">relationship</span><span class="p">.</span><span class="nf">follower_id</span> <span class="o">=</span> <span class="kp">nil</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should_not</span> <span class="n">be_valid</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-11-8"><p class="caption"><span>代码 11.8:</span>添加 Relationship 模型数据验证</p><p class="file"><code>app/models/relationship.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">Relationship</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">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="p">:</span><span class="n">followed</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"User"</span>
<span class="n">validates</span> <span class="p">:</span><span class="n">follower_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="n">validates</span> <span class="p">:</span><span class="n">followed_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="k">end</span>
</pre></div>
</div>
<h3 id='section-11-1-4'><span>11.1.4</span> 被关注的用户</h3>
<p>下面到了 Relationship 关联关系的核心部分了,获取 <code>followed_users</code> 和 <code>followers</code>。 我们首先从 <code>followed_users</code> 开始,测试如代码 11.9 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-9"><p class="caption"><span>代码 11.9:</span>测试 <code>user.followed_users</code> 属性</p><p class="file"><code>spec/models/user_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec helper'</span>
<span class="n">describe</span> <span class="no">User</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:relationships</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:followed_users</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>实现的代码会第一次使用 <code>has_many through</code>:用户通过 <code>relationships</code> 表拥有多个关注关系,就像图 11.7 所示的那样。默认情况下,在 <code>has_many through</code> 关联中,Rails 会寻找关联名单数形式对应的外键,也就是说,像下面的代码</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">has_many</span> <span class="p">:</span><span class="n">followeds</span><span class="p">,</span> <span class="ss">through: :relationships</span>
</pre></div>
</div>
<p>会使用 <code>relationships</code> 表中的 <code>followed_id</code> 列生成一个数组。但是,正如在 <a href="chapter11.html#section-11-1-1">11.1.1 节</a>中说过的,<code>user.followeds</code> 这种说法比较蹩脚,若使用“followed users”作为 “followed”的复数形式会好得多,那么被关注的用户数组就要写成 <code>user.followed_users</code> 了。Rails 当然会允许我们重写默认的设置,针对本例,我们可以使用 <code>:source</code> 参数(参见代码 11.10),告知 Rails <code>followed_users</code> 数组的来源是 <code>followed</code> 所代表的 id 集合。</p>
<div class="codeblock has-caption" id="codeblock-11-10"><p class="caption"><span>代码 11.10:</span>在 User 模型中添加 <code>followed_users</code> 关联</p><p class="file"><code>app/models/user.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">microposts</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">relationships</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="n">has_many</span> <span class="p">:</span><span class="n">followed_users</span><span class="p">,</span> <span class="ss">through: :relationships</span><span class="p">,</span> <span class="ss">source: :followed</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>为了创建关注关联关系,我们将定义一个名为 <code>follow!</code> 的方法,这样我们就能使用 <code>user.follow!(other_user)</code> 这样的代码创建关注了。(<code>follow!</code> 方法应该与 <code>create!</code> 和 <code>save!</code> 方法一样,失败时抛出异常,所以我们在后面加上了感叹号。)对应地,我们还会添加一个 <code>following?</code> 布尔值方法,检查一个用户是否关注了另一个用户。<sup class="footnote" id="fnref-11-7"><a href="#fn-11-7" rel="footnote">7</a></sup>代码 11.11 中的测试表明了我们希望如何使用这两个方法。</p>
<div class="codeblock has-caption" id="codeblock-11-11"><p class="caption"><span>代码 11.11:</span>测试关注关系用到的方法</p><p class="file"><code>spec/models/user_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="no">User</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:followed_users</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:following?</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:follow!</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"following"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:other_user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="k">do</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">save</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">follow!</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">be_following</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:followed_users</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="kp">include</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>在实现的代码中,<code>following</code> 方法接受一个用户对象作为参数,参数名为 <code>other_user</code>,检查这个被关注者的 id 在数据库中是否存在;<code>follow!</code> 方法直接调用 <code>create!</code> 方法,通过和 Relationship 模型的关联来创建关注关系,如代码 11.12 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-12"><p class="caption"><span>代码 11.12:</span>定义 <code>following?</code> 和 <code>follow!</code> 方法</p><p class="file"><code>app/models/user.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</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="k">def</span> <span class="nf">following?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="n">relationships</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">followed_id: </span><span class="n">other_user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</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="n">relationships</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">followed_id: </span><span class="n">other_user</span><span class="p">.</span><span class="nf">id</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">end</span>
</pre></div>
</div>
<p>注意,在代码 11.12 中我们忽略了用户对象自身,直接写成</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">relationships</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="err">…</span><span class="p">)</span>
</pre></div>
</div>
<p>而不是等效的</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="nb">self</span><span class="p">.</span><span class="nf">relationships</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="err">…</span><span class="p">)</span>
</pre></div>
</div>
<p>是否使用 <code>self</code> 关键字只是个人偏好而已。</p>
<p>当然,用户应该既能关注也能取消关注,那么还应该有一个 <code>unfollow!</code> 方法,如代码 11.13 所示。<sup class="footnote" id="fnref-11-8"><a href="#fn-11-8" rel="footnote">8</a></sup></p>
<div class="codeblock has-caption" id="codeblock-11-13"><p class="caption"><span>代码 11.13:</span>测试取消关注用户</p><p class="file"><code>spec/models/user_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="no">User</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:follow!</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:unfollow!</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"following"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"and unfollowing"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">unfollow!</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should_not</span> <span class="n">be_following</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:followed_users</span><span class="p">)</span> <span class="p">{</span> <span class="n">should_not</span> <span class="kp">include</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p><code>unfollow!</code> 方法的定义很容易理解,通过 <code>followed_id</code> 找到对应的“关系”删除就行了,如代码 11.14 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-14"><p class="caption"><span>代码 11.14:</span>删除“关系”取消关注用户</p><p class="file"><code>app/models/user.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</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="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="n">relationships</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">followed_id: </span><span class="n">other_user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</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="n">relationships</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">followed_id: </span><span class="n">other_user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</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="n">relationships</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">followed_id: </span><span class="n">other_user</span><span class="p">.</span><span class="nf">id</span><span class="p">).</span><span class="nf">destroy</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<h3 id='section-11-1-5'><span>11.1.5</span> 粉丝</h3>
<p>关注关系的最后一部分是定义和 <code>user.followed_users</code> 相对应的 <code>user.followers</code> 方法。从图 11.7 你或许发现了,获取粉丝数组所需的数据都已经存入 <code>relationships</code> 表中了。这里我们用到的方法和实现被关注者时一样,只要对调 <code>follower_id</code> 和 <code>followed_id</code> 的位置即可。这说明, 只要我们对调这两列的位置,组建成 <code>reverse_relationships</code> 表(如图 11.9 所示),<code>user.followers</code> 方法的定义就很容易了。</p>
<div class="figure" id="figure-11-9">
<img src="figures/user_has_many_followers_2nd_ed.png" alt="user has many followers 2nd ed" />
<p class="caption"><span>图 11.9:</span>使用倒转后的 Relationship 模型获取粉丝</p>
</div>
<p>我们先来编写测试,相信神奇的 Rails 将再一次显现威力,如代码 11.15 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-15"><p class="caption"><span>代码 11.15:</span>测试对调后的关注关系</p><p class="file"><code>spec/models/user_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="no">User</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:relationships</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:followed_users</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:reverse_relationships</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">respond_to</span><span class="p">(</span><span class="ss">:followers</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"following"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">be_following</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:followed_users</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="kp">include</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"followed user"</span> <span class="k">do</span>
<span class="n">subject</span> <span class="p">{</span> <span class="n">other_user</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:followers</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="kp">include</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</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">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意一下上述代码中,我们是如何使用 <code>subject</code> 来转变测试对象的,我们从 <code>@user</code> 转到了 <code>other_user</code>,然后,我们就能使用下面下面这种很自然的方式测试粉丝中是否包含 <code>@user</code> 了:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">subject</span> <span class="p">{</span> <span class="n">other_user</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:followers</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="kp">include</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="p">}</span>
</pre></div>
</div>
<p>你可能已经想到了,我们不会再建立一个完整的数据表来存放倒转后的关注关系。事实上,我们会通过被关注者和粉丝之间的对称关系来模拟一个 <code>reverse_relationships</code> 表,主键设为 <code>followed_id</code>。也就是说,<code>relationships</code> 表使用 <code>follower_id</code> 做外键:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">has_many</span> <span class="p">:</span><span class="n">relationships</span><span class="p">,</span> <span class="ss">foreign_key: </span><span class="s2">"follower_id"</span>
</pre></div>
</div>
<p>那么,<code>reverse_relationships</code> 虚拟表就用 <code>followed_id</code> 做外键:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">has_many</span> <span class="p">:</span><span class="n">reverse_relationships</span><span class="p">,</span> <span class="ss">foreign_key: </span><span class="s2">"followed_id"</span>
</pre></div>
</div>
<p>粉丝的关联就建立在这层反转的关系上,如代码 11.16 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-16"><p class="caption"><span>代码 11.16:</span>通过反转的关系实现 <code>user.followers</code></p><p class="file"><code>app/models/user.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">has_many</span> <span class="ss">:reverse_relationships</span><span class="p">,</span> <span class="ss">foreign_key: </span><span class="s2">"followed_id"</span><span class="p">,</span>
<span class="ss">class_name: </span><span class="s2">"Relationship"</span><span class="p">,</span>
<span class="ss">dependent: :destroy</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">followers</span><span class="p">,</span> <span class="ss">through: :reverse_relationships</span><span class="p">,</span> <span class="ss">source: :follower</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>(和代码 11.4 一样,针对 <code>dependent :destroy</code> 的测试会留作练习,参见 <a href="chapter11.html#section-11-1-5">11.5 节</a>。) 注意为了实现数据表之间的关联,我们要指定类名:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">has_many</span> <span class="p">:</span><span class="n">reverse_relationships</span><span class="p">,</span> <span class="ss">foreign_key: </span><span class="s2">"followed_id"</span><span class="p">,</span>
<span class="ss">class_name: </span><span class="s2">"Relationship"</span>
</pre></div>
</div>
<p>如果没有指定类名,Rails 会尝试寻找 <code>ReverseRelationship</code> 类,而这个类并不存在。</p>
<p>还有一点值得注意一下,在里我们其实可以省略 <code>:source</code> 参数,使用下面的简单方式</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">has_many</span> <span class="p">:</span><span class="n">followers</span><span class="p">,</span> <span class="ss">through: :reverse_relationships</span>
</pre></div>
</div>
<p>对 <code>:followers</code> 属性而言,Rails 会把“followers”转成单数形式,自动寻找名为 <code>follower_id</code> 的外键。在此我保留了 <code>:source</code> 参数是为了保持与 <code>has_many :followed_users</code> 关系之间结构上的对称,你也可以选择去掉它。</p>
<p>加入代码 11.16 之后,关注者和粉丝之间的关联就完成了,所有的测试应该都可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h2 id='section-11-2'><span>11.2</span> 关注用户功能的网页界面</h2>
<p>在本章的导言中,我们介绍了关注用户功能的操作流程。本节我们会实现这些构思的基本界面,以及关注和取消关注操作。同时,我们还会创建两个页面,分别列出关注的用户和粉丝。在 <a href="chapter11.html#section-11-3">11.3 节</a>中我们会加入用户的动态列表,其时,这个示例程序才算完成。</p>
<h3 id='section-11-2-1'><span>11.2.1</span> 用户关注用到的示例数据</h3>
<p>和之前的几章一样,我们会使用 Rake 任务生成示例数据,向数据库中存入临时的用户关注关联数据。有了这些示例数据,我们就可以先开发网页,而把后端功能的实现放在本节的最后。</p>
<p>我们在代码 10.20 中用到的示例数据生成器有点乱,所以现在我们要分别定义两个方法,用来生成用户和微博示例数据,然后再定义 <code>make_relationships</code> 方法,生成用户关注关联数据,如代码 11.17 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-17"><p class="caption"><span>代码 11.17:</span>加入用户关注关联示例数据</p><p class="file"><code>lib/tasks/sample_data.rake</code></p><div class="highlight type-ruby"><pre><span class="n">namespace</span> <span class="p">:</span><span class="n">db</span> <span class="k">do</span>
<span class="n">desc</span> <span class="s2">"Fill database with sample data"</span>
<span class="n">task</span> <span class="ss">populate: :environment</span> <span class="k">do</span>
<span class="n">make_users</span>
<span class="n">make_microposts</span>
<span class="n">make_relationships</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">make_users</span>
<span class="n">admin</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span>
<span class="ss">email: </span><span class="s2">"example@railstutorial.org"</span><span class="p">,</span>
<span class="ss">password: </span><span class="s2">"foobar"</span><span class="p">,</span>
<span class="ss">password_confirmation: </span><span class="s2">"foobar"</span><span class="p">,</span>
<span class="ss">admin: </span><span class="kp">true</span><span class="p">)</span>
<span class="mi">99</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
<span class="nb">name</span> <span class="o">=</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">name</span>
<span class="n">email</span> <span class="o">=</span> <span class="s2">"example-</span><span class="si">#{</span><span class="n">n</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s2">@railstutorial.org"</span>
<span class="n">password</span> <span class="o">=</span> <span class="s2">"password"</span>
<span class="no">User</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">name: </span><span class="nb">name</span><span class="p">,</span>
<span class="ss">email: </span><span class="n">email</span><span class="p">,</span>
<span class="ss">password: </span><span class="n">password</span><span class="p">,</span>
<span class="ss">password_confirmation: </span><span class="n">password</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">make_microposts</span>
<span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span><span class="p">(</span><span class="ss">limit: </span><span class="mi">6</span><span class="p">)</span>
<span class="mi">50</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
<span class="n">content</span> <span class="o">=</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Lorem</span><span class="p">.</span><span class="nf">sentence</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
<span class="n">users</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">user</span><span class="o">|</span> <span class="n">user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">content: </span><span class="n">content</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">make_relationships</span>
<span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">.</span><span class="nf">first</span>
<span class="n">followed_users</span> <span class="o">=</span> <span class="n">users</span><span class="p">[</span><span class="mi">2</span><span class="p">.</span><span class="nf">.</span><span class="mi">50</span><span class="p">]</span>
<span class="n">followers</span> <span class="o">=</span> <span class="n">users</span><span class="p">[</span><span class="mi">3</span><span class="p">.</span><span class="nf">.</span><span class="mi">40</span><span class="p">]</span>
<span class="n">followed_users</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">followed</span><span class="o">|</span> <span class="n">user</span><span class="p">.</span><span class="nf">follow!</span><span class="p">(</span><span class="n">followed</span><span class="p">)</span> <span class="p">}</span>
<span class="n">followers</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">follower</span><span class="o">|</span> <span class="n">follower</span><span class="p">.</span><span class="nf">follow!</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p>用户关注关联的示例数据是由下面的代码生成的:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">def</span> <span class="nf">make_relationships</span>
<span class="n">users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">users</span><span class="p">.</span><span class="nf">first</span>
<span class="n">followed_users</span> <span class="o">=</span> <span class="n">users</span><span class="p">[</span><span class="mi">2</span><span class="p">.</span><span class="nf">.</span><span class="mi">50</span><span class="p">]</span>
<span class="n">followers</span> <span class="o">=</span> <span class="n">users</span><span class="p">[</span><span class="mi">3</span><span class="p">.</span><span class="nf">.</span><span class="mi">40</span><span class="p">]</span>
<span class="n">followed_users</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">followed</span><span class="o">|</span> <span class="n">user</span><span class="p">.</span><span class="nf">follow!</span><span class="p">(</span><span class="n">followed</span><span class="p">)</span> <span class="p">}</span>
<span class="n">followers</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">follower</span><span class="o">|</span> <span class="n">follower</span><span class="p">.</span><span class="nf">follow!</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p>我们的安排是随机的,让第 1 个用户关注第 3 到第 51 个用户,再让第 4 到第 41 个用户关注第 1 个用户。形成了这样的用户关注网,就足够用来开发程序的界面了。</p>
<p>和之前一样,要想运行代码 11.17,就要执行下面的数据库命令:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rake db:reset
<span class="gp">$ </span>bundle <span class="nb">exec </span>rake db:populate
<span class="gp">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:prepare
</pre></div>
</div>
<h3 id='section-11-2-2'><span>11.2.2</span> 数量统计和关注表单</h3>
<p>现在用户已经有关注的人和粉丝了,我们要更新一下用户资料页面和首页,把这些变动显示出来。首先,我们要创建一个关注和取消关注的表单,然后再创建显示被关注用户列表和粉丝列表的页面。</p>
<p>我们在 <a href="chapter11.html#section-11-1-1">11.1.1 节</a>中说过,“following”这个词作为属性名是有点奇怪的(因为 <code>user.following</code> 既可以理解为被关注的用户,也可以理解为关注的用户),但可以作为标签使用,例如,可以说“50 following”。其实,Twitter 就使用了这种标签形式,图 11.1 中的构思图沿用了这种表述,这部分详细的构思如图 11.10 所示。</p>
<div class="figure" id="figure-11-10">
<img src="figures/stats_partial_mockup.png" alt="stats_partial_mockup" />
<p class="caption"><span>图 11.10:</span>数量统计局部视图的构思图</p>
</div>
<p>图 11.10 中显示的数量统计包含了当前用户关注的用户和关注了该用户的粉丝数,二者又分别链接到了各自详细的用户列表页面。在<a href="chapter5.html">第 5 章</a>中,我们使用 <code>#</code> 占位符代替真实的网址,因为那时我们还没怎么接触路由。现在,虽然在 <a href="chapter11.html#section-11-2-3">11.2.3 节</a>中才会创建所需的页面,不过我们可以先设置路由,如代码 11.18 所示。这段代码在 <code>resources</code> 块中使用了 <code>:member</code> 方法,以前没用过,你可以猜测一下这个方法的作用是什么。(注意,代码 11.18 是用来替换原来的 <code>resources :users</code> 的。)</p>
<div class="codeblock has-caption" id="codeblock-11-18"><p class="caption"><span>代码 11.18:</span>把 <code>following</code> 和 <code>folloers</code> 动作加入 Users 控制器的路由中</p><p class="file"><code>config/routes.rb</code></p><div class="highlight type-ruby"><pre><span class="no">SampleApp</span><span class="o">::</span><span class="no">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">resources</span> <span class="p">:</span><span class="n">users</span> <span class="k">do</span>
<span class="n">member</span> <span class="k">do</span>
<span class="n">get</span> <span class="p">:</span><span class="n">following</span><span class="p">,</span> <span class="ss">:followers</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>你可能猜到了,设定上述路由后,得到的 URL 地址应该是类似 /users/1/following 和 /users/1/followers 这种形式,不错,代码 11.18 的作用确实如此。因为这两个页面都是用来<strong>显示</strong>数据的,所以我们使用了 <code>get</code> 方法,指定这两个地址响应的是 GET 请求。(符合 REST 架构对这种页面的要求)。路由设置中使用的 <code>member</code> 方法作用是,设置这两个动作对应的 URL 地址中应该包含用户的 id。类似地,我们还可以使用 <code>collection</code> 方法,但 URL 中就没有用户 id 了,所以,如下的代码</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">resources</span> <span class="p">:</span><span class="n">users</span> <span class="k">do</span>
<span class="n">collection</span> <span class="k">do</span>
<span class="n">get</span> <span class="p">:</span><span class="n">tigers</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>设定路由后得到的 URL 是 /users/tigers(可以用来显示程序中所有的老虎)。关于路由的这种设置,更详细的说明可以阅读 Rails 指南中的《<a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>》一文。代码 11.18 所生成的路由如<a href="chapter11.html#table-11-1">表格 11.1</a> 所示。请留意一下被关注用户和粉丝页面的具名路由是什么,稍后我们会用到。为了避免用词混淆,在“following”路由中我们没有使用“followed users”这种说法,而沿用了 Twitter 的方式,采用“following”这个词,因为“followed users” 这种用法会生成 <code>followed_users_user_path</code> 这种奇怪的具名路由。如<a href="chapter11.html#table-11-1">表格 11.1</a> 所示,我们选择使用“following”这个词,因此得到的具名路由是 <code>following_user_path</code>。</p>
<div class="table has-caption" id="table-11-1"><p class="caption"><span>表格 11.1:</span>代码 11.18 中设置的路由生成的 REST 路由</p><table>
<thead>
<tr>
<th>HTTP 请求</th>
<th>URL</th>
<th>动作</th>
<th>具名路由</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td>/users/1/following</td>
<td><code>following</code></td>
<td><code>following_user_path(1)</code></td>
</tr>
<tr>
<td>GET</td>
<td>/users/1/followers</td>
<td><code>followers</code></td>
<td><code>followers_user_path(1)</code></td>
</tr>
</tbody>
</table>
</div>
<p>设好了路由后,我们来编写对数量统计局部视图的测试。(原本我们可以先写测试的,但是如果没加入所需的路由设置,可能无从下手编写测试。)数量统计局部视图会出现在用户资料页面和首页中,代码 11.19 只对首页进行了测试。</p>
<div class="codeblock has-caption" id="codeblock-11-19"><p class="caption"><span>代码 11.19:</span>测试首页中显示的关注和粉丝数量统计</p><p class="file"><code>spec/requests/static_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Static pages"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"Home page"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"for signed-in users"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="k">do</span>
<span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:micropost</span><span class="p">,</span> <span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">content: </span><span class="s2">"Lorem"</span><span class="p">)</span>
<span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:micropost</span><span class="p">,</span> <span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">content: </span><span class="s2">"Ipsum"</span><span class="p">)</span>
<span class="n">sign_in</span> <span class="n">user</span>
<span class="n">visit</span> <span class="n">root_path</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"should render the user's feed"</span> <span class="k">do</span>
<span class="n">user</span><span class="p">.</span><span class="nf">feed</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_selector</span><span class="p">(</span><span class="s2">"li#</span><span class="si">#{</span><span class="n">item</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="ss">text: </span><span class="n">item</span><span class="p">.</span><span class="nf">content</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"follower/following counts"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:other_user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="k">do</span>
<span class="n">other_user</span><span class="p">.</span><span class="nf">follow!</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">visit</span> <span class="n">root_path</span>
<span class="k">end</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s2">"0 following"</span><span class="p">,</span> <span class="ss">href: </span><span class="n">following_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s2">"1 followers"</span><span class="p">,</span> <span class="ss">href: </span><span class="n">followers_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>上述测试的核心是,检测页面中是否显示了关注和粉丝数,以及是否指向了正确的地址:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s2">"0 following"</span><span class="p">,</span> <span class="ss">href: </span><span class="n">following_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s2">"1 followers"</span><span class="p">,</span> <span class="ss">href: </span><span class="n">followers_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
</pre></div>
</div>
<p>我们用到了<a href="chapter11.html#table-11-1">表格 11.1</a>中的具名路由来测试链接是否指向了正确的地址。还有一处要注意一下,这里的“followers”是作为标签使用的,所以我们会一直用复数形式,即使只有一个粉丝也是如此。</p>
<p>数量统计局部视图的代码很简单,在一个 <code>div</code> 元素中显示几个链接就行了,如代码 11.20 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-20"><p class="caption"><span>代码 11.20:</span>显示关注数量统计的局部视图</p><p class="file"><code>app/views/shared/_stats.html.erb</code></p><div class="highlight type-erb"><pre><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">followed_users</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>
</pre></div>
</div>
<p>因为这个局部视图会同时在用户资料页面和首页中显示,所以在代码 11.20 的第一行中,我们要获取正确的用户对象:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="vi">@user</span> <span class="o">||=</span> <span class="n">current_user</span> <span class="cp">%></span>
</pre></div>
</div>
<p>我们在<a href="chapter8.html#aside-8-2">旁注 8.2</a>中介绍过这样的用法,如果 <code>@user</code> 不是 <code>nil</code>(在用户资料页面),这行代码就没什么效果,而如果是 <code>nil</code>(在首页), 就会把当前用户对象赋值给 <code>@user</code>。</p>
<p>还有一处也要注意一下,关注数量和粉丝数量是通过关联获取的,分别使用 <code>@user.followed_users.count</code> 和 <code>@user.followers.count</code>。</p>
<p>我们可以和代码 10.17 中获取微博数量的代码对比一下,微博的数量是通过 <code>@user.microposts.count</code> 获取的。</p>
<p>最后还有一些细节需要注意下,那就是某些元素的 CSS id,例如</p>
<div class="codeblock"><div class="highlight type-html"><pre><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>
</pre></div>
</div>
<p>这些 id 是为 <a href="chapter11.html#section-11-2-5">11.2.5 节</a>中实现 Ajax 功能服务的,Ajax 会通过独一无二的 id 获取页面中的元素。</p>
<p>编好了局部视图,把它放入首页中就很简单了,如代码 11.21 所示。(加入局部视图后,代码 11.19 中的测试也就会通过了。)</p>
<div class="codeblock has-caption" id="codeblock-11-21"><p class="caption"><span>代码 11.21:</span>在首页中显示的关注和粉丝数量统计</p><p class="file"><code>app/views/static_pages/home.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="k">if</span> <span class="n">signed_in?</span> <span class="cp">%></span>
.
.
.
<span class="nt"><section></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="nt"><section></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</span> <span class="cp">%></span>
<span class="nt"></section></span>
<span class="nt"><section></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="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
.
.
.
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</pre></div>
</div>
<p>我们会添加一些 SCSS 代码来美化一下数量统计部分,如代码 11.22 所示(这段代码包含了本章用到的所有样式)。添加样式后的页面如图 11.11 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-22"><p class="caption"><span>代码 11.22:</span>首页侧边栏的 SCSS 样式</p><p class="file"><code>app/assets/stylesheets/custom.css.scss</code></p><div class="highlight type-scss"><pre><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">.stats</span> <span class="p">{</span>
<span class="nl">overflow</span><span class="p">:</span> <span class="nb">auto</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">$grayLighter</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="nv">$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="nc">.user_avatars</span> <span class="p">{</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="p">}</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
</pre></div>
</div>
<div class="figure" id="figure-11-11">
<img src="figures/home_page_follow_stats_bootstrap.png" alt="home page follow stats bootstrap" />
<p class="caption"><span>图 11.11:</span>显示了关注数量统计的首页</p>
</div>
<p>稍后我们再把数量统计局部视图加入用户资料页面,现在先来编写关注和取消关注按钮的局部视图,如代码 11.23 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-23"><p class="caption"><span>代码 11.23:</span>关注和取消关注表单</p><p class="file"><code>app/views/users/_follow_form.html.erb</code></p><div class="highlight type-erb"><pre><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>
</pre></div>
</div>
<p>这段代码其实也没做什么,只是把具体的工作分配给 <code>follow</code> 和 <code>unfollow</code> 局部视图了,这样我们就要再次设置路由,加入 Relationships 资源,参照 Microposts 资源的设置(参见代码 10.25),如代码 11.24 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-24"><p class="caption"><span>代码 11.24:</span>添加 Relationships 资源的路由设置</p><p class="file"><code>config/routes.rb</code></p><div class="highlight type-ruby"><pre><span class="no">SampleApp</span><span class="o">::</span><span class="no">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="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">resources</span> <span class="ss">:sessions</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">:destroy</span><span class="p">]</span>
<span class="n">resources</span> <span class="p">:</span><span class="n">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="n">resources</span> <span class="p">:</span><span class="n">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 class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p><code>follow</code> 和 <code>unfollow</code> 局部视图的代码分别如代码 11.25 和代码 11.26 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-25"><p class="caption"><span>代码 11.25:</span>关注用户的表单</p><p class="file"><code>app/views/users/_follow.html.erb</code></p><div class="highlight type-erb"><pre><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">relationships</span><span class="p">.</span><span class="nf">build</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="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">f</span><span class="p">.</span><span class="nf">hidden_field</span> <span class="ss">:followed_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-large btn-primary"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-11-26"><p class="caption"><span>代码 11.26:</span>取消关注用户的表单</p><p class="file"><code>app/views/users/_unfollow.html.erb</code></p><div class="highlight type-erb"><pre><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">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 btn-large"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</pre></div>
</div>
<p>这两个表单都使用了 <code>form_for</code> 来处理 Relationship 模型对象,二者之间主要的不同点是,代码 11.25 中的代码是构建一个新的“关系”,而代码 11.26 是查找现有的“关系”。很显然,第一个表单会向 Relationships 控制器发送 POST 请求,创建“关系”;而第二个表单发送的是 DELETE 请求,销毁“关系”。(两个表单用到的动作会在 <a href="chapter11.html#section-11-2-4">11.2.4 节</a>编写。)你可能还注意到了,这两个表单中除了按钮之外什么内容也没有,但是还要传送 <code>followed_id</code>,我们会调用 <code>hidden_fields</code> 方法将其加入,生成的 HTML 如下:</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><input</span> <span class="na">id=</span><span class="s">"relationship_followed_id"</span> <span class="na">name=</span><span class="s">"relationship[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>
</pre></div>
</div>
<p>隐藏的 <code>input</code> 表单域会把所需的信息包含在表单中,但是在浏览器中不会显示出来。</p>
<p>现在我们可以在页面中加入关注表单和关注数量统计了,只需渲染相应的局部视图即可,如代码 11.27 所示。在用户资料页面中,根据实际的关注情况,会分别显示关注按钮或取消关注按钮,如图 11.12 和图 11.13 所示。</p>
<div class="figure" id="figure-11-12">
<img src="figures/profile_follow_button_bootstrap.png" alt="profile follow button bootstrap" />
<p class="caption"><span>图 11.12:</span>显示了关注按钮的用户资料页面(<a href="http://localhost:3000/users/2">/users/2</a>)</p>
</div>
<div class="codeblock has-caption" id="codeblock-11-27"><p class="caption"><span>代码 11.27:</span>在用户资料页面加入关注表单和关注数量统计</p><p class="file"><code>app/views/users/show.html.erb</code></p><div class="highlight type-erb"><pre><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">"span4"</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="nt"><section></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</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">"span8"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'follow_form'</span> <span class="k">if</span> <span class="n">signed_in?</span> <span class="cp">%></span>
.
.
.
<span class="nt"></div></span>
<span class="nt"></div></span>
</pre></div>
</div>
<p>稍后我们会让这些按钮起作用,而且我们会使用两种实现方式,一种是常规方式(<a href="chapter11.html#section-11-2-4">11.2.4 节</a>),另一种是使用 Ajax 的方式(<a href="chapter11.html#section-11-2-5">11.2.5 节</a>)。不过在此之前,我们要完成用户界面的制作,创建显示关注的用户列表和粉丝列表的页面。</p>
<h3 id='section-11-2-3'><span>11.2.3</span> 关注列表和粉丝列表页面</h3>
<p>显示关注列表和粉丝列表的页面基本上是重组用户的资料页面和用户索引页面(参见 <a href="chapter9.html#section-9-3-1">9.3.1 节</a>),在侧边栏中显示用户的信息(包括关注数量统计),再列出用户列表。除此之外,还会在侧边栏中显示一个由用户的头像组成的栅格。构思图如图 11.14(关注的人)和图 11.15(粉丝们)所示。</p>
<p>首先,我们要让这两个页面的地址可访问,按照 Twitter 的方式,访问这两个页面都需要先登录,测试如代码 11.28 所示。用户登录后,页面中应该分别显示关注的用户列表和粉丝列表,测试如代码 11.29 所示。</p>
<div class="codeblock has-caption" id="codeblock-11-28"><p class="caption"><span>代码 11.28:</span>测试关注列表和粉丝列表页面的访问权限设置</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"authorization"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"for non-signed-in users"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"in the Users controller"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"visiting the following page"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">following_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="s1">'Sign in'</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"visiting the followers page"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">followers_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="s1">'Sign in'</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>