woshidan's loose leaf

ぼんやり勉強しています

ActiveRecordで関連しているモデルに対してメソッドを定義する

Userと1対多関連の関係にあるCommentというモデルがあるとする。

あるUserに関連したコメントに対して user.commentes.some_method のようにスコープやメソッドを定義したいという場合があって、その場合以下のように書ける。

class User < ApplicationRecord
  has_many :comments do # has_many関連を定義した部分にブロックを渡すとuser.comments.shortest_commentのように書ける
    def shortest_comment
      self.order(Arel.sql('length(contents) asc')).first
    end
  end
end
class Comment < ApplicationRecord
  belongs_to :user

  def self.longest_comment # クラスメソッドでallを使うと呼び出された時点でのhas_many関連などの上にメソッドチェインをつなげることができる
    all.order(Arel.sql('length(contents) desc')).first
  end
end

確認用のテストコード

RSpec.describe Comment, type: :model do
  let(:user) { create(:user) }
  let!(:comment1) { create(:comment, contents: '' * 3, user: user)}
  let!(:comment2) { create(:comment, contents: '' * 5, user: user)}
  let!(:another_user_comment) { create(:comment, contents: '' * 7, user: create(:user)) }
  let!(:other_user_comment) { create(:comment, contents: '' * 1, user: create(:user)) }

  describe '.longest_comment' do
    it 'userのコメントの中で一番長いコメントが返ってくる' do
      expect(user.comments.longest_comment.contents).to eq comment2.contents
    end
  end

  describe '.shortest_comment' do
    it 'userのコメントの中で一番短いコメントが返ってくる' do
      expect(user.comments.shortest_comment.contents).to eq comment1.contents
    end
  end
end

参考

qiita.com

attributes_forでテスト用のハッシュを取得するときはFactoryに定義された属性しか取り出されない

下準備としてモデルのSchemeとFactoryの定義

# schema.rb
ActiveRecord::Schema.define(version: 2020_11_23_064724) do

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.integer "age"
    t.string "mail"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

end
# factories/user.rb
FactoryBot.define do
    factory :user do
        name { 'テストユーザー' }
        mail { 'test@example.com' }
        # age { 15 }
    end
end

テストでattributes_forを使ってみる

require 'rails_helper'

RSpec.describe UsersController do
  # attributes_forはおそらくコントローラのテストでパラメータの組み立てに使う
  describe 'attributes_forとbuild(:user).attributesの違い' do
    it 'attributes_forで取り出されるハッシュにはFactoryに定義した属性しか含まれない' do
      expect(attributes_for(:user)).to eq({:name=>"テストユーザー", :mail=>"test@example.com"})
      # ageやcreated_atなどの属性はFactoryに記述されていないのでattributes_forで取り出したハッシュには含まれない
    end

    it 'build(:user).attributesにはFactoryに定義した値以外も含まれる' do
      expect(build(:user).attributes).to eq({"id"=>nil, "name"=>"テストユーザー", "age"=>nil, "mail"=>"test@example.com", "created_at"=>nil, "updated_at"=>nil})
      # buildでインスタンスを作ってから、attributesメソッドで作るハッシュにはFactoryに定義されていないものもあわせて全属性が含まれている
    end
  end
end

参考

qiita.com

qiita.com

今週のGo

