Skip to content

クエリビルダー

プログラマティックにSQLクエリを構築するための強力なツールです。

概要

クエリビルダーを使用すると、文字列連結を使わずに安全で動的なSQLクエリを構築できます。

主な利点

  • 型安全 - TypeScriptの型チェックでエラーを防止
  • SQLインジェクション防止 - 自動的なパラメーターバインディング
  • 可読性 - チェーンメソッドで直感的なクエリ構築
  • 再利用性 - クエリの部分的な再利用が可能

基本的な使い方

クエリビルダーの作成

javascript
import { QueryBuilder } from '@northprint/duckdb-wasm-adapter-core';

const builder = new QueryBuilder();

SELECT文

javascript
// シンプルなSELECT
builder
  .select('id', 'name', 'email')
  .from('users');

// すべてのカラムを選択
builder
  .select('*')
  .from('users');

// エイリアス付き
builder
  .select('u.id', 'u.name', 'COUNT(o.id) as order_count')
  .from('users u')
  .leftJoin('orders o', 'u.id = o.user_id')
  .groupBy('u.id', 'u.name');

WHERE句

javascript
// 単一条件
builder
  .select('*')
  .from('users')
  .where('age', '>', 18);

// 複数条件(AND)
builder
  .select('*')
  .from('users')
  .where('age', '>', 18)
  .where('city', '=', '東京');

// OR条件
builder
  .select('*')
  .from('users')
  .where('age', '>', 18)
  .orWhere('vip', '=', true);

// IN句
builder
  .select('*')
  .from('users')
  .whereIn('status', ['active', 'pending']);

// NULL チェック
builder
  .select('*')
  .from('users')
  .whereNull('deleted_at');

// BETWEEN
builder
  .select('*')
  .from('users')
  .whereBetween('age', 20, 30);

JOIN

javascript
// INNER JOIN
builder
  .select('u.*', 'p.name as profile_name')
  .from('users u')
  .join('profiles p', 'u.id = p.user_id');

// LEFT JOIN
builder
  .select('u.*', 'COUNT(o.id) as order_count')
  .from('users u')
  .leftJoin('orders o', 'u.id = o.user_id')
  .groupBy('u.id');

// 複数のJOIN
builder
  .select('u.name', 'o.total', 'p.method')
  .from('users u')
  .join('orders o', 'u.id = o.user_id')
  .join('payments p', 'o.id = p.order_id')
  .where('o.status', '=', 'completed');

集計とグループ化

javascript
// GROUP BY
builder
  .select('department', 'COUNT(*) as count', 'AVG(salary) as avg_salary')
  .from('employees')
  .groupBy('department');

// HAVING
builder
  .select('department', 'COUNT(*) as count')
  .from('employees')
  .groupBy('department')
  .having('COUNT(*)', '>', 5);

// 複数の集計関数
builder
  .select(
    'category',
    'COUNT(*) as total',
    'SUM(price) as total_price',
    'AVG(price) as avg_price',
    'MIN(price) as min_price',
    'MAX(price) as max_price'
  )
  .from('products')
  .groupBy('category');

ソートと制限

javascript
// ORDER BY
builder
  .select('*')
  .from('users')
  .orderBy('created_at', 'DESC');

// 複数のソート条件
builder
  .select('*')
  .from('users')
  .orderBy('department', 'ASC')
  .orderBy('salary', 'DESC');

// LIMIT と OFFSET
builder
  .select('*')
  .from('users')
  .limit(10)
  .offset(20);

高度な使用方法

サブクエリ

javascript
// WHERE句のサブクエリ
const subquery = new QueryBuilder()
  .select('user_id')
  .from('orders')
  .where('total', '>', 10000);

builder
  .select('*')
  .from('users')
  .whereIn('id', subquery);

// FROM句のサブクエリ
const salesSubquery = new QueryBuilder()
  .select('user_id', 'SUM(total) as total_sales')
  .from('orders')
  .groupBy('user_id');

builder
  .select('u.name', 's.total_sales')
  .from('users u')
  .join(`(${salesSubquery.build()}) s`, 'u.id = s.user_id');

UNION

javascript
const activeUsers = new QueryBuilder()
  .select('id', 'name')
  .from('users')
  .where('status', '=', 'active');

