🐥

Supabaseで複数のテーブルから必要な情報を取得し、画面に描画する

2024/02/25に公開

概要

Supabaseで複数のテーブルからデータを取得するときに昔の自分は下記のようにやっていました。

ただこれじゃ2度Supabaseからデータを取得することになるので非効率です。
それを効率良くしていきたいと思います。

SupabaseのSchema Visualizer

まずSupabaseのSchema Visualizerはこちらになります。

その中でtodo_title,body,category_idを取得して、画面に描画させて行こうと思います。

Flutter側での使用パッケージについて

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  flutter_riverpod: 2.4.10 
  riverpod_annotation: 2.3.4
  supabase_flutter: 2.3.4
  freezed: 2.4.7
  freezed_annotation: 2.4.1
  json_annotation: 4.8.1


dev_dependencies:
  flutter_test:
    sdk: flutter
    
  flutter_lints: ^2.0.0
  build_runner: 2.4.8
  riverpod_generator: 2.3.9
  json_serializable: 6.7.1

使用するのは
RiverPod,Supabase,freezed(json系も含めて)になります。

Supabase セットアップについて

https://supabase.com/docs/reference/dart/installing

上記を参照してください

ソースの中身について

今回は AsyncNotifier を使用して、Supabaseからデータを取得し、表示を想定として作成します。

View層

画面側は至ってシンプルで、表示されるときに build()メソッドでtodoの情報を取得しています。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sample_project/domain/todo_notifier.dart';

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: ref.watch(todoNotifierProvider.select((value) => value)).when(
            data: (data) {
              return Padding(
                padding: const EdgeInsets.all(16),
                child: ListView.builder(
                  itemBuilder: (BuildContext context, int index) {
                    return Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: [
                        Text(data.todoModel[index].todoTitle),
                        Text(data.todoModel[index].categoryModel.categoryTitle),
                      ],
                    );
                  },
                  itemCount: data.todoModel.length,
                ),
              );
            },
            error: (_, __) {},
            loading: () => const Center(
              child: CircularProgressIndicator(),
            ),
          ),
    );
  }
}
state層

todo_state
List型のTodoModelを格納します。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sample_project/infrastructure/todo/model/todo_model.dart';

part 'todo_state.freezed.dart';
part 'todo_state.g.dart';

@freezed
class TodoState with _$TodoState {
  const factory TodoState({
    required List<TodoModel> todoModel,
  }) = _TodoState;

  factory TodoState.fromJson(Map<String, dynamic> json) => _$TodoStateFromJson(json);
}
domain層

Notifier,Serviceともに加工などは一切行っていないので、Notifierだけ抜粋します。
単純にService層にtodoModelを取得しに行っています。

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sample_project/domain/todo_service.dart';
import 'package:sample_project/domain/todo_state.dart';

part 'todo_notifier.g.dart';

@riverpod
class TodoNotifier extends _$TodoNotifier {
  @override
  FutureOr<TodoState> build() async {
    return TodoState(
      todoModel: await ref.read(todoServiceProvider).fetchTodoList(),
    );
  }
}
infra層

Supabaseからデータの取得を行います。
今回はこちらを使って作成しました。

それ以外の内容については公式Docを参照してください。
https://supabase.com/docs/reference/dart/insert?example=query-referenced-tables

import 'dart:convert';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sample_project/infrastructure/todo/model/todo_model.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

final todoRepositoryProvider = Provider.autoDispose((ref) => TodoRepository());

class TodoRepository {
  final SupabaseClient supabase = Supabase.instance.client;

  /// Todoの取得
  Future<List<TodoModel>> fetchTodoList() async {
    final response = await supabase.from('todo').select('todo_title,body,category:category_id(title)');
    List<TodoModel> todoList = response.map((e) => TodoModel.fromJson(e)).toList();
    return todoList;
  }
}
final response = await supabase.from('todo').select('todo_title,body,category!inner(title)');

ちなみにこれでもできます。

各動きに関してはアコーディオンを見ていただければと思います。

Supabase複数テーブルを参照するときどんなDataとして返ってくるのか

今回帰ってきたレスポンスとして

final response = await supabase.from('todo').select('todo_title,body,category:category_id(title)');

上記の返り値が下記になります。

[
    {
        todo_title: アプリ開発をする。,
        body: null,
        category: {
            title: 必ずやる
        }
    }
]

categoryだけがネストで格納されている為、fromJSONがそのままだとエラーで弾かれてしまう形になります。
Freezedをうまく使うことによってネストされているやつもそのままfromJSONで取得できないかなと考えました。

単純に入れ込む場合

@freezed
class CategoryModel with _$CategoryModel {
  const factory CategoryModel({
     required String categoryTitle,
  }) = _CategoryModel;

  factory CategoryModel.fromJson(Map<String, dynamic> json) => _$CategoryModelFromJson(json);

  factory CategoryModel.insert(String title) {
    return CategoryModel(categoryTitle: title);
  }
}

CategoryModel.insertを呼び出して、格納していくまた、都度都度、別のModel作成時にも作っていって格納していく為、非効率だと考えました。

ネストされているデータもJSONとして取得できるといいのになと考え・・・。

色々探した結果

@JsonSerializableのオプションコマンドexplicitToJsonです。

@JsonSerializable(explicitToJson: true)を設定することによって

TodoModel

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sample_project/infrastructure/todo/model/category_model.dart';

part 'todo_model.freezed.dart';
part 'todo_model.g.dart';

@freezed
class TodoModel with _$TodoModel {
  @JsonSerializable(explicitToJson: true)
  const factory TodoModel({
    // Todoテーブルの todo_title
    @JsonKey(name: 'todo_title') required String todoTitle,
    // Todoテーブルの body
    @JsonKey(name: 'body') required String? body,
    // categoryテーブル
    @JsonKey(name: 'category') required CategoryModel categoryModel,
  }) = _TodoModel;

  factory TodoModel.fromJson(Map<String, dynamic> json) => _$TodoModelFromJson(json);
}

CategoryModel

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'category_model.freezed.dart';
part 'category_model.g.dart';

@freezed
class CategoryModel with _$CategoryModel {
  const factory CategoryModel({
    required String categoryTitle,
  }) = _CategoryModel;

  factory CategoryModel.fromJson(Map<String, dynamic> json) => _$CategoryModelFromJson(json);

  factory CategoryModel.insert(String title) {
    return CategoryModel(categoryTitle: title);
  }
}

返り値

[
    {
        todo_title: アプリ開発をする。,
        body: null,
        category: {
            title: 必ずやる
        }
    }
]

上記のレスポンスを fromJSONとしてModelに格納することができます。
これで外部テーブルがあってもfromJSONに格納できるようになるので、都度都度違うテーブルを叩いて、DBを呼ぶ回数が減りました。

最後に

今回記事を書くのに使用した内容はGitHubに置きましたので、参照してください!
https://github.com/k1tsu2/sample_todo_app

あと是非X(Twitter)をフォローして頂けると嬉しいです。
https://twitter.com/kitsu2_

Discussion