2025年01月の学習振り返り

どうも、yukiです。

この記事では2025年01月の学習の振り返りをしていきます。

01月の学習時間

結果: 114h (累計: 748h)

記録していた学習時間としては114hですが、実情は寝る前に布団の中でコードを読んだり、仕事の合間にキャッチアップしたりしていました。

なので、そういう時間を含めると体感130hは超えているかなと思います。

01月に取り組んだ学習

よかったところ

  • Xクローンが完成した

Happiness Chainに入会して以来一番苦労した課題だったので、めちゃくちゃ達成感がありました。

もちろん学べたことも多く、X クローンと格闘していた期間は本当に実りあるものでした。

正直、1月後半はXクローンが完成した喜びで気持ちが浮ついてしまい、課題に手がつかない日もありました。

が、まだまだこれからが課題の本領だと思いますので、襟を正してこれからも精進していきます。

改善点

  • 学習時間の記録が少し杜撰
    布団に入ってからの学習や、仕事中の隙間時間でのキャッチアップを含めると平均して月 +20hくらいの学習時間にはなっていそうです。

これは無視できない値なので、できるだけ正確に記録するよう努力していこうと思います。

02月の目標

引き続き 月160h↑を目指して取り組んでいきます。

「JavaScript Primer - 迷わないための入門書」を読んだ。

はじめに

どうも、yukiです。

今回はJavaScript Primer - 迷わないための入門書を読了したので、その感想を綴っていきます。

対象読者

  • 初めてプログラミングに触れる方向けではない、と感じた。
  • 何か一つ、言語の基礎を習得してから取り組むべきかも。

良かったところ

  • JavaScriptの基礎が習得できる。
  • ハマりポイントを潰すように書かれている。転ばぬ先の杖を渡されたような感覚。

学んだこと(抜粋)

  • 非同期処理
  • Arrow Functionはthisを暗黙的な引数として受けつけない
  • クロージャの仕組み

難しかったこと

  • 非同期処理: 名前と簡単な概要は元々知っていたが、実際に処理を記述するのは初めてなので、慣れない動きに感じた。
  • this: 参照先が文脈によって変わるため、その把握が難しかった。

おわりに

JavaScriptは少々癖のある言語に感じました(私はとても好きです)。

これからどんどん書いて慣れていきたいと思います。

Django ORMにハマっていたはずが、結局はSQL基礎で躓いていた話

はじめに

Djangoを用いて、X(旧Twitter)のようなリツイート機能を実装していた際に遭遇したバグ取りのお話です。

要件としては、以下の通りです。

問題の発生箇所を最小構成で再現(importは省く)

  • models.py
class Tweet(models.Model):
    content = models.CharField(max_length=140)
    created_at = models.DateTimeField(default=timezone.now)

    class Meta:
        db_table = 'tweet'


class Retweet(models.Model):
    tweet = models.ForeignKey(
        Tweet,
        related_name='retweets',
        on_delete=models.CASCADE
    )
    created_at = models.DateTimeField(default=timezone.now)

    class Meta:
        db_table = 'retweet'
  • REPLで問題のクエリを再現
# ツイート作成
tweet = Tweet.objects.create(content="Test tweet")

# リツイート作成
Retweet.objects.create(tweet=tweet)  # 1つ目
Retweet.objects.create(tweet=tweet)  # 2つ目
Retweet.objects.create(tweet=tweet)  # 3つ目

# 問題のクエリを実行
problematic = Tweet.objects.annotate(
    retweets_count=Count('retweets', distinct=True),
    last_retweet_at=Max('retweets__created_at'),
    effective_date=Coalesce('retweets__created_at', 'created_at')
).order_by('-effective_date', '-created_at')

# 結果を確認
print(f"実際のリツイート数: {tweet.retweets.count()}") # => 3
print(f"クエリでのカウント: {problematic[0].retweets_count}") # => 1
  • 発行されたSQL(problematic.query)
SELECT
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  COUNT(DISTINCT "retweet"."id") AS "retweets_count",
  MAX("retweet"."created_at") AS "last_retweet_at",
  COALESCE("retweet"."created_at", "tweet"."created_at") AS "effective_date"