週5以上活動していればよい感じで。

  • 以下のようなコードを動かしながら、関数値gの中でその値が定義された関数f内の変数Aを利用すると、関数値gは変数Aの参照を持っているのだなぁ、という確認をしていた
  var once sync.Once

  onceFunc := func() {
    once.Do(func() { fmt.Println("A") })
  }

  func main() {
    var once sync.Once

    firstFunc := func() {
      once.Do(func() { fmt.Println("A") })
    }

    secondFunc := func() {
      once.Do(func() { fmt.Println("A") })
    }

    firstFunc() // 1回しか実行されない
    secondFunc()
  }

  // $ go run sync.go 
  // A
  func main() {
    var cnt int

    firstFunc := func() {
      cnt++
      fmt.Println("A: ", cnt)
    }

    secondFunc := func() {
      cnt++
      fmt.Println("B: ", cnt)
    }

    firstFunc()
    secondFunc()
  }

  // $ go run sync.go 
  // A:  1
  // B:  2 // それぞれの関数値で値を定義した時点でのコピーを持っているなら、ここは1のはず
  • 定数を見てて知らない概念がいてひよる
    • TLSDialTimeout TLSDialTimeout = 20 * time.Second
      • TLSにDialという概念がある..?
      • 通信が完了するまで待つ時間
    • HTTPClientTimeout HTTPClientTimeout = 60 * time.Second
      • 個々の通信自体の時間 + リダイレクト + リクエスト・レスポンスの読み書き合計したタイムアウト
    • TCPKeepAlive = 60 * time.Second
      • ネットワーク接続のキープアライブの時間
      • キープアライブ自信ねえ...
  • コメント読むの好き
    • // We add : because we include :port as part of host. のように一つずつ値について説明してあるやつとか
  • 値を直接入力させず、値を設定するメソッドを置いている
  // 値を直接入力させず、値を設定するメソッドを置いている
  // Development sets the Client to use the APNs development push endpoint.
  func (c *Client) Development() *Client {
    c.Host = HostDevelopment
    return c
  }

  // Production sets the Client to use the APNs production push endpoint.
  func (c *Client) Production() *Client {
    c.Host = HostProduction
    return c
  }
  • golintのusage が結構愚直に書いてあった
  func usage() {
    fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
    fmt.Fprintf(os.Stderr, "\tgolint [flags] # runs on package in current directory\n")
    fmt.Fprintf(os.Stderr, "\tgolint [flags] [packages]\n")
    fmt.Fprintf(os.Stderr, "\tgolint [flags] [directories] # where a '/...' suffix includes all sub-directories\n")
    fmt.Fprintf(os.Stderr, "\tgolint [flags] [files] # all must belong to a single package\n")
    fmt.Fprintf(os.Stderr, "Flags:\n")
    flag.PrintDefaults()
  }
  • 分岐について自分個人は早期returnしたくなるところで if { 短い } else { 長い }
  func main() {
    flag.Usage = usage
    flag.Parse()

    if flag.NArg() == 0 {
      lintDir(".")
    } else {
    uerr := func(err error) error {
      // the body may have been closed already by c.send()
      if !reqBodyClosed {
        req.closeBody()
      }
      method := valueOrDefault(reqs[0].Method, "GET")
      var urlStr string
      if resp != nil && resp.Request != nil {
        urlStr = resp.Request.URL.String()
      } else {
        urlStr = req.URL.String()
      }
      return &url.Error{
        Op:  method[:1] + strings.ToLower(method[1:]),
        URL: urlStr,
        Err: err,
      }
    }
  • switchに式を与えず case に 式を書くのも普通にやるようだ
  func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
      return c - '0'
    case 'a' <= c && c <= 'f':
      return c - 'a' + 10
    case 'A' <= c && c <= 'F':
      return c - 'A' + 10
    }
    return 0
  }

今週のGo

  • defer FILO/panicになっても実行される
  • recoverはpanicが起こる関数と同じgoroutineの中に置く
  • deferと対になる処理について
  • errorに判定を生やす標準ライブラリの実装例

ログにもありますが、試したGoのバージョンは1.7.3です

deferはFILOでpanicになっても実行される

package main

import "fmt"

func main() {
  defer func() { fmt.Println("1st in") }()
  defer func() { fmt.Println("2nd in") }()

  arr := []int{1, 2, 3}
  fmt.Printf("%v", arr[100]) // raise panic
}
2nd in
1st in
panic: runtime error: index out of range

goroutine 1 [running]:
panic(0x8efc0, 0xc42000a0d0)
  /usr/local/Cellar/go/1.7.3/libexec/src/runtime/panic.go:500 +0x1a1
main.main()
  /Users/yoshidanozomi/study/gopl/study/defer.go:10 +0x93
exit status 2

panicをrecoverで拾えるスコープはgoroutineの中

panicの情報をrecoverで拾える範囲がよくわかってなかったのですが、同じgoroutineで実行される範囲のようです。

package main

import "fmt"

