valid,invalid

関心を持てる事柄について

5分で終わるPrisma 2 Tutorial

「名前をよく聞くが実態がよくわかっていないものリスト」にいたPrismaだが、official tutorialが5分で終わるというのでやってみた。

www.prisma.io

"5分"!? と思ったがPrisma SchemaとPrisma Clientの説明が中心で、ORMの書き味を見てみる程度の内容なのでそんなものかもしれない。

補足説明を読んだりしながら進めたので5分以上かかったが、まったく追っていないJS/TS界のORMの進化をキャッチアップできてだいぶ面白かった。

学んだこと

Prisma 2はただのORM

まず、Hasuraと同列のプロダクトだと誤解していたけど違っていた。Prisma 2はただのORM

同列に語られがちだったのはどうやら今はメンテナンスモードに入っているPrisma 1の頃の話で、GraphQL DSLでデータモデルを定義したり、GraphQL APIサーバとしてCRUDする機能があったかららしい。

Prisma 1の時代のHasura official blogの記事Hasura vs Prisma (2018年10月) にも以下の記述がある。

Over the last few weeks, many people have asked me what the difference between Hasura & Prisma is.

Prismaのコンセプト

Prisma 2はどういうコンセプトなのか?を知るには以下のページが最もわかりやすかった。

Why Prisma? Comparison with SQL query builders & ORMs | Prisma Docs

Prisma 2のトッププライオリティは開発者の生産性へのフォーカス。その中には型安全性やエディタでの入力補完なども含まれる。

開発者はSQLそのものではなく、機能を開発するために必要なデータモデルを考えるべき。そういう意味では生SQL書くのも、SQL起点でデータを考えなければならないクエリビルダもPrisma的には生産性が低い。

Prisma自身も含めて)ORMは高い生産性をうむがobject-relational impedance mismatchの問題がある。「リレーショナルデータは簡単にオブジェクトにマッピングできる」という間違った前提に基づくと、オブジェクト指向では自然なコードがN+1のような問題をかんたんに引き起こしてしまう。

Prismaはcommon antipatternやpitfallを避けるための適切な制約を設けることで従来のORMに比して高い生産性を生むという、スタンス。

image

https://www.prisma.io/docs/concepts/overview/why-prisma より引用

適切な制約の一例はチュートリアルにも現れていたので後述する。

Prismaの構成要素

Prismaのプロダクトは以下の3つで構成されている。すべてをオールインワンで提供しているわけではなく使いたいものを選んで使う。

  • Prisma Client
    • Prisma Schemaに基づいて自動生成されるtype-safeなquery builder
    • Go実装もあるがEarly Accessという位置づけ
  • Prisma Migrate (preview)
    • Prisma Schemaに基づいてdatabase migrationを行えるツール
  • Prisma Studio
    • DBのviewer / editor

What is Prisma? (Overview) | Prisma Docs

Prisma Schema

Prismaのすべての中心。

モデルを定義するデータモデリング言語であり、データソースの定義やgeneratorの定義も含む。

データソースにはPostgreSQL, MySQL, SQLiteが使える。SQL Serverもすでにpreviewが出ているのでそのうち対応が完了しそう。

環境によってはハイライトされないが、VSCodePrisma extensionを使うと良い感じ。

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  Int?
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

Database features matrix (Reference) | Prisma Docsを見ながらこのようなDSLガリガリ書いていく。DBの機能としては存在するがPrisma Schemaではまだ対応していない機能もあるので留意する。

Prisma Client

こんな感じのコードを書く。

import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

async function main() {
  const allUsers = await prisma.user.findMany()
  console.log(allUsers)
}

