-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter10.html
1770 lines (1479 loc) · 168 KB
/
chapter10.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>第 10 章 用户的微博</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-10"><span>第 10 章</span> 用户的微博</h1>
<ol class="toc"> <li class="level-2">
<a href="#section-10-1">10.1 Microposts 模型</a>
</li>
<li class="level-3">
<a href="#section-10-1-1">10.1.1 基本模型</a>
</li>
<li class="level-3">
<a href="#section-10-1-2">10.1.2 第一个数据验证</a>
</li>
<li class="level-3">
<a href="#section-10-1-3">10.1.3 用户和微博之间的关联</a>
</li>
<li class="level-3">
<a href="#section-10-1-4">10.1.4 改进 Micropost 模型</a>
</li>
<li class="level-3">
<a href="#section-10-1-5">10.1.5 验证微博内容</a>
</li>
<li class="level-2">
<a href="#section-10-2">10.2 显示微博</a>
</li>
<li class="level-3">
<a href="#section-10-2-1">10.2.1 充实用户资料页面</a>
</li>
<li class="level-3">
<a href="#section-10-2-2">10.2.2 示例微博</a>
</li>
<li class="level-2">
<a href="#section-10-3">10.3 微博相关的操作</a>
</li>
<li class="level-3">
<a href="#section-10-3-1">10.3.1 访问限制</a>
</li>
<li class="level-3">
<a href="#section-10-3-2">10.3.2 创建微博</a>
</li>
<li class="level-3">
<a href="#section-10-3-3">10.3.3 临时的动态列表</a>
</li>
<li class="level-3">
<a href="#section-10-3-4">10.3.4 删除微博</a>
</li>
<li class="level-2">
<a href="#section-10-4">10.4 小结</a>
</li>
<li class="level-2">
<a href="#section-10-5">10.5 练习</a>
</li>
</ol>
<div class="main">
<p>我们在<a href="chapter9.html">第 9 章</a>中已经实现了一个完整且符合 REST 架构的资源:用户,本章我们要再实现一个资源:用户微博(micropost)。<sup class="footnote" id="fnref-10-1"><a href="#fn-10-1" rel="footnote">1</a></sup>微博是由用户发布的一种简短消息,我们在<a href="chapter2.html">第 2 章</a>中实现了微博的雏形。 本章我们会在 <a href="chapter2.html#section-2-3">2.3 节</a>的基础上,实现一个功能完善的 Microposts 资源。首先,我们要创建微博所需的数据模型,通过 <code>has_many</code> 和 <code>belongs_to</code> 方法把微博和用户关联起来,再建立处理和显示微博所需的表单及局部视图。在 <a href="chapter11.html">第 11 章</a>,还要加入关注其他用户的功能,其时,我们这个山寨版 Twitter 才算完成。</p>
<p>如果你使用 Git 做版本控制的话,和之前一样,我建议你新建一个分支:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git checkout -b user-microposts
</pre></div>
</div>
<h2 id='section-10-1'><span>10.1</span> Microposts 模型</h2>
<p>实现 Microposts 资源的第一步是创建微博所需的数据模型,在模型中设定微博的基本属性。和 <a href="chapter2.html#section-2-3">2.3 节</a> 创建的模型类似,我们要实现的 Micropost 模型要包含数据验证,以及和 User 模型的关联。除此之外,我们还会做充分的测试,指定默认的排序方式,自动删除已注销用户的微博。</p>
<h3 id='section-10-1-1'><span>10.1.1</span> 基本模型</h3>
<p>Micropost 模型只需要两个属性:一个是 <code>content</code>,用来保存微博的内容;<sup class="footnote" id="fnref-10-2"><a href="#fn-10-2" rel="footnote">2</a></sup>另一个是 <code>user_id</code>,把微博和用户关联起来。我们要使用 <code>generate model</code> 命令生成所需的模型,这一点和创建用户模型时是一样的(参见代码 6.1):</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails generate model Micropost content:string user_id:integer
</pre></div>
</div>
<p>这个命令有可能会生成一个微博预构件,你应该把它删掉,因为后面我们会自己编写所需的预构件(参见<a href="chapter10.html#section-10-1-4">10.1.4 节</a>):</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rm -f spec/factories/microposts.rb
</pre></div>
</div>
<p>上述命令会生成一个迁移文件,在数据库中生成一个名为 <code>microposts</code> 的表(参见代码 10.1)。读者朋友可以和生成 <code>users</code> 表的迁移文件对照一下(参见代码 6.2)。</p>
<div class="codeblock has-caption" id="codeblock-10-1"><p class="caption"><span>代码 10.1:</span>创建微博模型的迁移文件(注意:为 <code>user_id</code> 和 <code>created_at</code> 列加入了索引)</p><p class="file"><code>db/migrate/[timestamp]_create_microposts.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">CreateMicroposts</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">microposts</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">string</span> <span class="ss">:content</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:user_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">microposts</span><span class="p">,</span> <span class="p">[</span><span class="ss">:user_id</span><span class="p">,</span> <span class="ss">:created_at</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意,因为我们设想要按照发布时间的倒序查询某个用户所有的微博,所以在上述代码中为 <code>user_id</code> 和 <code>created_at</code> 列加入了索引:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">add_index</span> <span class="p">:</span><span class="n">microposts</span><span class="p">,</span> <span class="p">[</span><span class="ss">:user_id</span><span class="p">,</span> <span class="ss">:created_at</span><span class="p">]</span>
</pre></div>
</div>
<p>我们把 <code>user_id</code> 和 <code>created_at</code> 放在一个数组中,告诉 Rails 我们要创建的是“多键索引(multiple key index)”,Active Record 便会同时使用这两个键。还要注意 <code>t.timestamps</code> 这行,我们在 <a href="chapter6.html#section-6-1-1">6.1.1 节</a>中介绍过,它会自动创建 <code>created_at</code> 和 <code>updated_at</code> 两个属性。在 <a href="chapter10.html#section-10-1-4">10.1.4 节</a> 和 <a href="chapter10.html#section-10-2-1">10.2.1 节</a> 中才会用到 <code>created_at</code>。</p>
<p>我们先参照 User 模型的测试(参照代码 6.5),为 Micropost 模型编写一些基本的测试。我们要测试微博对象是否可以响应 <code>content</code> 和 <code>user_id</code> 方法,如代码 10.2 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-2"><p class="caption"><span>代码 10.2:</span>Micropost 模型测试(初始版)</p><p class="file"><code>spec/models/micropost_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">Micropost</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="c1"># This code is not idiomatically correct.</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">subject</span> <span class="p">{</span> <span class="vi">@micropost</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">:content</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">:user_id</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-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>执行上面两个命令之后,会生成 Micropost 模型,结构如图 10.1 所示。</p>
<div class="figure" id="figure-10-1">
<img src="figures/micropost_model.png" alt="micropost_model" />
<p class="caption"><span>图 10.1:</span>Micropost 数据模型</p>
</div>
<p>然后确认测试是否可以通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/models/micropost_spec.rb
</pre></div>
</div>
<p>测试虽然可以通过,不过你可能注意到代码 10.2 中的这几行代码了:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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="c1"># This code is not idiomatically correct.</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
</div>
<p>就像其中的注释所说,<code>before</code> 块中的代码并不完全正确。你可以想一下为什么,我们会在 <a href="chapter10.html#section-10-1-3">10.1.3 节</a>中告诉你答案。</p>
<h3 id='section-10-1-2'><span>10.1.2</span> 第一个数据验证</h3>
<p>Micropost 模型必须要有一个属性表明用户的 ID,这样才能知道是哪个用户发布的微博。要加入这个属性,最好的方法是使用 Active Record 关联,会在 <a href="chapter10.html#section-10-1-3">10.1.3 节</a>实现。实现的过程需要一点重构,本节我们就编写一个测试捕获可能出现的错误。</p>
<p>测试如代码 10.3 所示,我们先把用户 ID 设为 <code>nil</code>,然后检测对应的微博是否不合法。(可以和代码 6.8 中针对 User 模型的测试对比一下。)</p>
<div class="codeblock has-caption" id="codeblock-10-3"><p class="caption"><span>代码 10.3:</span>测试微博能否通过验证</p><p class="file"><code>spec/models/micropost_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">Micropost</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="c1"># This code is not idiomatically correct.</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">subject</span> <span class="p">{</span> <span class="vi">@micropost</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">:content</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">:user_id</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">be_valid</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"when user_id is not present"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="vi">@micropost</span><span class="p">.</span><span class="nf">user_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>
<p>这段代码测试了微博是否能够通过验证,以及是否指定了 <code>user_id</code> 的值。要想让上述测试通过,我们要按照代码 10.4 所示,加入一个简单的存在性验证。</p>
<div class="codeblock has-caption" id="codeblock-10-4"><p class="caption"><span>代码 10.4:</span>对微博 <code>user_id</code> 属性的验证</p><p class="file"><code>app/models/micropost.rb</code></p><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">validates</span> <span class="p">:</span><span class="n">user_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-10-1-3'><span>10.1.3</span> 用户和微博之间的关联</h3>
<p>在为 Web 程序构建数据模型时,最基本的考虑要素是要能够在不同的模型之间建立关联。在我们这个程序中,每篇微博都关联着一个用户,而每个用户一般都会关联多篇微博。用户和微博之间的关系在 <a href="chapter2.html#section-2-3-3">2.3.3 节</a>中简单的介绍过,二者之间的关系如图 10.2 和图 10.3 所示。在实现这种关联的时候,我们会编写针对 Micropost 模型的测试,还会编写针对 User 模型的测试。</p>
<div class="figure" id="figure-10-2">
<img src="figures/micropost_belongs_to_user.png" alt="micropost_belongs_to_user" />
<p class="caption"><span>图 10.2:</span>微博和用户之间的“属于(<code>belongs_to</code>)”关系</p>
</div>
<div class="figure" id="figure-10-3">
<img src="figures/user_has_many_microposts.png" alt="user_has_many_microposts" />
<p class="caption"><span>图 10.3:</span>用户和微博之间的“拥有多个(<code>has_many</code>)”关系</p>
</div>
<p>使用本小节介绍的 <code>belongs_to</code> 和 <code>has_many</code> 之后,Rails 会自动创建如<a href="chapter10.html#table-10-1">表格 10.1</a> 所示的方法。</p>
<div class="table has-caption" id="table-10-1"><p class="caption"><span>表格 10.1:</span>用户和微博关联后所得方法的简介</p><table>
<thead>
<tr>
<th>方法</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>micropost.user</code></td>
<td>返回该微博对应的用户对象</td>
</tr>
<tr>
<td><code>user.microposts</code></td>
<td>返回该用户的所有微博数组</td>
</tr>
<tr>
<td><code>user.microposts.create(arg)</code></td>
<td>创建一篇微博(<code>user_id = user.id</code>)</td>
</tr>
<tr>
<td><code>user.microposts.create!(arg)</code></td>
<td>创建一篇微博(失败时抛出异常)</td>
</tr>
<tr>
<td><code>user.microposts.build(arg)</code></td>
<td>生成一个新的微博对象(user_id = user.id)</td>
</tr>
</tbody>
</table>
</div>
<p>注意,从表格 10.1 可知,相较于以下的方法</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="no">Micropost</span><span class="p">.</span><span class="nf">create</span>
<span class="no">Micropost</span><span class="p">.</span><span class="nf">create!</span>
<span class="no">Micropost</span><span class="p">.</span><span class="nf">new</span>
</pre></div>
</div>
<p>我们得到了</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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="n">user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">create!</span>
<span class="n">user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">build</span>
</pre></div>
</div>
<p>后者才是创建微博的正确方式,即通过相关联的用户对象创建。通过这种方式创建的微博,其 <code>user_id</code> 属性会自动设为正确的值。所以,我们可以把代码 10.3 中的下述代码</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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="c1"># This code is not idiomatically correct.</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</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="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="p">{</span> <span class="vi">@micropost</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">build</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</span><span class="p">)</span> <span class="p">}</span>
</pre></div>
</div>
<p>只要正确定义了用户和微博之间的关联关系,<code>@micropost</code> 变量的 <code>user_id</code> 属性就会自动设为相对应用户的 id。</p>
<p>如<a href="chapter10.html#table-10-1">表格 10.1</a> 所示,用户和微博建立关联之后,还会生成 <code>micropost.user</code> 方法,返回该微博的用户对象。对此,我们可以使用 <code>it</code> 和 <code>its</code> 做个测试:</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">respond_to</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="n">eq</span> <span class="n">user</span> <span class="p">}</span>
</pre></div>
</div>
<p>以上对 Micropost 模型的测试结合在一起后如代码 10.5 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-5"><p class="caption"><span>代码 10.5:</span>测试微博和用户之间的关联</p><p class="file"><code>spec/models/micropost_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">Micropost</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="p">{</span> <span class="vi">@micropost</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">build</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">subject</span> <span class="p">{</span> <span class="vi">@micropost</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">:content</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">:user_id</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">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">its</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="n">should</span> <span class="n">eq</span> <span class="n">user</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="n">describe</span> <span class="s2">"when user_id is not present"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="vi">@micropost</span><span class="p">.</span><span class="nf">user_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>
<p>在用户和微博关联关系的 User 模型一边,我们会在 <a href="chapter10.html#section-10-1-4">10.1.4 节</a>做详细的测试,现在我们只是简单的测试下是否可以响应 <code>microposts</code> 方法,如代码 10.6 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-6"><p class="caption"><span>代码 10.6:</span>测试用户对象是否可以响应 <code>microposts</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="n">before</span> <span class="k">do</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</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">"[email protected]"</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="k">end</span>
<span class="n">subject</span> <span class="p">{</span> <span class="vi">@user</span> <span class="p">}</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">:admin</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">:microposts</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>写好了上面的测试,实现用户和微博之间关联就简单了:只需分别加入下面这两行代码,代码 10.5 和代码 10.6 中的测试就可以通过了:<code>belongs_to :user</code>(如代码 10.7 所示)和 <code>has_many :microposts</code>(如代码 10.8 所示)。</p>
<div class="codeblock has-caption" id="codeblock-10-7"><p class="caption"><span>代码 10.7:</span>微博“属于(<code>belongs_to</code>)”用户</p><p class="file"><code>app/models/micropost.rb</code></p><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="n">validates</span> <span class="p">:</span><span class="n">user_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-10-8"><p class="caption"><span>代码 10.8:</span>用户“拥有多篇(<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="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>现在,你应该结合<a href="chapter10.html#table-10-1">表格 10.1</a>和代码 10.5、代码 10.6,确保你理解了关联的基本知识点。你还应该检查一下测试是否可以通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/models
</pre></div>
</div>
<h3 id='section-10-1-4'><span>10.1.4</span> 改进 Micropost 模型</h3>
<p>代码 10.6 中的代码并没有深入测试通过 <code>has_many</code> 实现的关联,仅仅检测了是否可以响应 <code>microposts</code> 方法。本小节,我们会为 Micropost 模型加入排序方法和依属关系,还会测试 <code>user.microposts</code> 方法的返回结果是否为数组。</p>
<p>我们需要在 User 模型的测试中生成一些微博,所以现在我们先要创建一个生成微博的预构件。在所创建的预构件中我们要找到一种方法把微博和用户关联起来,幸运的是,在 FactoryGirl 中实现关联是很容易的,如代码 10.9 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-9"><p class="caption"><span>代码 10.9:</span>完整的预构件文件,包含了创建微博的新预构件</p><p class="file"><code>spec/factories.rb</code></p><div class="highlight type-ruby"><pre><span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
<span class="n">factory</span> <span class="p">:</span><span class="n">user</span> <span class="k">do</span>
<span class="n">sequence</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span> <span class="s2">"Person </span><span class="si">#{</span><span class="n">n</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
<span class="n">sequence</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span> <span class="s2">"person_</span><span class="si">#{</span><span class="n">n</span><span class="si">}</span><span class="s2">@example.com"</span><span class="p">}</span>
<span class="n">password</span> <span class="s2">"foobar"</span>
<span class="n">password_confirmation</span> <span class="s2">"foobar"</span>
<span class="n">factory</span> <span class="p">:</span><span class="n">admin</span> <span class="k">do</span>
<span class="n">admin</span> <span class="kp">true</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">factory</span> <span class="p">:</span><span class="n">micropost</span> <span class="k">do</span>
<span class="n">content</span> <span class="s2">"Lorem ipsum"</span>
<span class="n">user</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>在 FactoryGirl 中我们只需要在创建微博的预构件中包含一个用户对象就可以实现所需的关联了:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">factory</span> <span class="p">:</span><span class="n">micropost</span> <span class="k">do</span>
<span class="n">content</span> <span class="s2">"Lorem ipsum"</span>
<span class="n">user</span>
<span class="k">end</span>
</pre></div>
</div>
<p>在下一节中会介绍,我们可以使用下面的方法生成一篇微博:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
</pre></div>
</div>
<h4 id='section-10-1-4-1'><span></span>默认作用域</h4>
<p>默认情况下,使用 <code>user.microposts</code> 从数据库中读取用户的微博不能保证微博的次序,但是按照博客和 Twitter 的习惯,我们希望微博按照创建时间倒序排列,也就是最新创建的微博在最前面。要测试微博的次序,我们要先创建两篇微博:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
</pre></div>
</div>
<p>我们把第二篇微博的创建时间设的晚一些,即 <code>1.hour.ago</code>(利用了<a href="chapter8.html#aside-8-1">旁注 8.1</a>中介绍的帮助函数),第一篇微博的创建时间要早一些,是 <code>1.day.ago</code>。请注意一下使用 FactoryGirl 创建微博是多么方便:我们不仅可以直接指定微博所属的用户(FactoryGirl 会逃过 <code>attr_accessible</code> 限制),还可以设定通常情况下不能自由设定的 <code>created_at</code> 属性(因为在 Active Record 做了限制)。(再次说明一下,<code>created_at</code> 和 <code>updated_at</code> 两个属性是“魔法”列,会被自动设为相应的创建时间戳和更新时间戳,即使手动指定了值也会被覆盖。)</p>
<p>大多数数据库适配器(包括 SQLite 的适配器)读取的微博都是按照 ID 来排序的,因此代码 10.10 中的测试肯定不会通过。在这段测试代码中没有使用 <code>let</code>,而用了 <code>let!</code>(读作“let bang”),因为 <code>let</code> 方法指定的变量是“惰性”的,只有当后续有引用时才会被创建。而我们希望这两个微博变量立即被创建,这样才能保证两篇微博时间戳的顺序是正确的,也保证了 <code>@user.microposts</code> 数组不是空的。所以我们才用了 <code>let!</code> 方法,强制相应的变量立即被创建。</p>
<div class="codeblock has-caption" id="codeblock-10-10"><p class="caption"><span>代码 10.10:</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">describe</span> <span class="s2">"micropost associations"</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">save</span> <span class="p">}</span>
<span class="n">let!</span><span class="p">(</span><span class="ss">:older_micropost</span><span class="p">)</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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">let!</span><span class="p">(</span><span class="ss">:newer_micropost</span><span class="p">)</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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">it</span> <span class="s2">"should have the right microposts in the right order"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">to_a</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="p">[</span><span class="n">newer_micropost</span><span class="p">,</span> <span class="n">older_micropost</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>这个测试中最关键的一行是:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span><span class="p">(</span><span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">to_a</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="p">[</span><span class="n">newer_micropost</span><span class="p">,</span> <span class="n">older_micropost</span><span class="p">]</span>
</pre></div>
</div>
<p>这行代码表明所创建的微博应该按照创建时间倒序排列,即最新创建的微博排在最前面。这个测试注定是无法通过的,因为微博默认是按照 ID 排序的,即 <code>[older_micropost, newer_micropost]</code>。这个测试同时也验证了 <code>has_many</code> 关联最基本的效果是否正确,即检测 <code>user.microposts</code> 返回的结果是否是数组。其中用到的 <code>to_a</code> 方法,在 <a href="chapter4.html#section-4-3-1">4.3.1 节</a>中介绍过,会把 <code>@user.microposts</code> 从初始状态(Active Record 的“集合代理(collection proxy)”)转换成一个数组,以便和我们手动编写的数组比较。</p>
<p>要让这个测试通过,我们要使用 Rails 中的 <code>default_scope</code> 方法,还要设定它的 <code>:order</code> 参数,如代码 10.11 所示。(这是我们第一次接触作用域的概念,在<a href="chapter11.html">第 11 章</a>中会介绍作用域更一般的用法。)</p>
<div class="codeblock has-caption" id="codeblock-10-11"><p class="caption"><span>代码 10.11:</span>通过 <code>default_scope</code> 设定微博的排序</p><p class="file"><code>app/models/micropost.rb</code></p><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="n">default_scope</span> <span class="o">-></span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="s1">'created_at DESC'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">validates</span> <span class="p">:</span><span class="n">user_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="k">end</span>
</pre></div>
</div>
<p>我们通过 <code>microposts.created_at DESC</code> 设定了所需的排序,其中 <code>DESC</code> 在 SQL 中是“倒序”的意思,即按照由新到旧这种顺序排序。</p>
<p>从 Rails 4.0 开始,所有的作用域都需要通过匿名函数的方式定义,函数的返回值为所需的条件。这么做主要的好处是,作用域的条件不用立即计算,只在需要时才执行(所以叫做“惰性计算”)。本例所用的匿名函数是</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="o">-></span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="s1">'created_at DESC'</span><span class="p">)</span> <span class="p">}</span>
</pre></div>
</div>
<p>这种对象叫 Proc 或 lambda,以 <code>-></code> 开头,后面跟着一个代码块,调用 <code>call</code> 方法时才执行块中的代码。我们可以在控制台中看一下:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">>> </span>-> <span class="o">{</span> puts <span class="s2">"foo"</span> <span class="o">}</span>
<span class="gp">=> </span><span class="c">#<Proc:0x007fab938d0108@(irb):1 (lambda)></span>
<span class="gp">>> </span>-> <span class="o">{</span> puts <span class="s2">"foo"</span> <span class="o">}</span>.call
foo
<span class="gp">=> </span>nil
</pre></div>
</div>
<p>(Proc 属于 Ruby 语言的高级功能,所以如果没有立刻理解也不用担心。)</p>
<h4 id='section-10-1-4-2'><span></span>依属关系:destroy</h4>
<p>除了设定恰当的排序外,我们还要对微博模型做另一项改进。我们在 <a href="chapter9.html#section-9-4">9.4 节</a>中介绍过,管理员是有权限删除用户的。那么,在删除用户的同时,就有必要把该用户发布的微博也删除。对此我们可以编写一个测试,检测当用户被删除后,其发布的微博是否还在数据库中。</p>
<p>为了能够正确的测试微博是否被删除了,我们先要把用户的一篇微博赋值给一个局部变量,然后再删除这个用户。对此,一种比较直观的实现方式如下所示:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">microposts</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">to_a</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">destroy</span>
<span class="n">expect</span><span class="p">(</span><span class="n">microposts</span><span class="p">).</span><span class="nf">not_to</span> <span class="n">be_empty</span>
<span class="n">microposts</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">micropost</span><span class="o">|</span>
<span class="c1"># Make sure the micropost doesn't appear in the database.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>我们调用了 <code>to_a</code> 方法,高效地复制了一份微薄列表。我们还加入了下面这行</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span><span class="p">(</span><span class="n">microposts</span><span class="p">).</span><span class="nf">not_to</span> <span class="n">be_empty</span>
</pre></div>
</div>
<p>这是一份双保险,避免不小心漏掉了 <code>to_a</code>。如果没有 <code>to_a</code> 的话,一旦删除了用户就会删掉 <code>microposts</code> 变量中的该用户的微博,那么</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">microposts</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">micropost</span><span class="o">|</span>
<span class="c1"># Make sure the micropost doesn't appear in the database.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>就什么也测试不了,因为 <code>microposts</code> 是空的。</p>
<p>我们可以通过下面的代码确认微博不在数据库中:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">microposts</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">micropost</span><span class="o">|</span>
<span class="n">expect</span><span class="p">(</span><span class="no">Micropost</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">micropost</span><span class="p">.</span><span class="nf">id</span><span class="p">)).</span><span class="nf">to</span> <span class="n">be_empty</span>
<span class="k">end</span>
</pre></div>
</div>
<p>这里我们用的是 <code>Micropost.where</code>,而不是 <code>Micropost.find</code>,因为如果没有查询到记录 <code>where</code> 会返回一个空值对象,而 <code>find</code> 会抛出异常。测试空值对象要更容易。如果你好奇想知道如何测试异常,可以使用下面的代码:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">expect</span> <span class="k">do</span>
<span class="no">Micropost</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">micropost</span><span class="p">)</span>
<span class="k">end</span><span class="p">.</span><span class="nf">to</span> <span class="n">raise_error</span><span class="p">(</span><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">RecordNotFound</span><span class="p">)</span>
</pre></div>
</div>
<p>完整的测试代码如代码 10.12 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-12"><p class="caption"><span>代码 10.12:</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">describe</span> <span class="s2">"micropost associations"</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">save</span> <span class="p">}</span>
<span class="n">let!</span><span class="p">(</span><span class="ss">:older_micropost</span><span class="p">)</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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">let!</span><span class="p">(</span><span class="ss">:newer_micropost</span><span class="p">)</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="vi">@user</span><span class="p">,</span> <span class="ss">created_at: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">ago</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">it</span> <span class="s2">"should destroy associated microposts"</span> <span class="k">do</span>
<span class="n">microposts</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">to_a</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">destroy</span>
<span class="n">expect</span><span class="p">(</span><span class="n">microposts</span><span class="p">).</span><span class="nf">not_to</span> <span class="n">be_empty</span>
<span class="n">microposts</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">micropost</span><span class="o">|</span>
<span class="n">expect</span><span class="p">(</span><span class="no">Micropost</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">micropost</span><span class="p">.</span><span class="nf">id</span><span class="p">)).</span><span class="nf">to</span> <span class="n">be_empty</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>要让代码 10.12 中的测试通过,我们甚至不需要加入一行完整的代码,只需在 <code>has_many</code> 方法中设定一个参数即可,如代码 10.13 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-13"><p class="caption"><span>代码 10.13:</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="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="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">has_many</span> <span class="p">:</span><span class="n">microposts</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
</pre></div>
</div>
<p>其中的 <code>dependent: :destroy</code> 设定程序在用户被删除的时候,其所属的微博也要被删除。这么一来,如果管理员删除了用户,数据库中就不会出现无主的微博了。</p>
<p>至此,用户和微博之间的关联就设置好了,所有的测试应该都可以通过了:</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-10-1-5'><span>10.1.5</span> 验证微博内容</h3>
<p>在结束讨论 Micropost 模型之前,我们还要为微博的内容加上数据验证(参照 <a href="chapter2.html#section-2-3-2">2.3.2 节</a>)。和 <code>user_id</code> 一样,<code>content</code> 属性不能为空,而且还要限制内容的长度不能多于 140 个字符,这才是真正的“微”博。我们要编写的测试和 <a href="chapter6.html#section-6-2">6.2 节</a>中对用户模型的验证测试类似,如代码 10.14 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-14"><p class="caption"><span>代码 10.14:</span>测试 Micropost 模型的数据验证</p><p class="file"><code>spec/models/micropost_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">Micropost</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="p">{</span> <span class="vi">@micropost</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">build</span><span class="p">(</span><span class="ss">content: </span><span class="s2">"Lorem ipsum"</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">"when user_id is not present"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="vi">@micropost</span><span class="p">.</span><span class="nf">user_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">"with blank content"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="vi">@micropost</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="s2">" "</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">"with content that is too long"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="vi">@micropost</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="s2">"a"</span> <span class="o">*</span> <span class="mi">141</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>
<p>和 <a href="chapter6.html#section-6-2">6.2 节</a>一样,在代码 10.14 中我们用到了字符串乘积来测试微博内容长度的验证:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails console
<span class="gp">>> </span><span class="s2">"a"</span> <span class="k">*</span> 10
<span class="gp">=> </span><span class="s2">"aaaaaaaaaa"</span>
<span class="gp">>> </span><span class="s2">"a"</span> <span class="k">*</span> 141
<span class="gp">=> </span><span class="s2">"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"</span>
</pre></div>
</div>
<p>我们需要在程序中加入下面这行代码:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">validates</span> <span class="p">:</span><span class="n">content</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">140</span> <span class="p">}</span>
</pre></div>
</div>
<p>Micropost 模型的最终代码如代码 10.15 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-15"><p class="caption"><span>代码 10.15:</span>Micropost 模型的数据验证</p><p class="file"><code>app/models/micropost.rb</code></p><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="n">default_scope</span> <span class="o">-></span> <span class="p">{</span> <span class="n">order</span><span class="p">(</span><span class="s1">'created_at DESC'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">validates</span> <span class="p">:</span><span class="n">content</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">140</span> <span class="p">}</span>
<span class="n">validates</span> <span class="p">:</span><span class="n">user_id</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="k">end</span>
</pre></div>
</div>
<h2 id='section-10-2'><span>10.2</span> 显示微博</h2>
<p>尽管我们还没实现直接在网页中发布微博的功能(将在 <a href="chapter10.html#section-10-3-2">10.3.2 节</a>实现),不过我们还是有办法显示微博,并对显示的内容进行测试。我们将按照 Twitter 的方式,用户的微博不在 <code>index</code> 页面中显示,而在 <code>show</code> 页面中,构思图如图 10.4 所示。我们会先创建一些很简单的 ERb 代码,在用户的资料页面显示微博,然后要在 <a href="chapter9.html#section-9-3-2">9.3.2 节</a>中实现的数据生成器中加入生成微博的代码,这样我们才有内容可以显示。</p>
<p>和 <a href="chapter8.html#section-8-2-1">8.2.1 节</a>中对登录机制的介绍类似,<a href="chapter10.html#section-10-2-1">10.2.1 节</a>也会经常将一些元素推送到<a href="http://en.wikipedia.org/wiki/Stack\_\(data_structure\)">堆栈</a>里,然后再一个一个的从栈尾取出来。如果理解起来有点困难,多点耐心,你的付出会在 <a href="chapter10.html#section-10-2-2">10.2.2 节</a>得到回报。</p>
<h3 id='section-10-2-1'><span>10.2.1</span> 充实用户资料页面</h3>
<p>我们先在用户的 request spec 中加入对显示微博的测试。我们采用的方法是,先通过预构件创建几篇微博,然后检查用户资料页面是否显示了这几篇微博,同时我们还要验证是否显示了如图 10.4 中所示的总的微博数量。</p>
<p>我们可以使用 <code>let</code> 方法创建微博,不过如代码 10.10 所示,我们希望用户和微博的关联立即生效,这样微博才能显示在用户的资料页面中。所以,我们要使用 <code>let!</code> 方法:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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">let!</span><span class="p">(</span><span class="ss">:m1</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">: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">"Foo"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">let!</span><span class="p">(</span><span class="ss">:m2</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">: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">"Bar"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
</pre></div>
</div>
<div class="figure" id="figure-10-4">
<img src="figures/user_microposts_mockup_bootstrap.png" alt="user microposts mockup bootstrap" />
<p class="caption"><span>图 10.4:</span>显示了微博的资料页面构思图</p>
</div>
<p>按照上面这种方式定义了微博后,我们就可以使用代码 10.16 中的代码测试用户资料页面中是否显示了这些微博。</p>
<div class="codeblock has-caption" id="codeblock-10-16"><p class="caption"><span>代码 10.16:</span>检测用户资料页面是否显示了微博的测试</p><p class="file"><code>spec/requests/user_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">"User 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">"profile page"</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">let!</span><span class="p">(</span><span class="ss">:m1</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">: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">"Foo"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">let!</span><span class="p">(</span><span class="ss">:m2</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">: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">"Bar"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">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_content</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</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="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"microposts"</span> <span class="k">do</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="n">m1</span><span class="p">.</span><span class="nf">content</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_content</span><span class="p">(</span><span class="n">m2</span><span class="p">.</span><span class="nf">content</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_content</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">count</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>
</pre></div>
</div>
<p>注意,我们可以在关联关系的方法上调用 <code>count</code> 方法:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">user</span><span class="p">.</span><span class="nf">microposts</span><span class="p">.</span><span class="nf">count</span>
</pre></div>
</div>
<p>这个 <code>count</code> 方法是很聪明的,可以直接在数据库层统计数量。也就是说,<code>count</code> 的计数过程不是把微博从数据库中取出来,然后再在所得的数组上调用 <code>length</code> 方法,如果这样做的话,微博数量一旦很多的话,效率就会很低。其实,<code>count</code> 方法会直接在数据库层中统计用户的微博数量。如果统计数量仍然是程序的性能瓶颈的话,你可以使用“<a href="http://railscasts.com/episodes/23-counter-cache-column">计数缓存</a>”进一步提速。</p>
<p>在加入代码 10.18 之前,代码 10.16 中的测试是无法通过的,不过现在我们可以先在用户的资料页面中加入一些微博,如代码 10.17 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-17"><p class="caption"><span>代码 10.17:</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="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="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>
</pre></div>
</div>
<p>微博列表稍后分析,现在先看看其他部分。在这段代码中,<code>if @user.microposts.any?</code>(在代码 7.24 中见过类似的用法)的作用是,如果用户没有发布微博的话,就不会显示后面的列表。</p>
<p>还要注意一下,在代码 10.17 中我们也加入了微博的分页显示功能:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="vi">@microposts</span> <span class="cp">%></span>
</pre></div>
</div>
<p>如果和用户索引页面中对应的代码(参见代码 9.33)比较的话,会发现,之前所用的代码是:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="cp">%></span>
</pre></div>
</div>
<p>之前之所以可以直接调用,是因为在 Users 控制器中,<code>will_paginate</code> 默认程序中存在一个名为 <code>@users</code> 的变量(在 <a href="chapter9.html#section-9-3-3">9.3.3 节</a>中介绍过,该变量的值必须是 <code>AvtiveRecord::Relation</code> 的实例)。现在,虽然我们还在 Users 控制器中,但是我们要对<strong>微博</strong>分页,所以 <code>will_paginate</code> 方法要指定 <code>@microposts</code> 变量作为参数。当然了,我们还要在 <code>show</code> 动作中定义 <code>@microposts</code> 变量(参见代码 10.19)。</p>
<p>接着,还显示了当前微博的数量:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><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>
</pre></div>
</div>
<p>前面介绍过,<code>@user.microposts.count</code> 的作用和 <code>User.count</code> 类似,不过统计的微博数量却是建立在用户和微博的关联关系上的。</p>
<p>最后,我们来看一下显示微博的列表:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><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>
</pre></div>
</div>
<p>这段代码使用了一个有序列表标签 <code>ol</code>,显示一个微博列表,不过关键的部分是通过一个微博局部视图实现的。在 <a href="chapter9.html#section-9-3-4">9.3.4 节</a>中介绍过,如下的代码</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@users</span> <span class="cp">%></span>
</pre></div>
</div>
<p>会使用名为 <code>_user.html.erb</code> 的局部视图渲染 <code>@users</code> 变量中的每一个用户。因此,如下的代码</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@microposts</span> <span class="cp">%></span>
</pre></div>
</div>
<p>会对微博做同样的渲染操作。所以,我们要创建名为 <code>_micropost.html.erb</code> 的局部视图(存放在微博对应的视图文件夹中),如代码 10.18 所示。</p>
<div class="codeblock has-caption" id="codeblock-10-18"><p class="caption"><span>代码 10.18:</span>显示单篇微博的局部视图</p><p class="file"><code>app/views/microposts/_micropost.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><li></span>
<span class="nt"><span</span> <span class="na">class=</span><span class="s">"content"</span><span class="nt">></span><span class="cp"><%=</span> <span class="n">micropost</span><span class="p">.</span><span class="nf">content</span> <span class="cp">%></span><span class="nt"></span></span>
<span class="nt"><span</span> <span class="na">class=</span><span class="s">"timestamp"</span><span class="nt">></span>
Posted <span class="cp"><%=</span> <span class="n">time_ago_in_words</span><span class="p">(</span><span class="n">micropost</span><span class="p">.</span><span class="nf">created_at</span><span class="p">)</span> <span class="cp">%></span> ago.
<span class="nt"></span></span>
<span class="nt"></li></span>
</pre></div>
</div>
<p>这段代码使用了 <code>time_ago_in_words</code> 帮助方法,会在 <a href="chapter10.html#section-10-2-2">10.2.2 节</a>中介绍。</p>
<div class="figure" id="figure-10-5">
<img src="figures/user_profile_no_microposts_bootstrap.png" alt="user profile no microposts bootstrap" />
<p class="caption"><span>图 10.5:</span>添加了显示微博代码后的用户资料页面,不过还没有微博可显示</p>
</div>
<p>至此,尽管定义了所需的全部视图,但是代码 10.16 中的测试仍旧无法通过,提示未定义 <code>@microposts</code> 变量。加入代码 10.19 中的代码后,测试就可以通过了。</p>
<div class="codeblock has-caption" id="codeblock-10-19"><p class="caption"><span>代码 10.19:</span>在 Users 控制器的 <code>show</code> 动作中加入 <code>@microposts</code> 实例变量</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">show</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">@microposts</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">microposts</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="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>请注意一下 <code>paginate</code> 方法是多么的智能,它甚至可以通过关联关系,在 <code>microposts</code> 数据表中取出每个分页中要显示的微博。</p>
<p>现在,我们可以查看一下刚编好的用户资料页面了,如图 10.5 所示,可能会出乎你的意料,这也是理所当然的,因为我们还没有发布微博呢。下面我们就来发布微博。</p>
<h3 id='section-10-2-2'><span>10.2.2</span> 示例微博</h3>
<p>在 <a href="chapter10.html#section-10-2-1">10.2.1 节</a>中为了显示微博,创建了几个视图,但是结果有点不给力。为了改变这个悲剧,我们要在 <a href="chapter9.html#section-9-3-2">9.3.2 节</a>中用到的示例数据生成器中加入生成微博数据的代码。如果给所有的用户都生成一些微博的话要用很长的时间,所以我们暂且只给前六个用户<sup class="footnote" id="fnref-10-3"><a href="#fn-10-3" rel="footnote">3</a></sup>生成微博数据,这要用到 <code>User.all</code> 方法的 <code>:limit</code> 选项:<sup class="footnote" id="fnref-10-4"><a href="#fn-10-4" rel="footnote">4</a></sup></p>
<div class="codeblock"><div class="highlight type-ruby"><pre><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>
</pre></div>
</div>
<p>我们要为每个用户生成 50 篇微博(这个数量大于单页显示的 30 篇限制),使用 Faker gem 中简便的 <code>Lorem.sentence</code> 方法生成每篇微博的内容。(<code>Faker::Lorem.sentence</code> 生成的是 lorem ipsum 示例文字,我们在<a href="chapter6.html">第 6 章</a>中介绍过,lorem ipsum 背后有一段<a href="http://www.straightdope.com/columns/read/2290/what-does-the-filler-text-lorem-ipsum-mean">有趣的故事</a>。)代码 10.20 中显示的是修改后的示例数据生成器。</p>
<div class="codeblock has-caption" id="codeblock-10-20"><p class="caption"><span>代码 10.20:</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="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">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">end</span>
</pre></div>
</div>
<p>当然,如果要生成示例数据,我们要执行 <code>db:populate</code> 命令:</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>
<p>然后,我们就能看到 <a href="chapter10.html#section-10-2-1">10.2.1 节</a>中劳动的果实了,在用户资料页面显示了生成的微博。<sup class="footnote" id="fnref-10-5"><a href="#fn-10-5" rel="footnote">5</a></sup>初步结果如图 10.6 所示。</p>
<div class="figure" id="figure-10-6">
<img src="figures/user_profile_microposts_no_styling_bootstrap.png" alt="user profile microposts no styling bootstrap" />
<p class="caption"><span>图 10.6:</span>用户资料页面(<a href="http://localhost/users/1">/users/1</a>)中显示的尚未样式化的微博列表</p>
</div>
<p>图 10.6 中显示的微博列表还没有加入样式,那我们就加入一些样式(参见代码 10.21)<sup class="footnote" id="fnref-10-6"><a href="#fn-10-6" rel="footnote">6</a></sup>,再看一下页面显示的效果。图 10.7 显示的是第一个用户(当前登录用户)的资料页面,图 10.8 显示的是另一个用户的资料页面,图 10.9 显示的是第一个用户资料页面的第 2 页,页面底部还显示了分页链接。注意观察这三幅图,我们可以看到微博后面显示了距离发布的时间(例如,“Posted 1 minute ago.”),这个效果是通过代码 10.18 中的 <code>time_ago_in_words</code> 方法实现的。过一会再刷新页面,你会发现这些文字依据当前时间自动更新了。</p>
<div class="figure" id="figure-10-7">
<img src="figures/user_profile_with_microposts_bootstrap.png" alt="user profile with microposts bootstrap" />
<p class="caption"><span>图 10.7:</span>显示了微博的用户资料页面(<a href="http://localhost:3000/users/1">/users/1</a>)</p>
</div>
<div class="codeblock has-caption" id="codeblock-10-21"><p class="caption"><span>代码 10.21:</span>微博列表的样式(包含了本章用到的所有样式)</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">microposts</span> <span class="o">*/</span>
<span class="nc">.microposts</span> <span class="p">{</span>
<span class="nl">list-style</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">10px</span> <span class="m">0</span> <span class="m">0</span> <span class="m">0</span><span class="p">;</span>
<span class="nt">li</span> <span class="p">{</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">border-top</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="mh">#e8e8e8</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nc">.content</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="nc">.timestamp</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="nv">$grayLight</span><span class="p">;</span>
<span class="p">}</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="nt">aside</span> <span class="p">{</span>
<span class="nt">textarea</span> <span class="p">{</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
</div>
<div class="figure" id="figure-10-8">
<img src="figures/other_profile_with_microposts_bootstrap.png" alt="other profile with microposts bootstrap" />
<p class="caption"><span>图 10.8:</span>另一个用户的资料页面,也显示了微博列表(<a href="http://localhost:3000/users/5">/users/5</a>)</p>
</div>
<div class="figure" id="figure-10-9">
<img src="figures/user_profile_microposts_page_2_rails_3_bootstrap.png" alt="user profile microposts page 2 rails3 bootstrap" />
<p class="caption"><span>图 10.9:</span>微博分页链接(<a href="http://localhost:3000/users/1?page=2">/users/1?page=2</a>)</p>
</div>
<h2 id='section-10-3'><span>10.3</span> 微博相关的操作</h2>
<p>微博的数据模型构建好了,也编写了相关的视图文件,接下来我们的开发重点是,通过网页发布微博。在实现的过程中,我们会第三次用到表单来创建资源,这一次创建的是 Microposts 资源<sup class="footnote" id="fnref-10-7"><a href="#fn-10-7" rel="footnote">7</a></sup>。本节,我们还会初步实现动态列表(status feed),在<a href="chapter11.html">第 11 章</a>再完善。最后,和 Users 资源类似,我们还要实现在网页中删除微博的功能。</p>
<p>上述功能的实现和之前的惯例有一点是不一样的地方,需要特别注意,那就是,Microposts 资源相关的页面不是通过 Microposts 控制器实现的,而是依赖于 Users 和 StaticPages 控制器。这也就意味着,Microposts 资源的路由设置是很简单的,如代码 10.22 所示。代码 10.22 中的代码所代表的符合 REST 结构的路由如<a href="chapter10.html#table-10-2">表格 10.2</a> 所示,表中的路由只是<a href="chapter2.html#table-2-3">表格 2.3</a> 的一部分。不过,路由虽然简化了,但预示着实现的过程需要更高级的技术,而不会减小代码的复杂度。从<a href="chapter2.html">第 2 章</a>起我们就十分依赖脚手架,不过现在我们将舍弃脚手架的大部分功能。</p>
<div class="codeblock has-caption" id="codeblock-10-22"><p class="caption"><span>代码 10.22:</span>Microposts 资源的路由设置</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="n">resources</span> <span class="p">:</span><span class="n">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="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<div class="table has-caption" id="table-10-2"><p class="caption"><span>表格 10.2:</span>代码 10.25 设置的 Microposts 资源路由</p><table>
<thead>
<tr>
<th>HTTP 请求</th>
<th>URL</th>
<th>动作</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>POST</td>
<td>/microposts</td>
<td><code>create</code></td>
<td>创建新微博</td>
</tr>
<tr>
<td>DELETE</td>
<td>/microposts/1</td>
<td><code>destroy</code></td>
<td>删除 id 为 1 的微博</td>
</tr>
</tbody>
</table>
</div>
<h3 id='section-10-3-1'><span>10.3.1</span> 访问限制</h3>
<p>开发 Microposts 资源的第一步,我们要在 Microposts 控制器中实现访问限制。我们要实现的效果很简单:若要访问 <code>create</code> 和 <code>destroy</code> 动作就要先登录。针对访问限制的 RSpec 测试如代码 10.23 所示。(在 <a href="chapter10.html#section-10-3-4">10.3.4 节</a>中,我们还会测试并加入第三层保护措施,确保只有微博的发布者才能删除该微博。)</p>
<div class="codeblock has-caption" id="codeblock-10-23"><p class="caption"><span>代码 10.23:</span>限制访问 Microposts 资源的测试</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="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"in the Microposts controller"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"submitting to the create action"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">post</span> <span class="n">microposts_path</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">signin_path</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"submitting to the destroy action"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">delete</span> <span class="n">micropost_path</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="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">signin_path</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="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>上述代码没有使用即将实现的网页界面,而是直接在 Microposts 控制器层面操作:如果向 /microposts 发送 POST 请求(<code>POST microposts_path</code> 访问的是 <code>create</code> 动作),或者向 /microposts/1 发送 DELETE 请求(<code>delete micropost_path(micropost)</code> 访问的是 <code>destroy</code> 动作),则会转向登录页面——我们在代码 9.13 中就用过这种方法。</p>
<p>我们要先对程序的代码做点重构,然后再加入程序中,让代码 10.23 中的测试通过。在 <a href="chapter9.html#section-9-2-1">9.2.1 节</a>中,我们定义了一个名为 <code>signed_in_user</code> 的事前过滤器(参见代码 9.12),确保访问相关的动作之前用户要先登录。那时,我们只需要在 Users 控制器中使用这个事前过滤器,但是现在我们在 Microposts 控制器中也要用到,那么我们就把它移到 Sessions 的帮助方法中,如代码 10.24 所示。<sup class="footnote" id="fnref-10-8"><a href="#fn-10-8" rel="footnote">8</a></sup></p>
<div class="codeblock has-caption" id="codeblock-10-24"><p class="caption"><span>代码 10.24:</span>把 <code>signed_in_user</code> 方法移到 Sessions 帮助方法中</p><p class="file"><code>app/helpers/sessions_helper.rb</code></p><div class="highlight type-ruby"><pre><span class="k">module</span> <span class="nn">SessionsHelper</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">current_user?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">user</span> <span class="o">==</span> <span class="n">current_user</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">signed_in_user</span>
<span class="k">unless</span> <span class="n">signed_in?</span>
<span class="n">store_location</span>
<span class="n">redirect_to</span> <span class="n">signin_url</span><span class="p">,</span> <span class="ss">notice: </span><span class="s2">"Please sign in."</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>为了避免代码重复,同时还要把 <code>signed_in_user</code> 从 Users 控制器中删掉。</p>
<p>加入了代码 10.24 之后,我们就可以在 Microposts 控制器中使用 <code>signed_in_user</code> 方法了,因此我们就可以使用代码 10.25 中的事前过滤器来限制访问 <code>create</code> 和 <code>destroy</code> 动作了。(因为我们没有使用命令行生成 Microposts 控制器文件,因此需要手动创建。)</p>
<div class="codeblock has-caption" id="codeblock-10-25"><p class="caption"><span>代码 10.25:</span>在 Microposts 控制器中加入访问限制功能</p><p class="file"><code>app/controllers/microposts_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">MicropostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意,我们没有明确指定事前过滤器要限制的动作有哪几个,因为默认情况下仅有的两个动作都会被限制。如果我们要加入第三个动作,例如 <code>index</code> 动作,未登录的用户可以访问,那么我们就要明确的指定要限制的动作了:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">MicropostsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</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">def</span> <span class="nf">index</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>现在,测试应该可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/authentication_pages_spec.rb