func main() {
  defer func() {
  if err := recover(); err != nil {
        fmt.Println("err:", err)
    }
  }()
  panic("test")
}
err: test
package main

import "fmt"
import "sync"

func main() {
  var wg sync.WaitGroup
  wg.Add(1)

  go func () {
    defer wg.Done()

    // recoverはdeferの中でしか実行できない
    // 同じgoroutineの中であれば、
    // recoverを書くのはpanicが起こる関数の中でなくてもよい
    defer func() {
    if err := recover(); err != nil {
          fmt.Println("err:", err)
      }
    }()
    recoverTest()
  }()

  wg.Wait()
}

func recoverTest() {
  panic("test")
}
err: test
package main

import "fmt"
import "sync"

func main() {
  var wg sync.WaitGroup
  wg.Add(1)

  // deferをpanicが起こるgoroutineの外に置いた場合
  defer func() {
  if err := recover(); err != nil {
        fmt.Println("err:", err)
    }
  }()

  go func () {
    defer wg.Done()
    recoverTest()
  }()

  wg.Wait()
}

func recoverTest() {
  panic("test")
}
$ go run defer.go 
panic: test

goroutine 5 [running]:
panic(0x89e00, 0xc42000a2c0)
  /usr/local/Cellar/go/1.7.3/libexec/src/runtime/panic.go:500 +0x1a1
main.recoverTest()
  /Users/woshidan/study/gopl/study/defer.go:28 +0x6d
main.main.func2(0xc42000a2b0)
  /Users/woshidan/study/gopl/study/defer.go:21 +0x4a
created by main.main
  /Users/woshidan/study/gopl/study/defer.go:22 +0x9a
exit status 2

deferと対になる処理について

go fmt で整形してくれるわけではないですが、Go Blog Defer, Panic, and Recoverあたりを見るに*1

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

のように、後始末が行われてほしい処理の近くにおくのがよいかもです。

errorに判定を生やす標準ライブラリの実装例

net/http ライブラリの実装例 :eyes: .

https://golang.org/src/net/net.go?h=Temporary#L375

// An Error represents a network error.
type Error interface {
  error
  Timeout() bool   // Is the error a timeout?
  Temporary() bool // Is the error temporary?
}

Go、割と雑に一つのファイルに長く処理が書いてあったりして、復習してる間にhttpのコード眺めててひやっとしたり、いままで違う種類のプログラミングなので迷わしい。。なので慣れるまでもうちょっと読む量増やしてこ、ということで現場からは以上です。

*1:まあ、古いですがががが

Intentのフラグを使ってスタックの下の方のActivityをクリアする

下記のフラグをつけてstartActivityする。

Intent intent = new Intent(ThirdActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

サンプルコード https://gist.github.com/woshidan/72bafcbcfabbd4c91a1ee092e36c3a9f

参考

https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TASK

iOS9.0のシミュレータでローカルホストへ接続した時、NSPOSIXErrorDomainになる

開発用にダミーサーバへ接続したらやたらエラーになるので調べたところ、iOS9.0でシミュレータでlocalhostへ接続した場合だけに下記のエラーが出るみたいでした。

Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory" UserInfo={NSErrorFailingURLStringKey=http://192.168.10.5:8080/test, NSErrorFailingURLKey=http://192.168.10.5:8080/test, _kCFStreamErrorCodeKey=2, _kCFStreamErrorDomainKey=1}

関連すると思しきStackOverflowのページは以下。

stackoverflow.com

上記のエラーが発生している場合、リモートのホストと通信した場合はエラーになりません。エラーが発生するのを確認した条件だけメモします。

テスト時に利用していたXCodeは諸事情によりXCode 8.3です。

  • iPhone6s iOS 9.0 to remote host => エラーにならない
  • iPhone6s iOS 9.0 to local host => エラー
  • iPhone6s iOS 10.0 to local host => エラーにならない
  • iPhone6 iOS 9.0 to local host => エラー
  • iPhone6 iOS 9.3 to local host => エラーにならない
  • iPhone5s iOS 9.0 to local host => エラーになる
  • iPhone5 iOS 9.0 to local host => エラーになる

現場からは以上です。