FROM
  "tweet"
  LEFT OUTER JOIN "retweet" ON ("tweet"."id" = "retweet"."tweet_id")
GROUP BY
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  6
ORDER BY
  6 DESC,
  "tweet"."created_at" DESC
  • ツイートに対して3件のリツイートが存在するにもかかわらず、retweets_countが1を返してしまっています(次似たようなことがあれば、ここで原因に気付きたい)。

問題の特定

  • まずCount()単体が正常に動作しているか確認したかったので、クエリを切り分けて実行してみました。
# パターン1: 基本的なカウント
q1 = Tweet.objects.annotate(
    retweets_count=Count('retweets', distinct=True),
    last_retweet_at=Max('retweets__created_at')
)

# パターン2: effective_dateを追加
q2 = Tweet.objects.annotate(
    retweets_count=Count('retweets', distinct=True),
    last_retweet_at=Max('retweets__created_at'),
    effective_date=Coalesce('retweets__created_at', 'created_at')
)

# 結果
print("q1のカウント:", q1[0].retweets_count)  # => 3
print("q2のカウント:", q2[0].retweets_count)  # => 1

どうやらeffective_dateの追加がネックになっているようです。

分析

p1、p2で発行されているクエリを見比べてみます。

  • p1
SELECT
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  COUNT(DISTINCT "retweet"."id") AS "retweets_count",
  MAX("retweet"."created_at") AS "last_retweet_at"
FROM
  "tweet"
  LEFT OUTER JOIN "retweet" ON ("tweet"."id" = "retweet"."tweet_id")
GROUP BY
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at"
  • p2
SELECT
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  COUNT(DISTINCT "retweet"."id") AS "retweets_count",
  MAX("retweet"."created_at") AS "last_retweet_at",
  COALESCE("retweet"."created_at", "tweet"."created_at") AS "effective_date"
FROM
  "tweet"
  LEFT OUTER JOIN "retweet" ON ("tweet"."id" = "retweet"."tweet_id")
GROUP BY
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  6
  • q2では、GROUP BYに6、つまりCOALESCE("retweet"."created_at", "tweet"."created_at") AS "effective_date"が追加されています。

  • retweet.created_atをグループ化してしまうことによって、「リツイートごとに別グループとして扱われ各グループのカウントが1になっていた」、というのが原因のようです。

SQLでは、集約関数(COUNTやMAX)を使用する際、SELECT句に含まれる非集約カラムはすべてGROUP BY句に含める必要があるので、ORMもこういう挙動になる。結果を考えると当たり前なんですけどね・・基礎って大事・・

解決

retweets__created_atを直接Coalesce()で使用しているのが不味そうです。

なので、サブクエリで先に計算して単一の値を返す形にしておきます(GROUP BY句にretweet.created_atが含まれないようにするため)。

fixed = Tweet.objects.annotate(
    retweets_count=Count('retweets', distinct=True),
    last_retweet_at=Subquery(
        Retweet.objects.filter(
            tweet=OuterRef('pk')
        ).order_by('-created_at').values('created_at')[:1]
    ),
    effective_date=Coalesce('last_retweet_at', 'created_at')
).order_by('-effective_date', '-created_at')

print("\n解決策:")
print(f"実際のリツイート数: {tweet.retweets.count()}") # => 3
print(f"修正後のカウント: {fixed[0].retweets_count}") # => 3
  • 発行されたクエリ(fixed.query)
SELECT
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  COUNT(DISTINCT "retweet"."id") AS "retweets_count",
  (
    SELECT
      U0."created_at"
    FROM
      "retweet" U0
    WHERE
      U0."tweet_id" = ("tweet"."id")
    ORDER BY
      U0."created_at" DESC
    LIMIT
      1
  ) AS "last_retweet_at",
  COALESCE(
    (
      SELECT
        U0."created_at"
      FROM
        "retweet" U0
      WHERE
        U0."tweet_id" = ("tweet"."id")
      ORDER BY
        U0."created_at" DESC
      LIMIT
        1
    ), "tweet"."created_at"
  ) AS "effective_date"
