Convex クエリ/ミューテーション/アクション チートシート【保存版シンタックス】

Convex を使った開発では、クエリ、ミューテーション、アクションという 3 つの重要な概念があります。これらの使い分けに迷ったことはありませんか。本記事では、それぞれの役割と具体的なシンタックスをチートシート形式で整理し、すぐに実践で使えるコード例を豊富に紹介します。プロジェクトで迷わず開発を進められるよう、各パターンを網羅的にまとめました。
クイックリファレンス(早見表)
関数タイプ比較表
# | 関数タイプ | 用途 | DB 読み取り | DB 書き込み | 外部通信 | リアクティブ | React フック |
---|---|---|---|---|---|---|---|
1 | query | データ取得 | ★ | ☓ | ☓ | ★ | useQuery |
2 | mutation | データ更新 | ★ | ★ | ☓ | ★ | useMutation |
3 | action | 外部連携・複雑な処理 | △ | △ | ★ | ☓ | useAction |
基本構文早見表
# | 関数タイプ | インポート | 基本構文 |
---|---|---|---|
1 | query | import { query } from "./_generated/server"; | export const myQuery = query({ args: { id: v.id("table") }, handler: async (ctx, args) => { ... } }); |
2 | mutation | import { mutation } from "./_generated/server"; | export const myMutation = mutation({ args: { data: v.string() }, handler: async (ctx, args) => { ... } }); |
3 | action | import { action } from "./_generated/server"; import { api } | export const myAction = action({ args: { param: v.string() }, handler: async (ctx, args) => { ... } }); |
よく使う操作早見表
# | 操作 | query | mutation | action |
---|---|---|---|---|
1 | 全件取得 | await ctx.db.query("table").collect() | await ctx.db.query("table").collect() | await ctx.runQuery(api.table.getAll) |
2 | ID 指定取得 | await ctx.db.get(id) | await ctx.db.get(id) | await ctx.runQuery(api.table.getById, {}) |
3 | 条件検索 | await ctx.db.query("table").filter(...) | await ctx.db.query("table").filter(...) | await ctx.runQuery(api.table.search, {}) |
4 | 新規作成 | ☓ 不可 | await ctx.db.insert("table", { ... }) | await ctx.runMutation(api.table.create) |
5 | 更新 | ☓ 不可 | await ctx.db.patch(id, { ... }) | await ctx.runMutation(api.table.update) |
6 | 削除 | ☓ 不可 | await ctx.db.delete(id) | await ctx.runMutation(api.table.delete) |
7 | 外部 API 呼び出し | ☓ 不可 | ☓ 不可 | await fetch("https://...") |
8 | mutation 呼び出し | ☓ 不可 | ☓ 不可 | await ctx.runMutation(api.table.fn) |
9 | query 呼び出し | ☓ 不可 | ☓ 不可 | await ctx.runQuery(api.table.fn) |
バリデーション型早見表
# | 型 | 構文 | 説明 |
---|---|---|---|
1 | 文字列 | v.string() | 文字列型 |
2 | 数値 | v.number() | 数値型 |
3 | 真偽値 | v.boolean() | 真偽値型 |
4 | ID | v.id("tableName") | テーブルの ID 型 |
5 | オプショナル | v.optional(v.string()) | 省略可能な型 |
6 | 配列 | v.array(v.string()) | 配列型 |
7 | オブジェクト | v.object({ key: v.string() }) | オブジェクト型 |
8 | Union | v.union(v.string(), v.number()) | 複数型のいずれか |
9 | Null | v.null() | null 型 |
背景
Convex とは
Convex は、リアルタイムなデータベースと API を一体化したバックエンドプラットフォームです。従来の REST API や GraphQL とは異なり、リアクティブなデータフローを実現し、フロントエンドの状態が常にバックエンドと同期されます。
3 つの関数タイプ
Convex では、バックエンドのロジックを以下の 3 種類の関数で表現します。
mermaidflowchart TB
client["クライアント<br/>(React等)"]
query["query<br/>(データ読み取り)"]
mutation["mutation<br/>(データ更新)"]
action["action<br/>(外部処理)"]
db[("Convex DB")]
external["外部API<br/>(Stripe, OpenAI等)"]
client -->|useQuery| query
client -->|useMutation| mutation
client -->|useAction| action
query -->|リアクティブ読み取り| db
mutation -->|書き込み| db
action -.->|外部通信| external
action -.->|内部呼び出し| mutation
この図が示すように、query はデータの読み取り専用、mutation はデータベースへの書き込み、action は外部 API との連携や非リアクティブな処理を担当します。
課題
開発時に直面する疑問
Convex を使い始めると、次のような疑問に直面することが多いです。
- いつ query を使い、いつ action を使うべきか
- mutation と action の違いは何か
- リアクティブ性とは具体的に何を意味するのか
- 引数のバリデーションはどう書くのか
これらの疑問を解決するには、各関数タイプの特性と使い分けを体系的に理解する必要があります。
混乱しやすいポイント
# | ポイント | よくある誤解 |
---|---|---|
1 | query での書き込み | query 内で db.insert しようとしてエラーになる |
2 | mutation の外部通信 | mutation から fetch を呼ぼうとして失敗する |
3 | action のリアクティブ性 | action の戻り値がリアクティブに更新されると期待する |
4 | 引数の型安全性 | v.object の定義を省略してランタイムエラーになる |
これらの混乱を避けるため、本記事では各関数タイプの正しい使い方を明確に示します。
解決策
3 つの関数タイプの使い分け
以下の表で、それぞれの関数タイプがどのような場面で使われるかを整理しました。
# | 関数タイプ | 用途 | DB 読み取り | DB 書き込み | 外部通信 | リアクティブ |
---|---|---|---|---|---|---|
1 | query | データ取得 | ★ | ☓ | ☓ | ★ |
2 | mutation | データ更新 | ★ | ★ | ☓ | ★ |
3 | action | 外部連携・複雑な処理 | △ | △ | ★ | ☓ |
この表から、query は読み取り専用でリアクティブ、mutation は書き込みが可能でリアクティブ、action は外部通信が可能だがリアクティブではないことがわかります。
基本原則
各関数タイプを選ぶ際の基本原則は以下の通りです。
- query: データを読み取るだけで、変更はしない。リアルタイムに更新を受け取りたい場合に使用
- mutation: データベースに変更を加える。フォーム送信やボタンクリックなどのユーザーアクションに対応
- action: 外部 API との通信や、複雑な非リアクティブな処理が必要な場合に使用
これらの原則を守ることで、Convex の強みであるリアクティブ性を最大限に活用できます。
具体例
クエリ (query) の基本形
インポート
クエリを定義するには、まず必要なモジュールをインポートします。
typescriptimport { query } from './_generated/server';
import { v } from 'convex/values';
引数なしのシンプルなクエリ
最もシンプルなクエリは、引数を取らずにデータを返すものです。
typescriptexport const listTasks = query({
handler: async (ctx) => {
// すべてのタスクを取得
return await ctx.db.query('tasks').collect();
},
});
このクエリは tasks
テーブルのすべてのドキュメントを取得し、配列として返します。
引数付きクエリ
引数を受け取るクエリでは、args
プロパティを使ってバリデーションを定義します。
typescriptexport const getTaskById = query({
args: { taskId: v.id('tasks') },
handler: async (ctx, args) => {
// ID でタスクを検索
const task = await ctx.db.get(args.taskId);
return task;
},
});
v.id("tasks")
は、Convex の ID 型として taskId
をバリデーションします。型安全性が保証されるため、実行時エラーを防げます。
フィルタリングを含むクエリ
インデックスを使ったフィルタリングは、クエリのパフォーマンスを大幅に向上させます。
typescriptexport const getTasksByStatus = query({
args: { status: v.string() },
handler: async (ctx, args) => {
// status インデックスを使ってフィルタリング
return await ctx.db
.query('tasks')
.withIndex('by_status', (q) =>
q.eq('status', args.status)
)
.collect();
},
});
この例では、by_status
というインデックスを使って、特定のステータスのタスクだけを効率的に取得しています。
複数条件のクエリ
複数のフィルタ条件を組み合わせる場合は、filter
メソッドを使用します。
typescriptexport const getTasksByUserAndStatus = query({
args: {
userId: v.id('users'),
status: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db
.query('tasks')
.withIndex('by_user', (q) =>
q.eq('userId', args.userId)
)
.filter((q) => q.eq(q.field('status'), args.status))
.collect();
},
});
withIndex
でまず userId
でフィルタリングし、その後 filter
で status
による絞り込みを行います。
ミューテーション (mutation) の基本形
インポート
ミューテーションも同様に、必要なモジュールをインポートします。
typescriptimport { mutation } from './_generated/server';
import { v } from 'convex/values';
新規作成 (Insert)
データベースに新しいドキュメントを挿入する基本的なミューテーションです。
typescriptexport const createTask = mutation({
args: {
title: v.string(),
description: v.optional(v.string()),
status: v.string(),
},
handler: async (ctx, args) => {
// 新しいタスクを作成
const taskId = await ctx.db.insert('tasks', {
title: args.title,
description: args.description,
status: args.status,
createdAt: Date.now(),
});
return taskId;
},
});
ctx.db.insert
は新しいドキュメントを挿入し、生成された ID を返します。v.optional
を使うことで、省略可能なフィールドを定義できます。
更新 (Update)
既存のドキュメントを更新するには、patch
メソッドを使用します。
typescriptexport const updateTaskStatus = mutation({
args: {
taskId: v.id('tasks'),
status: v.string(),
},
handler: async (ctx, args) => {
// タスクのステータスを更新
await ctx.db.patch(args.taskId, {
status: args.status,
updatedAt: Date.now(),
});
},
});
patch
は指定したフィールドのみを更新し、他のフィールドはそのまま保持します。
削除 (Delete)
ドキュメントを削除するには、delete
メソッドを使用します。
typescriptexport const deleteTask = mutation({
args: { taskId: v.id('tasks') },
handler: async (ctx, args) => {
// タスクを削除
await ctx.db.delete(args.taskId);
},
});
シンプルですが、削除は元に戻せないため、実装時には注意が必要です。
条件付き更新
更新前に条件をチェックし、条件が満たされる場合のみ更新を実行します。
typescriptexport const completeTask = mutation({
args: { taskId: v.id('tasks') },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
// タスクが存在しない場合はエラー
if (!task) {
throw new Error('Task not found');
}
// すでに完了している場合はスキップ
if (task.status === 'completed') {
return { alreadyCompleted: true };
}
// ステータスを完了に更新
await ctx.db.patch(args.taskId, {
status: 'completed',
completedAt: Date.now(),
});
return { alreadyCompleted: false };
},
});
この例では、タスクの存在確認と現在のステータスをチェックしてから、更新を実行しています。
アクション (action) の基本形
インポート
アクションでは、外部通信や複雑な処理を行うため、必要に応じて追加のモジュールをインポートします。
typescriptimport { action } from './_generated/server';
import { v } from 'convex/values';
import { api } from './_generated/api';
外部 API 呼び出し
アクションの代表的な用途は、外部 API との通信です。
typescriptexport const sendNotification = action({
args: {
email: v.string(),
message: v.string(),
},
handler: async (ctx, args) => {
// 外部 API (例: SendGrid) を呼び出し
const response = await fetch(
'https://api.sendgrid.com/v3/mail/send',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
},
body: JSON.stringify({
personalizations: [
{
to: [{ email: args.email }],
},
],
from: { email: 'noreply@example.com' },
subject: '通知',
content: [
{
type: 'text/plain',
value: args.message,
},
],
}),
}
);
return { success: response.ok };
},
});
アクション内では fetch
が使えるため、外部サービスとの連携が可能です。
アクションから mutation を呼び出し
アクションから mutation を呼び出すことで、外部処理の結果をデータベースに保存できます。
typescriptexport const processPayment = action({
args: {
userId: v.id('users'),
amount: v.number(),
},
handler: async (ctx, args) => {
// 外部決済 API を呼び出し
const paymentResult = await fetch(
'https://api.stripe.com/v1/charges',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
},
body: new URLSearchParams({
amount: (args.amount * 100).toString(),
currency: 'jpy',
}),
}
);
const data = await paymentResult.json();
// 決済結果を mutation でデータベースに保存
if (data.status === 'succeeded') {
await ctx.runMutation(api.payments.recordPayment, {
userId: args.userId,
amount: args.amount,
stripeChargeId: data.id,
});
}
return { success: data.status === 'succeeded' };
},
});
ctx.runMutation
を使って、定義済みの mutation を呼び出しています。これにより、外部処理とデータベース更新を組み合わせられます。
スケジュールされたアクション
定期的に実行したい処理は、スケジュール機能を使って実装できます。
typescriptexport const dailyReport = action({
args: {},
handler: async (ctx, args) => {
// 過去 24 時間のタスク統計を取得
const stats = await ctx.runQuery(
api.tasks.getDailyStats
);
// レポートを外部サービスに送信
await fetch('https://api.slack.com/webhooks/xxx', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `本日のタスク統計: 完了 ${stats.completed} 件、未完了 ${stats.pending} 件`,
}),
});
},
});
このアクションは、Convex のスケジュール機能を使って毎日実行するように設定できます。
バリデーションのパターン
基本的な型
Convex では、v
オブジェクトを使って引数の型を定義します。
typescriptimport { v } from 'convex/values';
export const example = query({
args: {
// 文字列
name: v.string(),
// 数値
age: v.number(),
// 真偽値
isActive: v.boolean(),
// ID
userId: v.id('users'),
// 省略可能
nickname: v.optional(v.string()),
},
handler: async (ctx, args) => {
// ...
},
});
これらの基本的な型を組み合わせることで、ほとんどのユースケースに対応できます。
オブジェクトと配列
複雑な構造を持つ引数は、v.object
や v.array
を使って定義します。
typescriptexport const createProject = mutation({
args: {
project: v.object({
name: v.string(),
description: v.string(),
tags: v.array(v.string()),
metadata: v.object({
priority: v.number(),
dueDate: v.optional(v.number()),
}),
}),
},
handler: async (ctx, args) => {
const projectId = await ctx.db.insert(
'projects',
args.project
);
return projectId;
},
});
ネストしたオブジェクトや配列も、直感的に定義できます。
Union 型
複数の型のいずれかを受け付ける場合は、v.union
を使用します。
typescriptexport const updateField = mutation({
args: {
taskId: v.id('tasks'),
field: v.string(),
value: v.union(v.string(), v.number(), v.boolean()),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.taskId, {
[args.field]: args.value,
});
},
});
この例では、value
は文字列、数値、真偽値のいずれかを受け付けます。
フロントエンドからの呼び出し
React での query 使用
React コンポーネントから query を呼び出すには、useQuery
フックを使用します。
typescriptimport { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function TaskList() {
const tasks = useQuery(api.tasks.listTasks);
if (tasks === undefined) {
return <div>読み込み中...</div>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
);
}
useQuery
は、データが変更されると自動的にコンポーネントを再レンダリングします。これが Convex のリアクティブ性の核心です。
React での mutation 使用
mutation を呼び出すには、useMutation
フックを使用します。
typescriptimport { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function CreateTaskForm() {
const createTask = useMutation(api.tasks.createTask);
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await createTask({
title: formData.get('title') as string,
status: 'pending',
});
};
return (
<form onSubmit={handleSubmit}>
<input name='title' required />
<button type='submit'>タスク作成</button>
</form>
);
}
mutation は非同期関数として呼び出され、完了を待つことができます。
React での action 使用
action を呼び出すには、useAction
フックを使用します。
typescriptimport { useAction } from 'convex/react';
import { api } from '../convex/_generated/api';
function NotificationButton() {
const sendNotification = useAction(
api.notifications.sendNotification
);
const handleClick = async () => {
const result = await sendNotification({
email: 'user@example.com',
message: 'タスクが完了しました',
});
if (result.success) {
alert('通知を送信しました');
}
};
return <button onClick={handleClick}>通知送信</button>;
}
action は mutation と同様に非同期関数として呼び出されますが、リアクティブな更新は発生しません。
エラーハンドリング
query でのエラー
query でエラーが発生した場合、適切なメッセージを返すか、例外をスローします。
typescriptexport const getTask = query({
args: { taskId: v.id('tasks') },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) {
throw new Error(`Task not found: ${args.taskId}`);
}
return task;
},
});
エラーをスローすると、フロントエンド側で catch できます。
mutation でのエラー
mutation でも同様に、エラーハンドリングを行います。
typescriptexport const updateTask = mutation({
args: {
taskId: v.id('tasks'),
title: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
if (!task) {
throw new Error('タスクが見つかりません');
}
if (args.title.length < 3) {
throw new Error('タイトルは 3 文字以上必要です');
}
await ctx.db.patch(args.taskId, { title: args.title });
},
});
ビジネスロジックのバリデーションもここで行えます。
フロントエンド側でのエラーキャッチ
フロントエンド側では、try-catch を使ってエラーをハンドリングします。
typescriptfunction UpdateTaskForm({ taskId }: { taskId: string }) {
const updateTask = useMutation(api.tasks.updateTask);
const [error, setError] = React.useState<string | null>(
null
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
await updateTask({
taskId: taskId as any,
title: (e.target as any).title.value,
});
} catch (err) {
setError(
err instanceof Error
? err.message
: 'エラーが発生しました'
);
}
};
return (
<form onSubmit={handleSubmit}>
<input name='title' required />
<button type='submit'>更新</button>
{error && <div style={{ color: 'red' }}>{error}</div>}
</form>
);
}
エラーメッセージをユーザーに表示することで、問題を把握しやすくなります。
パフォーマンス最適化
ペジネーション
大量のデータを扱う場合、ペジネーションを使って効率的に取得します。
typescriptexport const paginatedTasks = query({
args: {
paginationOpts: v.object({
numItems: v.number(),
cursor: v.union(v.string(), v.null()),
}),
},
handler: async (ctx, args) => {
return await ctx.db
.query('tasks')
.order('desc')
.paginate(args.paginationOpts);
},
});
paginate
メソッドは、指定した件数だけデータを取得し、次のページ用のカーソルを返します。
インデックスの活用
インデックスを適切に使うことで、クエリのパフォーマンスを大幅に向上させられます。
typescript// schema.ts でインデックスを定義
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
tasks: defineTable({
title: v.string(),
status: v.string(),
userId: v.id('users'),
createdAt: v.number(),
})
.index('by_status', ['status'])
.index('by_user', ['userId'])
.index('by_user_and_status', ['userId', 'status']),
});
複合インデックスを使うことで、複数条件のフィルタリングも高速化できます。
不要なデータを取得しない
必要なフィールドだけを返すことで、ネットワークトラフィックを削減できます。
typescriptexport const listTaskTitles = query({
handler: async (ctx) => {
const tasks = await ctx.db.query('tasks').collect();
// タイトルだけを返す
return tasks.map((task) => ({
_id: task._id,
title: task.title,
}));
},
});
シンプルですが、大量のデータを扱う場合は効果的です。
認証との統合
ユーザー情報の取得
認証済みユーザーの情報を取得するには、ctx.auth
を使用します。
typescriptexport const getCurrentUser = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
// ユーザー情報を返す
return {
userId: identity.subject,
email: identity.email,
name: identity.name,
};
},
});
getUserIdentity
は、認証プロバイダーから提供されたユーザー情報を返します。
認証が必要な mutation
認証されていない場合はエラーをスローすることで、セキュアな mutation を実装できます。
typescriptexport const createPrivateTask = mutation({
args: {
title: v.string(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error('認証が必要です');
}
const taskId = await ctx.db.insert('tasks', {
title: args.title,
userId: identity.subject,
createdAt: Date.now(),
});
return taskId;
},
});
この例では、認証されたユーザーのみがタスクを作成できます。
ユーザー専用データの取得
ログインユーザー自身のデータだけを取得する query の実装例です。
typescriptexport const getMyTasks = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return [];
}
return await ctx.db
.query('tasks')
.withIndex('by_user', (q) =>
q.eq('userId', identity.subject)
)
.collect();
},
});
認証情報を使って、ユーザーごとにデータをフィルタリングしています。
まとめ
本記事では、Convex のクエリ、ミューテーション、アクションの基本から応用まで、チートシート形式で解説しました。
query はリアクティブなデータ取得に特化し、データベースの変更を即座にフロントエンドに反映します。mutation はデータベースへの書き込みを担当し、フォーム送信やボタンクリックなどのユーザーアクションに対応します。action は外部 API との連携や複雑な処理を実行し、必要に応じて mutation を呼び出してデータベースを更新します。
これらの使い分けを理解し、適切にバリデーションとエラーハンドリングを実装することで、型安全で堅牢なバックエンドを構築できます。インデックスやペジネーションを活用すれば、パフォーマンスも最適化できますね。
Convex の強みであるリアクティブ性を最大限に活かし、ユーザー体験の優れたアプリケーションを開発していきましょう。
関連リンク
- article
Convex クエリ/ミューテーション/アクション チートシート【保存版シンタックス】
- article
Convex 初期設定完全手順:CLI・環境変数・Secrets・権限までゼロから構築
- article
【比較検証】Convex vs Firebase vs Supabase:リアルタイム性・整合性・学習コストの最適解
- article
【2025 年最新】Convex の全体像を 10 分で理解:リアルタイム DB× 関数基盤の要点まとめ
- article
Convex × React/Next.js 最速連携:useQuery/useMutation の実践パターン
- article
Convex の基本アーキテクチャ徹底解説:データベース・関数・リアルタイム更新
- article
NotebookLM とは?Google 製 AI ノートの仕組みとできることを 3 分で解説
- article
Mermaid 図面の命名規約:ノード ID・エッジ記法・クラス名の統一ガイド
- article
Emotion 初期設定完全ガイド:Babel/SWC/型定義/型拡張のベストプラクティス
- article
Electron セキュリティ設定チートシート:webPreferences/CSP/許可リスト早見表
- article
MCP サーバー とは?Model Context Protocol の基礎・仕組み・活用メリットを徹底解説
- article
Yarn とは?npm・pnpm と何が違うのかを 3 分で理解【決定版】
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来