-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathchapter11.html
2413 lines (2051 loc) · 230 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>Ruby on Rails 教程 - 第 11 章 用户的微博</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="最好的 Ruby on Rails 入门教程"/>
<meta name="keywords" content="ruby, rails, tutorial"/>
<meta name="author" content="Michael Hartl"/>
<meta name="translator" content="安道"/>
<meta name="generator" content="persie 0.0.1.beta.3"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="assets/style.css"/>
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/js/collapse.min.js"></script>
<script type="text/javascript" src="assets/global.js"></script>
</head>
<body>
<header class="navbar navbar-default navbar-fixed-top navbar-book">
<div class="container">
<div class="navbar-header">
<a href="http://railstutorial-china.org" class="navbar-brand">Ruby on Rails 教程</a>
<button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target=".book-navbar-collapse">
<span class="sr-only">导航</span>
<i class="fa fa-bars"></i>
</button>
<a href="http://railstutorial-china.org/#purchase" id="navbar-purchase-xs" class="btn btn-warning navbar-btn visible-xs collapsed-purchase-btn">购买</a>
</div>
<nav class="collapse navbar-collapse book-navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="http://railstutorial-china.org" title="首页">首页</a></li>
<li class="active"><a href="http://railstutorial-china.org/read/" title="在线阅读">阅读</a></li>
<li><a href="http://railstutorial-china.org/blog/" title="最新消息">博客</a></li>
<li><a href="https://selfstore.io/products/189/topics" title="论坛">论坛</a></li>
<li class="hidden-xs"><div><a href="http://railstutorial-china.org/#purchase" id="navbar-purchase" class="btn btn-warning navbar-btn" title="购买电子书">购买</a></div></li>
</ul>
</nav>
</div>
</header>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<div class="alert alert-warning">
<p>在线版的内容可能落后于电子书,如果想及时获得更新,请<a href="http://railstutorial-china.org/#purchase" title="购买电子书">购买电子书</a>。</p>
</div>
<article class="article">
<section data-type="chapter" id="user-microposts">
<h1><span class="title-label">第 11 章</span> 用户的微博</h1>
<p>在开发这个演示应用的过程中,我们用到了四个资源:用户,会话,账户激活和密码重设。但只有第一个资源通过 Active Record 模型对应了数据库中的表。本章,我们要再实现一个这样的资源——用户的微博,即用户发布的短消息。<a href="chapter2.html#a-toy-app">第 2 章</a>实现了微博的雏形,本章则会在 <a href="chapter2.html#the-microposts-resource">2.3 节</a>的基础上,实现一个功能完整的微博资源。首先,我们要创建微博数据模型,通过 <code>has_many</code> 和 <code>belongs_to</code> 方法把微博和用户关联起来,然后再创建处理和显示微博所需的表单及局部视图(<a href="#micropost-images">11.4 节</a>还要实现上传图片功能)。在<a href="chapter12.html#following-users">第 12 章</a>,还要加入关注其他用户的功能,届时,我们这个山寨版 Twitter 才算完成。</p>
<section data-type="sect1" id="a-micropost-model">
<h1><span class="title-label">11.1</span> 微博模型</h1>
<p>实现微博资源的第一步是创建微博数据模型,在模型中设定微博的基本特征。和 <a href="chapter2.html#the-microposts-resource">2.3 节</a>创建的模型类似,我们要实现的微博模型要包含数据验证,以及和用户模型之间的关联。除此之外,我们还会做充分的测试,指定默认的排序方式,以及自动删除已注销用户的微博。</p>
<p>如果使用 Git 做版本控制的话,和之前一样,建议你新建一个主题分支:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>git checkout master
<span class="nv">$ </span>git checkout -b user-microposts
</pre></div>
</div>
<section data-type="sect2" id="the-basic-model">
<h2><span class="title-label">11.1.1</span> 基本模型</h2>
<p>微博模型只需要两个属性:一个是 <code>content</code>,用来保存微博的内容;另一个是 <code>user_id</code>,把微博和用户关联起来。微博模型的结构如<a href="#fig-micropost-model">图 11.1</a> 所示。</p>
<div id="fig-micropost-model" class="figure"><img src="images/chapter11/micropost_model_3rd_edition.png" alt="micropost model 3rd edition" /><div class="figcaption"><span class="title-label">图 11.1</span>:微博数据模型</div></div>
<p>注意,在这个模型中,<code>content</code> 属性的类型为 <code>text</code>,而不是 <code>string</code>,目的是存储任意长度的文本。虽然我们会限制微博内容的长度不超过 140 个字符(<a href="#micropost-validations">11.1.2 节</a>),也就是说在 <code>string</code> 类型的 255 个字符长度的限制内,但使用 <code>text</code> 能更好地表达微博的特性,即把微博看成一段文本更符合常理。在 <a href="#creating-microposts">11.3.2 节</a>,会把文本字段换成多行文本字段,用于提交微博。而且,如果以后想让微博的内容更长一些(例如包含多国文字),使用 <code>text</code> 类型处理起来更灵活。何况,在生产环境中使用 <code>text</code> 类型并<a href="http://www.postgresql.org/docs/9.1/static/datatype-character.html">没有什么性能差异</a>,所以不会有什么额外消耗。</p>
<p>和用户模型一样(<a href="chapter6.html#listing-generate-user-model">代码清单 6.1</a>),我们要使用 <code>generate model</code> 命令生成微博模型:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate model Micropost content:text user:references
</pre></div>
</div>
<p>这个命令会生成一个迁移文件,用于在数据库中生成一个名为 <code>microposts</code> 的表,如<a href="#listing-micropost-migration">代码清单 11.1</a> 所示。可以和生成 <code>users</code> 表的迁移对照一下,参见<a href="chapter6.html#listing-users-migration">代码清单 6.2</a>。二者之间最大的区别是,前者使用了 <code>references</code> 类型。<code>references</code> 会自动添加 <code>user_id</code> 列(以及索引),把用户和微博关联起来。和用户模型一样,微博模型的迁移中也自动生成了 <code>t.timestamps</code>。<a href="chapter6.html#database-migrations">6.1.1 节</a>说过,这行代码的作用是添加 <code>created_at</code> 和 <code>updated_at</code> 两列。(<a href="#micropost-refinements">11.1.4 节</a>和 <a href="#rendering-microposts">11.2.1 节</a>会使用 <code>created_at</code> 列。)</p>
<div id="listing-micropost-migration" data-type="listing">
<h5><span class="title-label">代码清单 11.1</span>:微博模型的迁移文件,还创建了索引</h5>
<div class="source-file">db/migrate/[timestamp]_create_microposts.rb</div>
<div class="highlight language-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="ss">: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="o">.</span><span class="n">text</span> <span class="ss">:content</span>
<span class="n">t</span><span class="o">.</span><span class="n">references</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">index</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">foreign_key</span><span class="p">:</span> <span class="kp">true</span>
<span class="n">t</span><span class="o">.</span><span class="n">timestamps</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="hll"> <span class="n">add_index</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="o">[</span><span class="ss">:user_id</span><span class="p">,</span> <span class="ss">:created_at</span><span class="o">]</span>
</span> <span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>因为我们会按照发布时间的倒序查询某个用户发布的所有微博,所以在上述代码中为 <code>user_id</code> 和 <code>created_at</code> 列创建了索引(参见<a href="chapter6.html#aside-database-indices">旁注 6.2</a>):</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">add_index</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="o">[</span><span class="ss">:user_id</span><span class="p">,</span> <span class="ss">:created_at</span><span class="o">]</span>
</pre></div>
</div>
<p>我们把 <code>user_id</code> 和 <code>created_at</code> 放在一个数组中,告诉 Rails 我们要创建的是“多键索引”(multiple key index),因此 Active Record 会同时使用这两个键。</p>
<p>然后像之前一样,执行下面的命令更新数据库:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate
</pre></div>
</div>
</section>
<section data-type="sect2" id="micropost-validations">
<h2><span class="title-label">11.1.2</span> 微博模型的数据验证</h2>
<p>我们已经创建了基本的数据模型,下面要添加一些验证,实现符合需求的约束。微博模型必须要有一个属性表示用户的 ID,这样才能知道某篇微博是由哪个用户发布的。实现这样的属性,最好的方法是使用 Active Record 关联。<a href="#user-micropost-associations">11.1.3 节</a>会实现关联,现在我们直接处理微博模型。</p>
<p>我们可以参照用户模型的测试(<a href="chapter6.html#listing-name-presence-test">代码清单 6.7</a>),在 <code>setup</code> 方法中新建一个微博对象,并把它和固件中的一个有效用户关联起来,然后在测试中检查这个微博对象是否有效。因为每篇微博都要和用户关联起来,所以我们还要为 <code>user_id</code> 属性的存在性验证编写一个测试。综上所述,测试如<a href="#listing-micropost-validity-test">代码清单 11.2</a> 所示。</p>
<div id="listing-micropost-validity-test" data-type="listing">
<h5><span class="title-label">代码清单 11.2</span>:测试微博是否有效 <span class="red">RED</span></h5>
<div class="source-file">test/models/micropost_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">MicropostTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="hll"> <span class="c1"># 这行代码不符合常见做法</span>
</span><span class="hll"> <span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="vi">@user</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"user id should be present"</span> <span class="k">do</span>
<span class="vi">@micropost</span><span class="o">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>如 <code>setup</code> 方法中的注释所说,创建微博使用的方法不符合常见做法,我们会在 <a href="#user-micropost-associations">11.1.3 节</a>修正。</p>
<p>微博是否有效的测试能通过,但用户 ID 存在性验证的测试无法通过,因为微博模型目前还没有任何验证规则:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.3</span>:<strong class="red">RED</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:models
</pre></div>
</div>
<p>为了让测试通过,我们要添加用户 ID 存在性验证,如<a href="#listing-micropost-user-id-validation">代码清单 11.4</a> 所示。(注意,这段代码中 <code>belongs_to</code> 那行由<a href="#listing-micropost-migration">代码清单 11.1</a> 中的迁移自动生成。<a href="#user-micropost-associations">11.1.3 节</a>会深入介绍这行代码的作用。)</p>
<div id="listing-micropost-user-id-validation" data-type="listing">
<h5><span class="title-label">代码清单 11.4</span>:微博模型 <code>user_id</code> 属性的验证 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/micropost.rb</div>
<div class="highlight language-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="ss">:user</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>现在,整个测试组件应该都能通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.5</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
<p>接下来,我们要为 <code>content</code> 属性加上数据验证(参照 <a href="chapter2.html#putting-the-micro-in-microposts">2.3.2 节</a>的做法)。和 <code>user_id</code> 一样,<code>content</code> 属性必须存在,而且还要限制内容的长度不能超过 140 个字符,这才是真正的“微”博。首先,我们要参照 <a href="chapter6.html#user-validations">6.2 节</a>用户模型的验证测试,编写一些简单的测试,如<a href="#listing-micropost-validations-tests">代码清单 11.6</a> 所示。</p>
<div id="listing-micropost-validations-tests" data-type="listing">
<h5><span class="title-label">代码清单 11.6</span>:测试微博模型的验证 <span class="red">RED</span></h5>
<div class="source-file">test/models/micropost_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">MicropostTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="vi">@user</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"user id should be present"</span> <span class="k">do</span>
<span class="vi">@micropost</span><span class="o">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="hll"> <span class="nb">test</span> <span class="s2">"content should be present"</span> <span class="k">do</span>
</span><span class="hll"> <span class="vi">@micropost</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="s2">" "</span>
</span><span class="hll"> <span class="n">assert_not</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
</span><span class="hll"> <span class="k">end</span>
</span>
<span class="hll"> <span class="nb">test</span> <span class="s2">"content should be at most 140 characters"</span> <span class="k">do</span>
</span><span class="hll"> <span class="vi">@micropost</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="s2">"a"</span> <span class="o">*</span> <span class="mi">141</span>
</span><span class="hll"> <span class="n">assert_not</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
</span><span class="hll"> <span class="k">end</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>和 <a href="chapter6.html#user-validations">6.2 节</a>一样,<a href="#listing-micropost-validations-tests">代码清单 11.6</a>也用到了字符串连乘来测试微博内容长度的验证:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console</span>
<span class="gp">>> </span><span class="s2">"a"</span> <span class="o">*</span> <span class="mi">10</span>
<span class="go">=> "aaaaaaaaaa"</span>
<span class="gp">>> </span><span class="s2">"a"</span> <span class="o">*</span> <span class="mi">141</span>
<span class="go">=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
<span class="go">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"</span>
</pre></div>
</div>
<p>在模型中添加的代码基本上和用户模型 <code>name</code> 属性的验证一样(<a href="chapter6.html#listing-length-validation">代码清单 6.16</a>),如<a href="#listing-micropost-validations">代码清单 11.7</a> 所示。</p>
<div id="listing-micropost-validations" data-type="listing">
<h5><span class="title-label">代码清单 11.7</span>:微博模型的验证 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/micropost.rb</div>
<div class="highlight language-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="ss">:user</span>
<span class="n">validates</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">140</span> <span class="p">}</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>现在,测试组件应该能通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.8</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
<section data-type="sect2" id="user-micropost-associations">
<h2><span class="title-label">11.1.3</span> 用户和微博之间的关联</h2>
<p>为 Web 应用构建数据模型时,最基本的要求是要能够在不同的模型之间建立关联。在这个应用中,每篇微博都属于某个用户,而每个用户一般都有多篇微博。用户和微博之间的关系在 <a href="chapter2.html#a-user-has-many-microposts">2.3.3 节</a>简单介绍过,如<a href="#fig-micropost-belongs-to-user">图 11.2</a> 和<a href="#fig-user-has-many-microposts">图 11.3</a> 所示。在实现这种关联的过程中,我们会为微博模型和用户模型编写一些测试。</p>
<div id="fig-micropost-belongs-to-user" class="figure"><img src="images/chapter11/micropost_belongs_to_user.png" alt="micropost belongs to user" /><div class="figcaption"><span class="title-label">图 11.2</span>:微博和所属用户之间的 <code>belongs_to</code>(属于)关系</div></div>
<div id="fig-user-has-many-microposts" class="figure"><img src="images/chapter11/user_has_many_microposts.png" alt="user has many microposts" /><div class="figcaption"><span class="title-label">图 11.3</span>:用户和微博之间的 <code>has_many</code>(拥有多个)关系</div></div>
<p>使用本节实现的 <code>belongs_to</code>/<code>has_many</code> 关联之后,Rails 会自动创建一些方法,如<a href="#table-association-methods">表 11.1</a> 所示。注意,从表中可知,相较于下面的方法</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="no">Micropost</span><span class="o">.</span><span class="n">create</span>
<span class="no">Micropost</span><span class="o">.</span><span class="n">create!</span>
<span class="no">Micropost</span><span class="o">.</span><span class="n">new</span>
</pre></div>
</div>
<p>我们得到了</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">create</span>
<span class="n">user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">create!</span>
<span class="n">user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">build</span>
</pre></div>
</div>
<p>后者才是创建微博的正确方式,即通过相关联的用户对象创建。通过这种方式创建的微博,其 <code>user_id</code> 属性会自动设为正确的值。所以,我们可以把<a href="#listing-micropost-validity-test">代码清单 11.2</a> 中的下述代码</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="c1"># 这行代码不符合常见做法</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="no">Micropost</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="vi">@user</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</pre></div>
</div>
<p>改为</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">build</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span><span class="p">)</span>
</pre></div>
</div>
<p>(和 <code>new</code> 方法一样,<code>build</code> 方法返回一个存储在内存中的对象,不会修改数据库。)只要关联定义的正确,<code>@micropost</code> 变量的 <code>user_id</code> 属性就会自动设为所关联用户的 ID。</p>
<table id="table-association-methods" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 11.1</span>:用户和微博之间建立关联后得到的方法简介</caption>
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">方法</th>
<th class="tableblock halign-left valign-top">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>micropost.user</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">返回和微博关联的用户对象</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.microposts</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">返回用户发布的所有微博</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.microposts.create(arg)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建一篇 <code>user</code> 发布的微博</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.microposts.create!(arg)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建一篇 <code>user</code> 发布的微博(失败时抛出异常)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.microposts.build(arg)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">返回一个 <code>user</code> 发布的新微博对象</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.microposts.find_by(id: 1)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">查找 <code>user</code> 发布的一篇微博,而且微博的 ID 为 1</p></td>
</tr>
</tbody>
</table>
<p>为了让 <code>@user.microposts.build</code> 这样的代码能使用,我们要修改用户模型和微博模型,添加一些代码,把这两个模型关联起来。<a href="#listing-micropost-migration">代码清单 11.1</a> 中的迁移已经自动添加了 <code>belongs_to :user</code>,如<a href="#listing-micropost-belongs-to-user">代码清单 11.9</a> 所示。关联的另一头,<code>has_many :microposts</code>,我们要自己动手添加,如<a href="#listing-user-has-many-microposts">代码清单 11.10</a> 所示。</p>
<div id="listing-micropost-belongs-to-user" data-type="listing">
<h5><span class="title-label">代码清单 11.9</span>:一篇微博属于(<code>belongs_to</code>)一个用户 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/micropost.rb</div>
<div class="highlight language-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="hll"> <span class="n">belongs_to</span> <span class="ss">:user</span>
</span> <span class="n">validates</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
<span class="n">validates</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">140</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<div id="listing-user-has-many-microposts" data-type="listing">
<h5><span class="title-label">代码清单 11.10</span>:一个用户有多篇(<code>has_many</code>)微博 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-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="hll"> <span class="n">has_many</span> <span class="ss">:microposts</span>
</span> <span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>定义好关联后,我们可以修改<a href="#listing-micropost-validity-test">代码清单 11.2</a> 中的 <code>setup</code> 方法了,使用正确的方式创建一个微博对象,如<a href="#listing-micropost-validity-test-idiomatic">代码清单 11.11</a> 所示。</p>
<div id="listing-micropost-validity-test-idiomatic" data-type="listing">
<h5><span class="title-label">代码清单 11.11</span>:使用正确的方式创建微博对象 <span class="green">GREEN</span></h5>
<div class="source-file">test/models/micropost_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">MicropostTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="hll"> <span class="vi">@micropost</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">build</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"user id should be present"</span> <span class="k">do</span>
<span class="vi">@micropost</span><span class="o">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@micropost</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>当然,经过这次简单的重构后测试组件应该还能通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.12</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
<section data-type="sect2" id="micropost-refinements">
<h2><span class="title-label">11.1.4</span> 改进微博模型</h2>
<p>本节,我们要改进一下用户和微博之间的关联:按照特定的顺序取回用户的微博,并且让微博依属于用户,如果用户注销了,就自动删除这个用户发布的所有微博。</p>
<section data-type="sect3" id="default-scope">
<h3>默认作用域</h3>
<p>默认情况下,<code>user.microposts</code> 不能确保微博的顺序,但是按照博客和 Twitter 的习惯,我们希望微博按照创建时间倒序排列,也就是最新发布的微博在前面。<sup>[<a id="fn-ref-1" href="#fn-1">1</a>]</sup>为此,我们要使用“默认作用域”(default scope)。</p>
<p>这样的功能很容易让测试意外通过(就算应用代码不对,测试也能通过),所以我们要使用测试驱动开发技术,确保实现的方式是正确的。首先,我们编写一个测试,检查数据库中的第一篇微博和微博固件中名为 <code>most_recent</code> 的微博相同,如<a href="#listing-micropost-order-test">代码清单 11.13</a> 所示。</p>
<div id="listing-micropost-order-test" data-type="listing">
<h5><span class="title-label">代码清单 11.13</span>:测试微博的排序 <span class="red">RED</span></h5>
<div class="source-file">test/models/micropost_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">MicropostTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"order should be most recent first"</span> <span class="k">do</span>
<span class="n">assert_equal</span> <span class="no">Micropost</span><span class="o">.</span><span class="n">first</span><span class="p">,</span> <span class="n">microposts</span><span class="p">(</span><span class="ss">:most_recent</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>这段代码要使用微博固件,所以我们要定义固件,如<a href="#listing-micropost-fixtures">代码清单 11.14</a> 所示。</p>
<div id="listing-micropost-fixtures" data-type="listing">
<h5><span class="title-label">代码清单 11.14</span>:微博固件</h5>
<div class="source-file">test/fixtures/microposts.yml</div>
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain">orange</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"I</span><span class="nv"> </span><span class="s">just</span><span class="nv"> </span><span class="s">ate</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">orange!"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 10.minutes.ago %></span>
<span class="l-Scalar-Plain">tau_manifesto</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"Check</span><span class="nv"> </span><span class="s">out</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">@tauday</span><span class="nv"> </span><span class="s">site</span><span class="nv"> </span><span class="s">by</span><span class="nv"> </span><span class="s">@mhartl:</span><span class="nv"> </span><span class="s">http://tauday.com"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 3.years.ago %></span>
<span class="l-Scalar-Plain">cat_video</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"Sad</span><span class="nv"> </span><span class="s">cats</span><span class="nv"> </span><span class="s">are</span><span class="nv"> </span><span class="s">sad:</span><span class="nv"> </span><span class="s">http://youtu.be/PKffm2uI4dk"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 2.hours.ago %></span>
<span class="l-Scalar-Plain">most_recent</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"Writing</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">short</span><span class="nv"> </span><span class="s">test"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= Time.zone.now %></span>
</pre></div>
</div>
<p>注意,我们使用嵌入式 Ruby 明确设置了 <code>created_at</code> 属性的值。因为这个属性由 Rails 自动更新,一般无法手动设置,但在固件中可以这么做。实际上可能不用自己设置这些属性,因为在某些系统中固件会按照定义的顺序创建。在这个文件中,最后一个固件最后创建(因此是最新的一篇微博)。但是绝不要依赖这种行为,因为并不可靠,而且在不同的系统中有差异。</p>
<p>现在,测试应该无法通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.15</span>:<strong class="red">RED</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test </span><span class="nv">TEST</span><span class="o">=</span><span class="nb">test</span>/models/micropost_test.rb <span class="se">\</span>
> <span class="nv">TESTOPTS</span><span class="o">=</span><span class="s2">"--name test_order_should_be_most_recent_first"</span>
</pre></div>
</div>
<p>我们要使用 Rails 提供的 <code>default_scope</code> 方法让测试通过。这个方法的作用很多,这里我们要用它设定从数据库中读取数据的默认顺序。为了得到特定的顺序,我们要在 <code>default_scope</code> 方法中指定 <code>order</code> 参数,按 <code>created_at</code> 列的值排序,如下所示:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">order</span><span class="p">(</span><span class="ss">:created_at</span><span class="p">)</span>
</pre></div>
</div>
<p>可是,这实现的是“升序”,从小到大排列,即最早发布的微博排在最前面。为了让微博降序排列,我们要向下走一层,使用纯 SQL 语句:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">order</span><span class="p">(</span><span class="s1">'created_at DESC'</span><span class="p">)</span>
</pre></div>
</div>
<p>在 SQL 中,<code>DESC</code> 表示“降序”,即新发布的微博在前面。在以前的 Rails 版本中,必须使用纯 SQL 语句才能实现这个需求,但从 Rails 4.0 起,可以使用纯 Ruby 句法实现:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">order</span><span class="p">(</span><span class="ss">created_at</span><span class="p">:</span> <span class="ss">:desc</span><span class="p">)</span>
</pre></div>
</div>
<p>把默认作用域加入微博模型,如<a href="#listing-micropost-ordering">代码清单 11.16</a> 所示。</p>
<div id="listing-micropost-ordering" data-type="listing">
<h5><span class="title-label">代码清单 11.16</span>:使用 <code>default_scope</code> 排序微博 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/micropost.rb</div>
<div class="highlight language-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="ss">:user</span>
<span class="hll"> <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="ss">created_at</span><span class="p">:</span> <span class="ss">:desc</span><span class="p">)</span> <span class="p">}</span>
</span> <span class="n">validates</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
<span class="n">validates</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">140</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p><a href="#listing-micropost-ordering">代码清单 11.16</a> 中使用了“箭头”句法,表示一种对象,叫 Proc(procedure)或 lambda,即“匿名函数”(没有名字的函数)。<code>-></code> 接受一个代码块(<a href="chapter4.html#blocks">4.3.2 节</a>),返回一个 Proc。然后在这个 Proc 上调用 <code>call</code> 方法执行其中的代码。我们可以在控制台中看一下怎么使用 Proc:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="o">-></span> <span class="p">{</span> <span class="nb">puts</span> <span class="s2">"foo"</span> <span class="p">}</span>
<span class="go">=> #<Proc:0x007fab938d0108@(irb):1 (lambda)></span>
<span class="gp">>> </span><span class="o">-></span> <span class="p">{</span> <span class="nb">puts</span> <span class="s2">"foo"</span> <span class="p">}</span><span class="o">.</span><span class="n">call</span>
<span class="go">foo</span>
<span class="go">=> nil</span>
</pre></div>
</div>
<p>(Proc 是高级 Ruby 知识,如果现在不理解也不用担心。)</p>
<p>按照<a href="#listing-micropost-ordering">代码清单 11.16</a> 修改后,测试应该可以通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.17</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
<section data-type="sect3" id="dependent-destroy">
<h3>依属关系:destroy</h3>
<p>除了设定恰当的顺序外,我们还要对微博模型做一项改进。我们在 <a href="chapter9.html#deleting-users">9.4 节</a>介绍过,管理员有删除用户的权限。那么,在删除用户的同时,有必要把该用户发布的微博也删除。</p>
<p>为此,我们可以把一个参数传给 <code>has_many</code> 关联方法,如<a href="#listing-micropost-dependency">代码清单 11.18</a> 所示。</p>
<div id="listing-micropost-dependency" data-type="listing">
<h5><span class="title-label">代码清单 11.18</span>:确保用户的微博在删除用户的同时也被删除</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-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="hll"> <span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
</span> <span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p><code>dependent: :destroy</code> 的作用是在用户被删除的时候,把这个用户发布的微博也删除。这么一来,如果管理员删除了用户,数据库中就不会出现无主的微博了。</p>
<p>我们可以为用户模型编写一个测试,证明<a href="#listing-micropost-dependency">代码清单 11.18</a> 中的代码是正确的。我们要保存一个用户(因此得到了用户的 ID),再创建一个属于这个用户的微博,然后检查删除用户后微博的数量有没有减少一个,如<a href="#listing-dependent-destroy-test">代码清单 11.19</a> 所示。(和<a href="chapter9.html#listing-delete-link-integration-test">代码清单 9.57</a> 中“删除”链接的集成测试对比一下。)</p>
<div id="listing-dependent-destroy-test" data-type="listing">
<h5><span class="title-label">代码清单 11.19</span>:测试 <code>dependent: :destroy</code> <span class="green">GREEN</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">password</span><span class="p">:</span> <span class="s2">"foobar"</span><span class="p">,</span> <span class="ss">password_confirmation</span><span class="p">:</span> <span class="s2">"foobar"</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"associated microposts should be destroyed"</span> <span class="k">do</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">save</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span><span class="p">)</span>
<span class="n">assert_difference</span> <span class="s1">'Micropost.count'</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span> <span class="k">do</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">destroy</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>如果<a href="#listing-micropost-dependency">代码清单 11.18</a> 正确,测试组件就应该能通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.20</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
</section>
</section>
<section data-type="sect1" id="showing-microposts">
<h1><span class="title-label">11.2</span> 显示微博</h1>
<p>尽管我们还没实现直接在网页中发布微博的功能(将在 <a href="#creating-microposts">11.3.2 节</a>实现),不过还是有办法显示微博,并对显示的内容进行测试。我们将按照 Twitter 的方式,不在微博资源的 <code>index</code> 页面显示用户的微博,而在用户资源的 <code>show</code> 页面显示,构思图如<a href="#fig-user-microposts-mockup">图 11.4</a> 所示。我们会先使用一些简单的 ERb 代码,在用户的资料页面显示微博,然后在 <a href="chapter9.html#sample-users">9.3.2 节</a>的种子数据中添加一些微博,这样才有内容可以显示。</p>
<div id="fig-user-microposts-mockup" class="figure"><img src="images/chapter11/user_microposts_mockup_3rd_edition.png" alt="user microposts mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 11.4</span>:显示有微博的资料页面构思图</div></div>
<section data-type="sect2" id="rendering-microposts">
<h2><span class="title-label">11.2.1</span> 渲染微博</h2>
<p>我们计划在用户的资料页面(<code>show.html.erb</code>)显示用户的微博,还要显示用户发布了多少篇微博。你会发现,很多做法和 <a href="chapter9.html#showing-all-users">9.3 节</a>列出所有用户时类似。</p>
<p>虽然 <a href="#manipulating-microposts">11.3 节</a>才会用到微博控制器,但马上就需要使用视图,所以现在就要生成控制器:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate controller Microposts
</pre></div>
</div>
<p>这一节的主要目的是渲染用户发布的所有微博。<a href="chapter9.html#partial-refactoring">9.3.5 节</a>用过这样的代码:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="x"><ul class="users"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@users</span> <span class="cp">%></span><span class="x"></span>
<span class="x"></ul></span>
</pre></div>
</div>
<p>这段代码会自动使用局部视图 <code>_user.html.erb</code> 渲染 <code>@users</code> 变量中的每个用户。同样地,我们要编写 <code>_micropost.html.erb</code> 局部视图,使用类似的方式渲染微博集合:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="x"><ol class="microposts"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@microposts</span> <span class="cp">%></span><span class="x"></span>
<span class="x"></ol></span>
</pre></div>
</div>
<p>注意,我们使用的是有序列表标签 <code>ol</code>(而不是无需列表 <code>ul</code>),因为微博是按照一定顺序显示的(按时间倒序)。相应的局部视图如<a href="#listing-micropost-partial">代码清单 11.21</a> 所示。</p>
<div id="listing-micropost-partial" data-type="listing">
<h5><span class="title-label">代码清单 11.21</span>:渲染单篇微博的局部视图</h5>
<div class="source-file">app/views/microposts/_micropost.html.erb</div>
<div class="highlight language-erb"><pre><span class="x"><li id="micropost-</span><span class="cp"><%=</span> <span class="n">micropost</span><span class="o">.</span><span class="n">id</span> <span class="cp">%></span><span class="x">"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="n">gravatar_for</span><span class="p">(</span><span class="n">micropost</span><span class="o">.</span><span class="n">user</span><span class="p">,</span> <span class="ss">size</span><span class="p">:</span> <span class="mi">50</span><span class="p">),</span> <span class="n">micropost</span><span class="o">.</span><span class="n">user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <span class="user"></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="n">micropost</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">micropost</span><span class="o">.</span><span class="n">user</span> <span class="cp">%></span><span class="x"></span></span>
<span class="x"> <span class="content"></span><span class="cp"><%=</span> <span class="n">micropost</span><span class="o">.</span><span class="n">content</span> <span class="cp">%></span><span class="x"></span></span>
<span class="x"> <span class="timestamp"></span>
<span class="x"> Posted </span><span class="cp"><%=</span> <span class="n">time_ago_in_words</span><span class="p">(</span><span class="n">micropost</span><span class="o">.</span><span class="n">created_at</span><span class="p">)</span> <span class="cp">%></span><span class="x"> ago.</span>
<span class="x"> </span></span>
<span class="x"></li></span>
</pre></div>
</div>
<p>这个局部视图使用了 <code>time_ago_in_words</code> 辅助方法,这个方法的作用应该很明显,效果会在 <a href="#sample-microposts">11.2.2 节</a>看到。<a href="#listing-micropost-partial">代码清单 11.21</a> 还为每篇微博指定了 CSS ID:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="x"><li id="micropost-</span><span class="cp"><%=</span> <span class="n">micropost</span><span class="o">.</span><span class="n">id</span> <span class="cp">%></span><span class="x">"></span>
</pre></div>
</div>
<p>这是好习惯,说不定以后要处理(例如使用 JavaScript)单篇微博呢。</p>
<p>接下来要解决显示大量微博的问题。我们可以使用 <a href="chapter9.html#pagination">9.3.3 节</a>显示大量用户的方法来解决这个问题,即使用分页。和前面一样,我们要使用 <code>will_paginate</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="vi">@microposts</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>如果和用户列表页面的代码(<a href="chapter9.html#listing-will-paginate-index-view">代码清单 9.41</a>)比较的话,会发现之前使用的代码是:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>前面之所以可以直接调用,是因为在用户控制器中,<code>will_paginate</code> 假定有一个名为 <code>@users</code> 的实例变量(<a href="chapter9.html#pagination">9.3.3 节</a>说过,这个变量所属的类应该是 <code>AvtiveRecord::Relation</code>)。现在,因为还在用户控制器中,但是我们要分页显示微博,所以必须明确地把 <code>@microposts</code> 变量传给 <code>will_paginate</code> 方法。当然了,我们还要在 <code>show</code> 动作中定义 <code>@microposts</code> 变量,如<a href="#listing-user-show-microposts-instance">代码清单 11.22</a> 所示。</p>
<div id="listing-user-show-microposts-instance" data-type="listing">
<h5><span class="title-label">代码清单 11.22</span>:在用户控制器的 <code>show</code> 动作中定义 <code>@microposts</code> 变量</h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="hll"> <span class="vi">@microposts</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">paginate</span><span class="p">(</span><span class="ss">page</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:page</span><span class="o">]</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意看 <code>paginate</code> 方法是多么智能,甚至可以在关联上使用,从 <code>microposts</code> 表中取出每一页要显示的微博。</p>
<p>最后,还要显示用户发布的微博数量。我们可以使用 <code>count</code> 方法实现:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">count</span>
</pre></div>
</div>
<p>和 <code>paginate</code> 方法一样,<code>count</code> 方法也可以在关联上使用。<code>count</code> 的计数过程不是把所有微博都从数据库中读取出来,然后再在所得的数组上调用 <code>length</code> 方法,如果这样做的话,微博数量一旦很多,效率就会降低。其实,<code>count</code> 方法直接在数据库层计算,让数据库统计指定的 <code>user_id</code> 拥有多少微博。(所有数据库都会对这种操作做性能优化。如果统计数量仍然是应用的性能瓶颈,可以使用“<a href="http://railscasts.com/episodes/23-counter-cache-column">计数缓存</a>”进一步提速。)</p>
<p>综上所述,现在可以把微博添加到资料页面了,如<a href="#listing-user-show-microposts">代码清单 11.23</a> 所示。注意,<code>if @user.microposts.any?</code>(在<a href="chapter7.html#listing-errors-partial">代码清单 7.19</a> 中见过类似的用法)的作用是,如果用户没有发布微博,不显示一个空列表。</p>
<div id="listing-user-show-microposts" data-type="listing">
<h5><span class="title-label">代码清单 11.23</span>:在用户资料页面中加入微博</h5>
<div class="source-file">app/views/users/show.html.erb</div>
<div class="highlight language-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="o">.</span><span class="n">name</span><span class="p">)</span> <span class="cp">%></span><span class="x"></span>
<span class="x"><div class="row"></span>
<span class="x"> <aside class="col-md-4"></span>
<span class="x"> <section class="user_info"></span>
<span class="x"> <h1></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </h1></span>
<span class="x"> </section></span>
<span class="x"> </aside></span>
<span class="hll"><span class="x"> <div class="col-md-8"></span>
</span><span class="hll"><span class="x"> </span><span class="cp"><%</span> <span class="k">if</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">any?</span> <span class="cp">%></span><span class="x"></span>
</span><span class="hll"><span class="x"> <h3>Microposts (</span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">count</span> <span class="cp">%></span><span class="x">)</h3></span>
</span><span class="hll"><span class="x"> <ol class="microposts"></span>
</span><span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@microposts</span> <span class="cp">%></span><span class="x"></span>
</span><span class="hll"><span class="x"> </ol></span>
</span><span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="vi">@microposts</span> <span class="cp">%></span><span class="x"></span>
</span><span class="hll"><span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</span><span class="hll"><span class="x"> </div></span>
</span><span class="x"></div></span>
</pre></div>
</div>
<p>现在,我们可以查看一下修改后的用户资料页面,如<a href="#fig-user-profile-no-microposts">图 11.5</a>。可能会出乎你的意料,不过也是理所当然的,因为现在还没有微博。下面我们就来改变这种状况。</p>
<div id="fig-user-profile-no-microposts" class="figure"><img src="images/chapter11/user_profile_no_microposts_3rd_edition.png" alt="user profile no microposts 3rd edition" /><div class="figcaption"><span class="title-label">图 11.5</span>:添加显示微博的代码后用户的资料页面,但没有微博</div></div>
</section>
<section data-type="sect2" id="sample-microposts">
<h2><span class="title-label">11.2.2</span> 示例微博</h2>
<p>在 <a href="#rendering-microposts">11.2.1 节</a>,为了显示用户的微博,创建或修改了几个模板,但是结果有点不给力。为了改变这种状况,我们要在 <a href="chapter9.html#sample-users">9.3.2 节</a>用到的种子数据中加入一些微博。</p>
<p>为所有用户添加示例微博要花很长时间,所以我们决定只为前六个用户添加。为此,要使用 <code>take</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="no">User</span><span class="o">.</span><span class="n">order</span><span class="p">(</span><span class="ss">:created_at</span><span class="p">)</span><span class="o">.</span><span class="n">take</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span>
</pre></div>
</div>
<p>调用 <code>order</code> 方法的作用是按照创建用户的顺序查找六个用户。</p>
<p>我们要分别为这六个用户创建 50 篇微博(数量要多于 30 个才能分页)。为了生成微博的内容,我们要使用 Faker 提供的 <a href="http://rubydoc.info/gems/faker/1.3.0/Faker/Lorem"><code>Lorem.sentence</code></a> 方法。<sup>[<a id="fn-ref-2" href="#fn-2">2</a>]</sup>添加示例微博后的种子数据如<a href="#listing-sample-microposts">代码清单 11.24</a> 所示。</p>
<div id="listing-sample-microposts" data-type="listing">
<h5><span class="title-label">代码清单 11.24</span>:添加示例微博</h5>
<div class="source-file">db/seeds.rb</div>
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain">.</span>
<span class="l-Scalar-Plain">.</span>
<span class="l-Scalar-Plain">.</span>
<span class="l-Scalar-Plain">users = User.order(:created_at).take(6)</span>
<span class="l-Scalar-Plain">50.times do</span>
<span class="l-Scalar-Plain">content = Faker::Lorem.sentence(5)</span>
<span class="l-Scalar-Plain">users.each { |user| user.microposts.create!(content</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">content) }</span>
<span class="l-Scalar-Plain">end</span>
</pre></div>
</div>
<p>然后,像之前一样重新把种子数据写入开发数据库:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate:reset
<span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:seed
</pre></div>
</div>
<p>完成后还要重启 Rails 开发服务器。</p>
<p>现在,我们能看到 <a href="#rendering-microposts">11.2.1 节</a>的劳动成果了——用户资料页面显示了微博。<sup>[<a id="fn-ref-3" href="#fn-3">3</a>]</sup>初步结果如<a href="#fig-user-profile-microposts-no-styling">图 11.6</a> 所示。</p>
<div id="fig-user-profile-microposts-no-styling" class="figure"><img src="images/chapter11/user_profile_microposts_no_styling_3rd_edition.png" alt="user profile microposts no styling 3rd edition" /><div class="figcaption"><span class="title-label">图 11.6</span>:用户资料页面显示的微博,还没添加样式</div></div>
<p><a href="#fig-user-profile-microposts-no-styling">图 11.6</a> 中显示的微博还没有样式,那我们就加入一些样式,如<a href="#listing-micropost-css">代码清单 11.25</a> 所示,<sup>[<a id="fn-ref-4" href="#fn-4">4</a>]</sup>然后再看一下页面显示的效果。</p>
<div id="listing-micropost-css" data-type="listing">
<h5><span class="title-label">代码清单 11.25</span>:微博的样式(包含本章要使用的所有 CSS)</h5>
<div class="source-file">app/assets/stylesheets/custom.css.scss</div>
<div class="highlight language-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="na">list-style</span><span class="o">:</span> <span class="no">none</span><span class="p">;</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="nt">li</span> <span class="p">{</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span> <span class="mi">0</span><span class="p">;</span>
<span class="na">border-top</span><span class="o">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="no">solid</span> <span class="mh">#e8e8e8</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.user</span> <span class="p">{</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">5</span><span class="kt">em</span><span class="p">;</span>
<span class="na">padding-top</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.content</span> <span class="p">{</span>
<span class="na">display</span><span class="o">:</span> <span class="no">block</span><span class="p">;</span>
<span class="na">margin-left</span><span class="o">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span>
<span class="nt">img</span> <span class="p">{</span>
<span class="na">display</span><span class="o">:</span> <span class="no">block</span><span class="p">;</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">5</span><span class="kt">px</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nc">.timestamp</span> <span class="p">{</span>
<span class="na">color</span><span class="o">:</span> <span class="nv">$gray-light</span><span class="p">;</span>
<span class="na">display</span><span class="o">:</span> <span class="no">block</span><span class="p">;</span>
<span class="na">margin-left</span><span class="o">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.gravatar</span> <span class="p">{</span>
<span class="na">float</span><span class="o">:</span> <span class="no">left</span><span class="p">;</span>
<span class="na">margin-right</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</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="na">height</span><span class="o">:</span> <span class="mi">100</span><span class="kt">px</span><span class="p">;</span>
<span class="na">margin-bottom</span><span class="o">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nt">span</span><span class="nc">.picture</span> <span class="p">{</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
<span class="nt">input</span> <span class="p">{</span>
<span class="na">border</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
</div>
<p><a href="#fig-user-profile-with-microposts">图 11.7</a> 是第一个用户的资料页面,<a href="#fig-other-profile-with-microposts">图 11.8</a> 是另一个用户的资料页面,<a href="#fig-user-profile-microposts">图 11.9</a> 是第一个用户资料页面的第 2 页,页面底部还显示了分页链接。注意观察这三幅图,可以看到,微博后面显示了距离发布的时间(例如,“Posted 1 minute ago.”),这就是<a href="#listing-micropost-partial">代码清单 11.21</a> 中 <code>time_ago_in_words</code> 方法实现的效果。过一会再刷新页面,这些文字会根据当前时间自动更新。</p>
<div id="fig-user-profile-with-microposts" class="figure"><img src="images/chapter11/user_profile_with_microposts_3rd_edition.png" alt="user profile with microposts 3rd edition" /><div class="figcaption"><span class="title-label">图 11.7</span>:显示有微博的用户资料页面(<a href="http://localhost:3000/users/1">/users/1</a>)</div></div>
<div id="fig-other-profile-with-microposts" class="figure"><img src="images/chapter11/other_profile_with_microposts_3rd_edition.png" alt="other profile with microposts 3rd edition" /><div class="figcaption"><span class="title-label">图 11.8</span>:另一个用户的资料页面(<a href="http://localhost:3000/users/5">/users/5</a>),也显示有微博</div></div>
<div id="fig-user-profile-microposts" class="figure"><img src="images/chapter11/user_profile_microposts_page_2_3rd_edition.png" alt="user profile microposts page 2 3rd edition" /><div class="figcaption"><span class="title-label">图 11.9</span>:微博分页链接(<a href="http://localhost:3000/users/1?page=2">/users/1?page=2</a>)</div></div>
</section>
<section data-type="sect2" id="profile-micropost-tests">
<h2><span class="title-label">11.2.3</span> 资料页面中微博的测试</h2>
<p>新激活的用户会重定向到资料页面,那时已经测试了资料页面是否能正确渲染(<a href="chapter10.html#listing-signup-with-account-activation-test">代码清单 10.31</a>)。本节,我们要编写几个简短的集成测试,检查资料页面中的其他内容。首先,生成资料页面的集成测试文件:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate integration_test users_profile
invoke test_unit
create <span class="nb">test</span>/integration/users_profile_test.rb
</pre></div>
</div>
<p>为了测试资料页面中显示有微博,我们要把微博固件和用户关联起来。Rails 提供了一种便利的方法,可以在固件中建立关联,例如:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain">orange</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"I</span><span class="nv"> </span><span class="s">just</span><span class="nv"> </span><span class="s">ate</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">orange!"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 10.minutes.ago %></span>
<span class="hll"> <span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</span></pre></div>
</div>
<p>把 <code>user</code> 的值设为 <code>michael</code> 后,Rails 会把这篇微博和指定的用户固件关联起来:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><span class="hll"><span class="l-Scalar-Plain">michael</span><span class="p-Indicator">:</span>
</span> <span class="l-Scalar-Plain">name</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">Michael Example</span>
<span class="l-Scalar-Plain">email</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">[email protected]</span>
<span class="l-Scalar-Plain">.</span>
<span class="l-Scalar-Plain">.</span>
<span class="l-Scalar-Plain">.</span>
</pre></div>
</div>
<p>为了测试微博分页,我们要使用<a href="chapter9.html#listing-users-fixtures-extra-users">代码清单 9.43</a> 中用到的方法,通过嵌入式 Ruby 代码多生成一些微博固件:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain"><% 30.times do |n| %></span>
<span class="l-Scalar-Plain">micropost_<%= n %></span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= Faker::Lorem.sentence(5) %></span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 42.days.ago %></span>
<span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
<span class="l-Scalar-Plain"><% end %></span>
</pre></div>
</div>
<p>综上,修改后的微博固件如<a href="#listing-updated-micropost-fixtures">代码清单 11.26</a> 所示。</p>
<div id="listing-updated-micropost-fixtures" data-type="listing">
<h5><span class="title-label">代码清单 11.26</span>:添加关联用户后的微博固件</h5>
<div class="source-file">test/fixtures/microposts.yml</div>
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain">orange</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"I</span><span class="nv"> </span><span class="s">just</span><span class="nv"> </span><span class="s">ate</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">orange!"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 10.minutes.ago %></span>
<span class="hll"> <span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</span>
<span class="l-Scalar-Plain">tau_manifesto</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"Check</span><span class="nv"> </span><span class="s">out</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">@tauday</span><span class="nv"> </span><span class="s">site</span><span class="nv"> </span><span class="s">by</span><span class="nv"> </span><span class="s">@mhartl:</span><span class="nv"> </span><span class="s">http://tauday.com"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 3.years.ago %></span>
<span class="hll"> <span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</span>
<span class="l-Scalar-Plain">cat_video</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"Sad</span><span class="nv"> </span><span class="s">cats</span><span class="nv"> </span><span class="s">are</span><span class="nv"> </span><span class="s">sad:</span><span class="nv"> </span><span class="s">http://youtu.be/PKffm2uI4dk"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 2.hours.ago %></span>
<span class="hll"> <span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</span>
<span class="l-Scalar-Plain">most_recent</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"Writing</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">short</span><span class="nv"> </span><span class="s">test"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= Time.zone.now %></span>
<span class="hll"> <span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</span>
<span class="hll"><span class="l-Scalar-Plain"><% 30.times do |n| %></span>
</span><span class="hll"><span class="l-Scalar-Plain">micropost_<%= n %></span><span class="p-Indicator">:</span>
</span><span class="hll"> <span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= Faker::Lorem.sentence(5) %></span>
</span><span class="hll"> <span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 42.days.ago %></span>
</span><span class="hll"> <span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</span><span class="hll"><span class="l-Scalar-Plain"><% end %></span>
</span></pre></div>
</div>
<p>测试数据准备好了,测试本身也很简单:访问资料页面,检查页面的标题、用户的名字、Gravatar 头像、微博数量和分页显示的微博,如<a href="#listing-user-profile-test">代码清单 11.27</a> 所示。注意,为了使用<a href="chapter4.html#listing-title-helper">代码清单 4.2</a> 中的 <code>full_title</code> 辅助方法测试页面的标题,我们要把 <code>ApplicationHelper</code> 模块引入测试。<sup>[<a id="fn-ref-5" href="#fn-5">5</a>]</sup></p>
<div id="listing-user-profile-test" data-type="listing">
<h5><span class="title-label">代码清单 11.27</span>:用户资料页面的测试 <span class="green">GREEN</span></h5>
<div class="source-file">test/integration/users_profile_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UsersProfileTest</span> <span class="o"><</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">IntegrationTest</span>
<span class="hll"> <span class="kp">include</span> <span class="no">ApplicationHelper</span>
</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"profile display"</span> <span class="k">do</span>
<span class="n">get</span> <span class="n">user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>
<span class="n">assert_template</span> <span class="s1">'users/show'</span>
<span class="n">assert_select</span> <span class="s1">'title'</span><span class="p">,</span> <span class="n">full_title</span><span class="p">(</span><span class="vi">@user</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
<span class="n">assert_select</span> <span class="s1">'h1'</span><span class="p">,</span> <span class="ss">text</span><span class="p">:</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span>
<span class="n">assert_select</span> <span class="s1">'h1>img.gravatar'</span>
<span class="n">assert_match</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">count</span><span class="o">.</span><span class="n">to_s</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">body</span>
<span class="n">assert_select</span> <span class="s1">'div.pagination'</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">paginate</span><span class="p">(</span><span class="ss">page</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">micropost</span><span class="o">|</span>
<span class="n">assert_match</span> <span class="n">micropost</span><span class="o">.</span><span class="n">content</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">body</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>检查微博数量时用到了 <code>response.body</code>,<a href="chapter10.html#account-activation-and-password-reset-exercises">第 10 章的练习</a>中见过。别被名字迷惑了,其实 <code>response.body</code> 的值是整个页面的 HTML 源码(不只是 <code>body</code> 元素中的内容)。如果我们只关心页面中某处显示的微博数量,使用下面的断言找到匹配的内容即可:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">assert_match</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">count</span><span class="o">.</span><span class="n">to_s</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">body</span>
</pre></div>
</div>
<p><code>assert_match</code> 没有 <code>assert_select</code> 的针对性强,无需指定要查找哪个 HTML 标签。</p>
<p><a href="#listing-user-profile-test">代码清单 11.27</a> 还在 <code>assert_select</code> 中使用了嵌套式句法:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">assert_select</span> <span class="s1">'h1>img.gravatar'</span>
</pre></div>
</div>
<p>这行代码的意思是,在 <code>h1</code> 标签中查找类为 <code>gravatar</code> 的 <code>img</code> 标签。</p>
<p>因为应用能正常运行,所以测试组件应该也能通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 11.28</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
</section>
<section data-type="sect1" id="manipulating-microposts">
<h1><span class="title-label">11.3</span> 微博相关的操作</h1>
<p>微博的数据模型构建好了,也编写了相关的视图文件,接下来我们的开发重点是,通过网页发布微博。本节,我们会初步实现动态流,<a href="chapter12.html#following-users">第 12 章</a>再完善。最后,和用户资源一样,我们还要实现在网页中删除微博的功能。</p>
<p>上述功能的实现和之前的方式有点不同,需要特别注意:微博资源相关的页面不通过微博控制器实现,而是通过资料页面和首页实现。因此微博控制器不需要 <code>new</code> 和 <code>edit</code> 动作,只需要 <code>create</code> 和 <code>destroy</code> 动作。所以,微博资源的路由如<a href="#listing-microposts-resource">代码清单 11.29</a> 所示。 <a href="#listing-microposts-resource">代码清单 11.29</a> 中的代码对应的 REST 路由如<a href="#table-restful-microposts">表 11.2</a> 所示,这张表中的路由只是<a href="chapter2.html#table-demo-restful-microposts">表 2.3</a> 的一部分。不过,路由虽然简化了,但预示着实现的过程需要用到更高级的技术,而不会降低代码的复杂度。从<a href="chapter2.html#a-toy-app">第 2 章</a>起我们就十分依赖脚手架,不过现在我们将舍弃脚手架的大部分功能。</p>
<div id="listing-microposts-resource" data-type="listing">
<h5><span class="title-label">代码清单 11.29</span>:微博资源的路由设置</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">routes</span><span class="o">.</span><span class="n">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'help'</span> <span class="o">=></span> <span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'about'</span> <span class="o">=></span> <span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'contact'</span> <span class="o">=></span> <span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'signup'</span> <span class="o">=></span> <span class="s1">'users#new'</span>
<span class="n">get</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#new'</span>
<span class="n">post</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#create'</span>
<span class="n">delete</span> <span class="s1">'logout'</span> <span class="o">=></span> <span class="s1">'sessions#destroy'</span>
<span class="n">resources</span> <span class="ss">:users</span>
<span class="n">resources</span> <span class="ss">:account_activations</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:edit</span><span class="o">]</span>
<span class="n">resources</span> <span class="ss">:password_resets</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="o">]</span>
<span class="hll"> <span class="n">resources</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="o">]</span>
</span><span class="k">end</span>
</pre></div>
</div>
<table id="table-restful-microposts" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 11.2</span>:<a href="#listing-microposts-resource">代码清单 11.29</a> 设置的微博资源路由</caption>
<colgroup>
<col style="width: 25%;" />
<col style="width: 25%;" />
<col style="width: 25%;" />
<col style="width: 25%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">HTTP 请求</th>
<th class="tableblock halign-left valign-top">URL</th>
<th class="tableblock halign-left valign-top">动作</th>
<th class="tableblock halign-left valign-top">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>POST</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/microposts</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>create</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建新微博</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>DELETE</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/microposts/1</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>destroy</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">删除 ID 为 1 的微博</p></td>
</tr>
</tbody>
</table>
<section data-type="sect2" id="access-control">
<h2><span class="title-label">11.3.1</span> 访问限制</h2>
<p>开发微博资源的第一步,我们要在微博控制器中实现访问限制:若想访问 <code>create</code> 和 <code>destroy</code> 动作,用户要先登录。</p>
<p>针对这个要求的测试和用户控制器中相应的测试类似(<a href="chapter9.html#listing-edit-update-redirect-tests">代码清单 9.17</a> 和<a href="chapter9.html#listing-action-tests-admin">代码清单 9.56</a>),我们要使用正确的请求类型访问这两个动作,然后确认微博的数量没有变化,而且会重定向到登录页面,如<a href="#listing-create-destroy-micropost-tests">代码清单 11.30</a> 所示。</p>
<div id="listing-create-destroy-micropost-tests" data-type="listing">
<h5><span class="title-label">代码清单 11.30</span>:微博控制器的访问限制测试 <span class="red">RED</span></h5>
<div class="source-file">test/controllers/microposts_controller_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">MicropostsControllerTest</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@micropost</span> <span class="o">=</span> <span class="n">microposts</span><span class="p">(</span><span class="ss">:orange</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should redirect create when not logged in"</span> <span class="k">do</span>
<span class="n">assert_no_difference</span> <span class="s1">'Micropost.count'</span> <span class="k">do</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">micropost</span><span class="p">:</span> <span class="p">{</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">"Lorem ipsum"</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">assert_redirected_to</span> <span class="n">login_url</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should redirect destroy when not logged in"</span> <span class="k">do</span>