FROM
  "tweet"
  LEFT OUTER JOIN "retweet" ON ("tweet"."id" = "retweet"."tweet_id")
GROUP BY
  "tweet"."id",
  "tweet"."content",
  "tweet"."created_at",
  6
ORDER BY
  6 DESC,
  "tweet"."created_at" DESC

おわりに

回り回って、SQLの基礎を身につけているか否かを問われるようなバグ取りでした。

2024年12月の学習振り返り

どうも、yukiです。

この記事では2024年12月の学習の振り返りをしていきます。

12月の学習時間

結果: 125h (累計: 634h)

今月はコロナで一週間ほど療養していたことを踏まえると、悪くない学習時間を確保できました。

内容としてもXクローンの途中まで進めることができ、順調な成果を収められたかと思います。

今後はもう少し濃い内容を成果物に盛り込めるよう頑張ります。

12月に取り組んだ学習

  • Django  ← 取り組み中

よかったところ

  • 新規機能実装に慣れてきた
    「実装に必要な知識を軽くインプット」 ⇨ 「とにかく手を動かして、疑問点を炙り出す/深ぼる」といったスタイルに落ち着きました。

以前は「手を動かすより前にしっかり考えなければならない/設計が大事」、とかなりの時間をかけてインプットしていましたが、結局のところ経験が圧倒的に足りていない現段階ではとにかく転んで覚える方が楽でした。

実際にコードを書いて機能をどんどん追加していくことで、拡張性が悪い書き方に気付くことも多かったです。

このように振り返ると、かの有名な「愚者は経験に学び・・」の言葉が頭に浮かびますね(笑)

でも、前に進むことが今の自分には先決なので、今後もこのスタイルでしばらく続けてみようと思います。

改善点

  • 成果重視もよいが、過程ももう少し大切にする
    ちょっとした機能やデザインもしっかり咀嚼して、丁寧な実装を心がけたいです!

01月の目標

引き続き 月160h↑を目指して取り組んでいきます。

PS: 本日は2024年最終日

今年ももう少しで終わりますね。 皆さま、どうか良いお年をお迎えください!

読みたい本まとめ

Shougoware(dpp.vim、ddc.vim、ddu.vim)を使用したNeovim環境

これはHappiness Chain Advent Calendar 2024の18日目の記事です。

はじめに

Neovimを使い始めて半年弱、そこそこ使える環境になってきたので紹介します。

プラグインマネージャー

dpp.vimを使用しています。 こちらを採用している理由は以下です。

  • 遅延読み込み機能がある
  • ちゃんと設定を書かないと動かない(つまりわからないことは実現できないので安心)

遅延読み込み機能自体は様々なプラグインマネージャーが提供していますが、私は以下のようにプラグインをtomlファイルで管理し、on_eventで遅延読み込みのタイミングを指定できるのが好みで気に入っています。

[[plugins]]
repo = 'neovim/nvim-lspconfig'
on_event = ['BufRead', 'CursorHold']
lua_source = """
require("hooks/lsp")
"""

現在、計65個ほどのプラグインを入れていますが、起動時間は26ミリ秒くらいに抑えられています(プレーンのファイルを開いた場合)。 image.png

主要なプラグイン

補完機能 (ddc.vim)

補完機能にはddc.vimを使用しています。 HitPawOnline_12332.gif

LSP、Copilot、Around、Fileなど、様々な補完ソースを自分で設定して使用するプラグインです。

このプラグインも導入したら動くという代物ではなく設定が必要なので、自ずと自分が触っている物への理解が生まれはじめます。

ファイラー・ファインダー (ddu.vim)

ファイラー・ファインダーとしてddu.vimを使用しています(ddu.vim自体はUI作成プラグイン)。

ファイラー(ddu-ui-filer)

HitPawOnline_134423.gif

フォルダ/ファイルの削除、名前変更もこのUIから可能です。

mr(ddu-source-mr)