main()
  .catch(e => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

ORMとして期待する通りこんな配列が得られる。

[
  { id: 1, email: "sarah@prisma.io", name: "Sarah" },
  { id: 2, email: "maria@prisma.io", name: "Maria" },
]

面白いことに、PrismaClientの返すデータにはすべて、発行するクエリに応じた適切な型が付いている。

(推論してくれるので自前で書く必要はないが)Prisma Schema DSLで定義したモデルをimportできる。

いわゆるeager load的に、関連するテーブルのオブジェクトを取得したときはどうなるだろうか。

const allUsers = await prisma.user.findMany({
  include: { posts: true },
});

これもちゃんとIntersection Typesとして型がつく。(User & { posts: Post[] })[] のような感じ。

select *をやめてcolumnを指定すると

const allUsers = await prisma.user.findMany({
  select: { id: true, email: true },
});

column nameでPickした型になる。

こりゃあすげぇ…!

型定義の出力場所

このすげぇ型たちはいったいどこにいるのかと調べたら、node_modules/.prisma/client/index.d.tsにいた。 ユーザーが定義したDSLに基づく成果物をnode_modules配下に吐くこともあるのか。

読めないことはないがなかなか厳しい、というか、開発者が頻繁に読むものではない。

prisma.user.findManyの後ろにはこんなのが控えている。

  export type UserGetPayload<
    S extends boolean | null | undefined | UserArgs,
    U = keyof S
      > = S extends true
        ? User
    : S extends undefined
    ? never
    : S extends UserArgs | FindManyUserArgs
    ?'include' extends U
    ? User  & {
    [P in TrueKeys<S['include']>]: 
          P extends 'posts'
        ? Array < PostGetPayload<S['include'][P]>>  : never
  } 
    : 'select' extends U
    ? {
    [P in TrueKeys<S['select']>]: P extends keyof User ?User [P]
  : 
          P extends 'posts'
        ? Array < PostGetPayload<S['select'][P]>>  : never
  } 
    : User
  : User

上述のたった少しのPrisma Schemaに対してこの雰囲気の型定義が3,000行。これを出力するPrismaのengineの実装はすごいことになっていそうだ。

Prisma Clientが課す制約

上述の型を見てわかるようにPrisma Clientが返すのはただのオブジェクトである。クラスのインスタンスではないのでモデルに関する操作を持つことはできずfat modelを作れないし、associationを辿ってN+1を発生させることはできない。

// posts を include していないので返り値は `User[]`
const allUsers = await prisma.user.findMany(); 

allUsers.forEach((user) => {
  // 型エラーになる
  // Property 'posts' does not exist on type 'User'.
  console.log(user.posts);
})

common antipatternやpitfallを避けるための適切な制約を設ける、という思想の一端が見える。

感想

やはり型の生成がすさまじく便利そうだ。

チュートリアルをやるときもあえてコピペではなく写経をしてみたのだが、入力補完や型チェックなどの支援が心強い。

複雑なクエリをどこまで組み立てられるのか?とか気になる点はまだあるが、Node.js + TypeScriptでサーバサイドアプリケーションを書く機会があれば積極的に検討してみたい。


This article is for ohbarye Advent Calendar 2020.

Rubyでemojiとcodepointsの変換

emoji から codepoints に変換

"👍".unpack("U*")
=> [128077]

"👍".codepoints
=> [128077]

# Convert to hexadecimal
"👍".each_codepoint.map {|n| n.to_s(16) }
=> ["1f44d"]

codepoints から emoji に変換

[128077].pack("U*")
=> "👍"

0x1f44d.chr('UTF-8')
=> "👍"

"\u{1f44d}"
=> "👍"

環境

Ruby 3.0.0

rubocopのNaming/MethodParameterName, Naming/BlockParameterNameで警告の指摘箇所がずれているのを修正してみた

ActiveRecord Association Extensionとwith_optionsを併用するとrubocop-railsのRails/HasManyOrHasOneDependent警告が出るので修正してみた - valid,invalid でrubocopの中身に少し興味を持ったのでシュッと倒せそうなissueがないかを拙作の goofi で検索してみたところ、過去に遭遇したことあるバグを見かけたので対応してpull requestを送ってみた。

事象

issueはこれ Faulty calculation in UncommunicativeName · Issue #8229 · rubocop-hq/rubocop · GitHub

Naming/MethodParameterName, Naming/BlockParameterName copsで警告する対象に _*といったprefixが付いていると警告の表示位置がずれる…、実際の挙動を見たほうがわかりやすい。

以下のファイルがあるとする。

def test1(_a); end

def test2(__a); end

def test3(*a); end

def test4(**a); end

def test5(**__a); end

rubocopを実行すると、警告箇所を示すlocation ^ が引数名の全体にかかっていないことがわかる。

$ rubocop test.rb
Inspecting 1 file
C

Offenses:

test.rb:3:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test1(_a); end
          ^
test.rb:5:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test2(__a); end
          ^
test.rb:7:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test3(*a); end
          ^
test.rb:9:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test4(**a); end
          ^
test.rb:11:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test5(**__a); end
          ^

1 file inspected, 5 offenses detected

本来はこうあるべき。

$ rubocop test.rb
Inspecting 1 file
C

Offenses:

test.rb:3:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test1(_a); end
          ^^
test.rb:5:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test2(__a); end
          ^^^
test.rb:7:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test3(*a); end
          ^^
test.rb:9:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test4(**a); end
          ^^^
test.rb:11:11: C: Naming/MethodParameterName: Method parameter must be at least 3 characters long.
def test5(**__a); end
          ^^^^^

1 file inspected, 5 offenses detected

修正

Naming/MethodParameterName, Naming/BlockParameterName copsでincludeしているUncommunicativeName moduleにて、引数名の長さの計算が誤っていたようだ。

def check(node, args)
  args.each do |arg|
    # Argument names might be "_" or prefixed with "_" to indicate they
    # are unused. Trim away this prefix and only analyse the basename.
    name_child = arg.children.first
    next if name_child.nil?
    full_name = name_child.to_s
    next if full_name == '_'
    name = full_name.gsub(/\A(_+)/, '')
    next if allowed_names.include?(name)

    # name.size が location の range を決めている
    # name は prefix を除いた文字列
    range = arg_range(arg, name.size)
    issue_offenses(node, range, name)
  end
end

lengthを愚直に再計算して渡すようにした。

def check(node, args)
  args.each do |arg|
    # Argument names might be "_" or prefixed with "_" to indicate they
    # are unused. Trim away this prefix and only analyse the basename.
    name_child = arg.children.first
    next if name_child.nil?
    full_name = name_child.to_s
    next if full_name == '_'
    name = full_name.gsub(/\A(_+)/, '')
    next if allowed_names.include?(name)

-   range = arg_range(arg, name.size)
+   length = full_name.size
+   length += 1 if arg.restarg_type?
+   length += 2 if arg.kwrestarg_type?

+   range = arg_range(arg, length)
    issue_offenses(node, range, name)
  end
end

rubocopへのpull requestは初めてだったがシュッとマージしてもらえて良かった。

環境

  • rubocop 1.8.0

This article is for ohbarye Advent Calendar 2020.

ActiveRecord Association Extensionとwith_optionsを併用するとrubocop-railsのRails/HasManyOrHasOneDependent警告が出るので修正してみた

ActiveRecord Association extensionsでメソッドを追加する - valid,invalid で書いたActiveRecord Association Extensionだが、with_optionsと併用するとrubocop-railsRails/HasManyOrHasOneDependent copに警告されることがわかった。

with_options dependent: :destroy do
  has_many :foo do
  end
end
with_options dependent: :destroy do
  has_many :foo do
  ^^^^^^^^ Specify a `:dependent` option.
  end
end

原因調査

disableしてもよいのだがどのような原理で警告が出るのか気になったので調べて修正してみた。

github.com

対象のcopの実装を読みつつprint debugすると、Association Extensionのblockの存在有無によってS式の構造が変わることが原因のようだった。

まず、警告が出ないblockなしのケース。

with_options dependent: :destroy do
  has_many :foo
end
s(:block,                      # <= node.parent
  s(:send, nil, :with_options,
    s(:hash,
      s(:pair,
        s(:sym, :dependent),
        s(:sym, :destroy)))),
  s(:args),
  s(:send, nil, :has_many,     # <= node
    s(:sym, :foo))))

