😇

Supabaseでリレーションを使って他のテーブルのデータを取得 

2024/05/31に公開

Tips💡

Supabaseでリレーションを使って他のテーブルのデータを取得して、Viewに表示する実験を最近やってます。綺麗なコードではないが実験に成功したので、記録を残そうと記事を書きます。本当はモデルクラスを作ったりして、データ型をdynamicにしないようにしないといけないんですけどね(−_−;)

ダミーのテーブルのデータを作成しておく

create table
  followings (
    id bigint primary key generated always as identity,
    name text
  );

create table
  followers (
    id bigint primary key generated always as identity,
    name text
  );

create table
  relationships (
    id bigint primary key generated always as identity,
    following_id bigint references followings (id),
    follower_id bigint references followers (id)
  );

  -- master data for followings
insert into followings (name) values ('Alice');

insert into followings (name) values ('Bob');

insert into followings (name) values ('Charlie');

-- master data for followers
insert into followers (name) values ('Alice');

insert into followers (name) values ('Bob');

insert into followers (name) values ('Charlie');

-- relationships

insert into relationships (following_id, follower_id) values (1, 1);

insert into relationships (following_id, follower_id) values (1, 2);

insert into relationships (following_id, follower_id) values (1, 3);

中間テーブルが今回あるのですが、こちらをデータ取得のテーブルとして使います。便利なツールを使うとER図を作れます。

https://zenn.dev/takumikunn15/articles/cfd2ed144a9093

ER図

仕組みは、フォローする人、される人のテーブルの間にリレーションがあって、このテーブルにidを保存して、その idを参照すると、他のテーブルのデータを取得することができます。認証もするアプリだろうから、本当はuuidとか入れるのでしょうけど(^_^;)

[サンプルコード]

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:follower_app/view/sign_in_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  Future<List<dynamic>?> relationships() async {
    final response = await Supabase.instance.client
        .from('relationships')
        .select('following_id, followings(id, name)');
    print(response);
    return response as List<dynamic>;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
              onPressed: () async {
                // ログアウトするボタン.
                await Supabase.instance.client.auth.signOut();
                Navigator.of(context).pushReplacement(MaterialPageRoute(
                    builder: (context) => const SigninPage()));
              },
              icon: const Icon(Icons.logout)),
        ],
        title: const Text('Home'),
      ),
      body: FutureBuilder(
        future: relationships(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return const Center(child: Text('エラーが発生しました'));
          }
          final data = snapshot.data as List<dynamic>;
          return ListView.builder(
            itemCount: data.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(data[index]['followings']['name']),
                subtitle: Text(data[index]['followings']['id'].toString()),
              );
            },
          );
        },
      ),
    );
  }
}

[screen shot]
すいません。今回は同じカラムのデータを3回取ってきてるみたいです😅

最後に

SQLを使っていると、リレーションの知識を求めれるようになってきました。やることは、「参照」すること。何をするのか。idを使って、他のテーブルのデータを取得できる。

海外の動画でも参考になりそうなものがありました。綺麗なコードに整えてから仕事では使います。このままでは使いずらい💦
https://www.youtube.com/watch?v=RegqACBCCNc&t=292s

追加情報

Null型のデータをMap<String, dynamic>型として扱うので、モデルクラスを作成するときは、null checkが必要でした💦

AIによると

エラーメッセージから判断すると、Null型のデータをMap<String, dynamic>型として扱おうとしているため、エラーが発生しているようです。これは、取得したデータがnullであるか、または取得したデータの一部がnullで、それをマップとして扱おうとしたときに発生します。 モデルクラスを使用してデータを取得する場合、JSONからモデルクラスへの変換中にnullチェックを行うことが重要です。これにより、nullの値が存在する場合でも適切に処理することができます。 例えば、以下のようにfromJsonメソッドを修正することで、nullチェックを行うことができます。

こんな感じで作ってください。

class Relationships {
  final int followingId;
  final Following followings;

  Relationships({required this.followingId, required this.followings});

  factory Relationships.fromJson(Map<String, dynamic> json) {
    return Relationships(
      followingId: json['following_id'] ?? 0,
      followings: Following.fromJson(json['followings'] ?? {}),
    );
  }
}

class Following {
  final int id;
  final String name;

  Following({required this.id, required this.name});

  factory Following.fromJson(Map<String, dynamic> json) {
    return Following(
      id: json['id'] ?? 0,
      name: json['name'] ?? '',
    );
  }
}

View側のコードも修正。さっきと表示は変わらないですけどね😅

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:follower_app/view/sign_in_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import '../entity/relationships.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  Future<List<dynamic>?> relationships() async {
    final response = await Supabase.instance.client
        .from('relationships')
        .select('following_id, followings(id, name)');
    print(response);
    return response as List<dynamic>;
  }

  // class Relationships data type
  Future<List<Relationships>> fetchRelationships() async {
    try {
      List<Relationships> relationships = [];
      final response = await Supabase.instance.client
          .from('relationships')
          .select('following_id, followings(id, name)');
      for (var data in response) {
        relationships.add(Relationships.fromJson(data));
      }
      return relationships;
    } catch (e) {
      print(e);
    }
    return [];
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
              onPressed: () async {
                // ログアウトするボタン.
                await Supabase.instance.client.auth.signOut();
                Navigator.of(context).pushReplacement(MaterialPageRoute(
                    builder: (context) => const SigninPage()));
              },
              icon: const Icon(Icons.logout)),
        ],
        title: const Text('Home2222'),
      ),
      // body: FutureBuilder(
      //   future: relationships(),
      //   builder: (context, snapshot) {
      //     if (snapshot.connectionState == ConnectionState.waiting) {
      //       return const Center(child: CircularProgressIndicator());
      //     }
      //     if (snapshot.hasError) {
      //       return const Center(child: Text('エラーが発生しました'));
      //     }
      //     final data = snapshot.data as List<dynamic>;
      //     return ListView.builder(
      //       itemCount: data.length,
      //       itemBuilder: (context, index) {
      //         return ListTile(
      //           title: Text(data[index]['followings']['name']),
      //           subtitle: Text(data[index]['followings']['id'].toString()),
      //         );
      //       },
      //     );
      //   },
      // ),
      body: FutureBuilder(
        future: fetchRelationships(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return const Center(child: Text('エラーが発生しました'));
          }
          final data = snapshot.data as List<Relationships>;
          return ListView.builder(
            itemCount: data.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(data[index].followings.name),
                subtitle: Text(data[index].followings.id.toString()),
              );
            },
          );
        },
      ),
    );
  }
}

Discussion