HitPawOnline_135315.gif

設定を怠っているので、ファイルが全て絶対パス表記になっていますが、大体曖昧検索で使用するので不便はしていません。

バッファ管理(ddu-source-buffer)

HitPawOnline_14911 (1).gif

かなり助けられているソースです。目的のバッファまで小回りが利く場合はもちろん:bnなどを使います。

git diff(ddu-source-git_status)

HitPawOnline_141748.gif

普段git diffするときはターミナルでコマンドを叩いて見ていますが、全体をサッと把握したいときはこちらで確認しています。

CopilotChat.nvim

CopilotChatで複数ファイルを参照しての質問がしやすいよう、既存の全バッファを「ファイル名で区切って一つのバッファに出力する」関数を書いてそれを主軸に運用しています。

function CopilotChatAllBuffers()
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_name(buf, "AllBuffers")
  
  vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile')
  vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe')
  vim.api.nvim_buf_set_option(buf, 'swapfile', false)
  
  local function should_exclude(bufnr)
    local buftype = vim.api.nvim_buf_get_option(bufnr, 'buftype')
    local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype')
    local bufname = vim.api.nvim_buf_get_name(bufnr)
    
    return buftype ~= ""
      or filetype == 'qf'
      or filetype == 'help'
      or filetype == 'git'
      or bufname:match("^%[.*%]$")
      or bufname == ""
      or bufnr == buf
  end

  local contents = {}
  for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
    if vim.api.nvim_buf_is_valid(bufnr) 
      and vim.api.nvim_buf_is_loaded(bufnr) 
      and not should_exclude(bufnr) then
      
      local bufname = vim.api.nvim_buf_get_name(bufnr)
      local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
      table.insert(contents, string.format("バッファ: %s\n\n%s\n%s\n", 
        vim.fn.fnamemodify(bufname, ":t"),
        table.concat(lines, "\n"),
        string.rep("-", 50)))
    end
  end

  vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(table.concat(contents, "\n"), "\n"))
  
  vim.cmd('vsplit')
  vim.api.nvim_win_set_buf(0, buf)
end

まとめられたバッファに質問することで擬似的に複数ファイルへの質問を可能にしています。

HitPawOnline_144915.gif

CopilotChatへ直接複数バッファを渡す方法も多分あるのかなと思いますが、この方法では外部の生成AIにもそのままコピペで渡せるので重宝しています。

w3m.vim

古くからあるテキストベースのwebブラウザw3mvimで快適に使えるようにしてくれるプラグインです。

HitPawOnline_15744.gif

軽い調べ物ならこのプラグインのおかげで大体vim内にて解決します。

おわりに

まだ煮詰まっていない/問題を放置している部分も多いです。

が、開発に必要な最低限の機能は詰め込めたと思っているので、もしこれからvimを始めるという方がいらっしゃれば少しでもご参考になれば幸いです。

以下、私の設定をまとめたdotfiles(dotvimといったほうがよいかもしれません)を置いておきます。

https://github.com/yuki-0224-ui/dotfiles/tree/main

それでは、少し気が早いかもしれませんが皆さんの2025年も最高の年になりますように!良いお年を!

11月の学習振り返り

どうも、yukiです。

この記事では11月の学習の振り返りをしていきます。

11月の学習時間

結果: 111h (累計: 509h)

今月はモチベーションが低迷しており、全体的にあまりよろしくない月でした。

月終盤に達成したい目標を再認識することで、なんとかエンジンを掛け直すことに成功しました。

今後は気持ちに左右されすぎないよう、しっかりと軸を持って進んでいきます。

11月に取り組んだ学習

よかったところ

  • フルスタックフレームワークに慣れてきた
    触れる前は全く太刀打ちできなかったらどうしようと思っていたのですが、色々いじっていると実装のため見ないといけない部分がわかってきて楽しいです。

改善点

これまで色々寄り道しました。これからは取捨選択をしっかりと行なって、自分が選べる最短でマラソンしていきます。

12月の目標

引き続き 月160h↑を目指して取り組んでいきます。