has_manyに対して:dependent optionが渡されるかどうかはnode.parentのnodeからwith_optionsを辿れば判断できる。

一方、blockありのケース。

with_options dependent: :destroy do
  has_many :foo do
  end
end

node.parentのnode下にはwith_options blockがない。

s(:block,                      # <= node.parent.parent
  s(:send, nil, :with_options,
    s(:hash,
      s(:pair,
        s(:sym, :dependent),
        s(:sym, :destroy)))),
  s(:args),
  s(:block,                    # <= node.parent
    s(:send, nil, :has_many,   # <= node
      s(:sym, :foo)),
    s(:args), nil)))

with_optionsの引数を参照するにはnode.parent.parentから辿らなければいけないのだが、node.parentが検査対象となってしまう。

# https://github.com/rubocop-hq/rubocop-rails/blob/9808efdb80078f1382da7cab8fe0b6a1917af047/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb#L64-L70
def valid_options_in_with_options_block?(node)
  return true unless node.parent

  n = node.parent.begin_type? ? node.parent.parent : node.parent

  contain_valid_options_in_with_options_block?(n)
end

修正

block付きの場合のS式にmatchしたらnode.parent.parentを参照して検証させるようにした。

+       def_node_matcher :association_extension_block?, <<~PATTERN
+         (block
+           (send nil? :has_many _)
+           (args) ...)
+       PATTERN

        def valid_options_in_with_options_block?(node)
          return true unless node.parent

-         n = node.parent.begin_type? ? node.parent.parent : node.parent
+         n = node.parent.begin_type? || association_extension_block?(node.parent) ? node.parent.parent : node.parent

          contain_valid_options_in_with_options_block?(n)
        end

rubocop-railsへの初pull requestであり雰囲気で書いているのでacceptされるかはわからない。

(2020-01-13 追記) mergeされた!

これまでrubocopの中身をちゃんと見たことがなかったので学びがあってよかった。