const vipUsers = new QueryBuilder()
  .select('id', 'name')
  .from('users')
  .where('vip', '=', true);

const combined = activeUsers.union(vipUsers);

CTE (Common Table Expressions)

javascript
builder
  .with('active_users', (qb) => {
    qb.select('*')
      .from('users')
      .where('status', '=', 'active');
  })
  .with('recent_orders', (qb) => {
    qb.select('*')
      .from('orders')
      .where('created_at', '>', '2024-01-01');
  })
  .select('au.name', 'COUNT(ro.id) as order_count')
  .from('active_users au')
  .leftJoin('recent_orders ro', 'au.id = ro.user_id')
  .groupBy('au.id', 'au.name');

条件付きクエリ構築

javascript
function buildUserQuery(filters) {
  const builder = new QueryBuilder()
    .select('*')
    .from('users');
  
  // 条件付きでWHERE句を追加
  if (filters.name) {
    builder.where('name', 'LIKE', `%${filters.name}%`);
  }
  
  if (filters.minAge) {
    builder.where('age', '>=', filters.minAge);
  }
  
  if (filters.maxAge) {
    builder.where('age', '<=', filters.maxAge);
  }
  
  if (filters.cities && filters.cities.length > 0) {
    builder.whereIn('city', filters.cities);
  }
  
  if (filters.excludeDeleted) {
    builder.whereNull('deleted_at');
  }
  
  // ソート
  if (filters.sortBy) {
    builder.orderBy(filters.sortBy, filters.sortOrder || 'ASC');
  }
  
  // ページネーション
  if (filters.page && filters.perPage) {
    builder
      .limit(filters.perPage)
      .offset((filters.page - 1) * filters.perPage);
  }
  
  return builder;
}

// 使用例
const query = buildUserQuery({
  name: '田中',
  minAge: 20,
  cities: ['東京', '大阪'],
  excludeDeleted: true,
  sortBy: 'created_at',
  sortOrder: 'DESC',
  page: 2,
  perPage: 20
});

INSERT文

javascript
// 単一行の挿入
builder
  .insert('users')
  .values({
    name: '田中太郎',
    email: 'tanaka@example.com',
    age: 30
  });

// 複数行の挿入
builder
  .insert('users')
  .values([
    { name: '田中太郎', email: 'tanaka@example.com', age: 30 },
    { name: '佐藤花子', email: 'sato@example.com', age: 25 },
    { name: '鈴木一郎', email: 'suzuki@example.com', age: 35 }
  ]);

// SELECT結果の挿入
builder
  .insert('archived_users')
  .columns('id', 'name', 'email')
  .select((qb) => {
    qb.select('id', 'name', 'email')
      .from('users')
      .where('deleted_at', 'IS NOT', null);
  });

UPDATE文

javascript
// 基本的なUPDATE
builder
  .update('users')
  .set({
    status: 'inactive',
    updated_at: new Date()
  })
  .where('last_login', '<', '2023-01-01');

// 条件付きUPDATE
builder
  .update('products')
  .set('price', builder.raw('price * 1.1'))
  .where('category', '=', 'electronics');

// JOINを使用したUPDATE
builder
  .update('users u')
  .set('u.vip', true)
  .from('orders o')
  .where('o.user_id', '=', builder.raw('u.id'))
  .where('o.total', '>', 100000);

DELETE文

javascript
// 基本的なDELETE
builder
  .delete()
  .from('users')
  .where('status', '=', 'deleted');

// 複数条件のDELETE
builder
  .delete()
  .from('logs')
  .where('created_at', '<', '2023-01-01')
  .where('level', '=', 'debug');

// サブクエリを使用したDELETE
const inactiveUsers = new QueryBuilder()
  .select('id')
  .from('users')
  .where('last_login', '<', '2022-01-01');

builder
  .delete()
  .from('user_sessions')
  .whereIn('user_id', inactiveUsers);

フレームワーク統合

React

jsx
import { useQueryBuilder } from '@northprint/duckdb-wasm-adapter-react';