環境

  • rubocop 1.8.0
  • rubocop-rails 2.9.1

This article is for ohbarye Advent Calendar 2020.

GitHubのメール通知先をorganization単位で分ける

長年 GitHub の仕事関連の通知と個人の趣味でウォッチしている organization / repository / issue の通知を分けられないものかと思っていたが、あっさり解決した。

https://github.com/settings/notifications (Settings -> Notifications) の Custom routing から organization 単位で通知を分けられるようだ。

docs.github.com

個人的に実現したいことは以下の設定でなんとかなりそうだ。

  • Default notification email を個人のメールアドレスにする
  • 仕事関連の organization の通知先を Custom routing で社用メールアドレスにする

メール通知なので https://github.com/settings/emails (Settings -> Emails) を何度か見た気がするが、そこで設定できなかったのでできないと思いこんでいた〜

My favorite dancers in 2020

『ワンダンス』の影響もあり、YouTubeでよくダンスバトルの動画を見ている

あまり真剣なファンではないしシーンの現状もよくわかっていないのだけど、3年前に少しだけダンススクールに通っていたこともあり少しだけジャンルやテクニックの知識があるのも楽しめる要因かもしれない

あとバトルで使われるような"ビート"の強い音楽がシンプルに好きというのもある

Bouboo

フランス

HIP-HOPメインで滑らかでしなやかな動きとグラウンドの大胆な使い方が好き

Poppin'ぽいジャンルのときのキレもすごくて、たとえばこのバトルのムーヴ。2:20から銃声までの流れはいったいどうやったら狙えるんだ…

www.youtube.com

伝説の"七人抜き"は流れに完全にノッていて相手のいろんなジャンルをなぎ倒すところが見られる

www.youtube.com

この中国の番組、技名がダンスゲームやペルソナみたいにオシャレ表示されて面白い

www.youtube.com

Salah

フランス

フランスの英雄にして世界最強のストリートダンサー

とのこと。アニメーションやLOCK中心だが何でもできそう

ダンサーよりも"パフォーマー"としてのセンスが爆発しているバトル。KANNONの動けるデブ感もすごい好きなんだけど完全に"喰"ってる

www.youtube.com

5:00のムーヴ、完全にLes Twinsと"会話"してるんだよな〜

www.youtube.com

ちなみにLes TwinsはBoubooと3人でCriminalz Crewというチームをやっている

UKAY

UKAYだけどドイツ

パワームーヴもあり、シンプルなステップでも軽やかさと動きのキレがあり、なんか忍者ぽい。

www.youtube.com

2:10からのムーヴで湧きすぎて観客がカメラを遮ってしまい、ダンスが何も見えなくなっているのウケる

www.youtube.com

Poppin Jun & Phoenix lil'Mini

Poppin'

センスも表現力もあるキッズが大人とガチでやりあってるのめちゃ良いな

www.youtube.com

超ハイレベルなキッズ同士でやりあうのも良いな

www.youtube.com


全体的にめっちゃ"密"、ゆえに盛り上がる感じがCOVID-19以前の風景というもののあわれがして切ない。

こんなんだがohbarye Advent Calendar 2020の記事とする

VSCodeのimport-cost extensionでimportするライブラリのサイズをチェックする

import / require文で読み込む3rd party libraryのコストを表示してくれるVSCode extension。3年も前からあったのにまったく知らなかったな〜

marketplace.visualstudio.com

完璧な分析ツールを目指しているわけではなく「開発中に明らかな問題に気づくためのものが必要(webpack-bundle-analyzerなどは素晴らしいけど見落とされがちなので)」というモチベーション。

asset bundleを分析するならもっと良いツールを使ってくれとのこと。

Vim pluginもあるらしい。

GitHub - yardnsm/vim-import-cost: 🏋️‍♂️ Display the import size of the JavaScript packages in Vim!

仕組み

どうやって計算しているのかなと思ったら作者によって解説されていた。

citw.medium.com

  • ファイルの変更を検知する
  • Typescript / Babylon AST parserで有効なimport/require文のリストを得る
  • 3rd partyライブラリに関するimport or require の行をtmp fileとして書き出す
  • このtmp fileをentrypointとしてWebpackを走らせる
  • compile結果のstatsからサイズを取得して表示する

コードはこのへん

面白い!

High CPU Load問題

僕はまだ遭遇していないのだけど、依存しているbabel-minify-webpack-pluginのためにCPU使用率が高くなる問題が報告されているのはちょっと気になる。このplugin自体はすでにdeprecatedなのでそのうちterser-webpack-pluginに移行されるかもしれないが、放置され気味なのでどうだろう…。


This article corresponds to the 8th day of ohbarye Advent Calendar 2020.