function UserSearch() {
  const { builder, execute, data, loading } = useQueryBuilder();
  const [filters, setFilters] = useState({});
  
  const handleSearch = async () => {
    builder
      .select('*')
      .from('users');
    
    if (filters.name) {
      builder.where('name', 'LIKE', `%${filters.name}%`);
    }
    
    if (filters.age) {
      builder.where('age', '>=', filters.age);
    }
    
    await execute();
  };
  
  return (
    <div>
      <input 
        placeholder="名前"
        onChange={(e) => setFilters({...filters, name: e.target.value})}
      />
      <input 
        type="number"
        placeholder="年齢"
        onChange={(e) => setFilters({...filters, age: e.target.value})}
      />
      <button onClick={handleSearch}>検索</button>
      
      {loading && <p>検索中...</p>}
      {data && (
        <ul>
          {data.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Vue

vue
<template>
  <div>
    <input v-model="filters.name" placeholder="名前">
    <input v-model.number="filters.age" type="number" placeholder="年齢">
    <button @click="search">検索</button>
    
    <ul v-if="data">
      <li v-for="user in data" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { useQueryBuilder } from '@northprint/duckdb-wasm-adapter-vue';

const { builder, execute, data, loading } = useQueryBuilder();
const filters = reactive({ name: '', age: null });

const search = async () => {
  builder.value
    .select('*')
    .from('users');
  
  if (filters.name) {
    builder.value.where('name', 'LIKE', `%${filters.name}%`);
  }
  
  if (filters.age) {
    builder.value.where('age', '>=', filters.age);
  }
  
  await execute();
};
</script>

Svelte

svelte
<script>
  import { createDuckDB } from '@northprint/duckdb-wasm-adapter-svelte';
  
  const db = createDuckDB({ autoConnect: true });
  const builder = db.builder();
  
  let filters = { name: '', age: null };
  
  $: {
    builder.reset();
    builder.select('*').from('users');
    
    if (filters.name) {
      builder.where('name', 'LIKE', `%${filters.name}%`);
    }
    
    if (filters.age) {
      builder.where('age', '>=', filters.age);
    }
  }
  
  const search = () => builder.execute();
</script>

<input bind:value={filters.name} placeholder="名前">
<input bind:value={filters.age} type="number" placeholder="年齢">
<button on:click={search}>検索</button>

{#if $builder.data}
  <ul>
    {#each $builder.data as user}
      <li>{user.name}</li>
    {/each}
  </ul>
{/if}

パフォーマンスのヒント

インデックスの活用

javascript
// インデックスを活用できるクエリ
builder
  .select('*')
  .from('users')
  .where('email', '=', 'user@example.com'); // emailにインデックスがある場合

// インデックスを活用できないクエリ
builder
  .select('*')
  .from('users')
  .where('email', 'LIKE', '%@example.com'); // 前方一致でない

必要なカラムのみ選択

javascript
// 良い例
builder
  .select('id', 'name', 'email')
  .from('users');

// 悪い例
builder
  .select('*')
  .from('users');

バッチ処理

javascript
// トランザクション内でバッチ処理
const connection = await createConnection();
await connection.execute('BEGIN TRANSACTION');

try {
  for (const batch of dataBatches) {
    const query = new QueryBuilder()
      .insert('large_table')
      .values(batch)
      .build();
    
    await connection.execute(query);
  }
  
  await connection.execute('COMMIT');
} catch (error) {
  await connection.execute('ROLLBACK');
  throw error;
}

エラーハンドリング

javascript
try {
  const query = builder
    .select('*')
    .from('users')
    .where('age', '>', 'invalid') // 型エラー
    .build();
  
  const result = await connection.execute(query);
} catch (error) {
  if (error.code === 'INVALID_TYPE') {
    console.error('データ型エラー');
  } else if (error.code === 'SYNTAX_ERROR') {
    console.error('SQL構文エラー');
  } else {
    console.error('予期しないエラー:', error);
  }
}

まとめ

クエリビルダーを使用することで、安全で保守性の高いデータベースアクセスコードを書くことができます。文字列連結によるSQL構築と比較して、以下の利点があります:

  • SQLインジェクションの防止
  • 型安全性の向上
  • コードの可読性向上
  • 動的クエリの構築が容易
  • 再利用可能なクエリパーツ

Released under the MIT License.