-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simple many-to-many joins? #528
Comments
@schvenk are you defining |
Sorry for all the questions! Feel free to point me to the right spots in the docs...I just haven't been able to find what I'm looking for there but maybe I'm missing something. Anyway. Question 1: What do I need to do to define
Question 2: How would that |
@schvenk So part 'tag.model.g.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'tasks_tags'),
)
@CopyWith()
class TaskTag extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
final Task task;
final Tag tag;
TaskTag({
required this.task,
required this.tag,
}) : id = id ?? const Uuid().v4();
} However, looking at this now, you can't have a So what you'll likely need is something closer to this: part 'tag.model.g.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'tasks_tags'),
)
@CopyWith()
class TaskTag extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
@Supabase(ignore: true)
final Task task;
@Sqlite(ignore: true)
String get taskId => task.id;
@Supabase(ignore: true)
final Tag tag;
@Sqlite(ignore: true)
String get tagId => tag.id;
TaskTag({
required this.task,
required this.tag,
}) : id = id ?? const Uuid().v4();
} And then your tag model can be @ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'tags'),
)
@CopyWith()
class Tag extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
final String name;
final String? type;
final DateTime? lastUsedAt;
final int? sortScore;
final List<TaskTag> tasksTags;
Tag({
String? id,
required this.name,
this.type,
this.lastUsedAt,
this.sortScore,
required this.taskTags,
}) : id = id ?? const Uuid().v4();
} This is all untested, so I'd encourage you to play with it and see if it fits your schema / use case. To answer your second question, Brick determines if a field is an association using Dart's analyzer as part of the |
Thanks! But...as you surmised I'm getting errors, not in the model files themselves but at code-generation time. Is there a good spot in the documentation for me to read to understand the details e.g., the various types of ignore, when to use the object vs. the ID vs. both as the relation field, and so on? |
should the final |
I wouldn't have expected this. What errors?
You can try looking at the KitchenSink model to see how code is generated. There's a note on recursive association generation for Supabase specifically and generic annotation documentation. As a generally single maintainer, I've found it difficult to keep the documentation and code in sync, so the most up-to-date information will be in the Dart doc of what you're trying to use. |
This would theoretically be the join table described in the initial comment. There'd be a PK column, a column for |
Commenting on this to see if I can understand this concept or get support: @ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'purchases'),
)
class Purchase extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
@Supabase(ignore: true)
// @Sqlite(ignore: true)
List<Variant>? variants;
}
and @ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'variants'),
)
class Variant extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
@Sqlite(index: true)
final String? purchaseId;
} I have Purchase purchase = Purchase(
spplrTin: "11",
spplrNm: id,
spplrBhfId: "01",
spplrInvcNo: 1,
rcptTyCd: "N",
pmtTyCd: "N",
cfmDt: "cfmDt",
salesDt: "salesDt",
totItemCnt: 1,
taxblAmtA: 1.0,
taxblAmtB: 1.0,
taxblAmtC: 1.0,
taxblAmtD: 1.0,
taxRtA: 1.0,
taxRtB: 1.0,
taxRtC: 1.0,
taxRtD: 1.0,
taxAmtA: 1.0,
taxAmtB: 1.0,
taxAmtC: 1.0,
taxAmtD: 1.0,
totTaxblAmt: 1.0,
totTaxAmt: 1.0,
totAmt: 1.0,
);
repository.upsert<Purchase>(purchase);
Variant variant = Variant(name: "name", purchaseId:purchase.id);
repository.upsert<Variant>(purchase);
On supabase I have made purchase_id part of the variant to be the foreign key of purchase and I can see data in supabase |
I even wrote a test but it still fail: import 'package:supabase_models/brick/brick.g.dart';
import 'package:brick_supabase/src/supabase_provider.dart';
import 'package:test/test.dart';
import 'package:brick_supabase/testing.dart';
import 'package:uuid/uuid.dart';
void main() {
final mock = SupabaseMockServer(modelDictionary: supabaseModelDictionary);
group('Purchase with Variants', () {
setUp(mock.setUp);
tearDown(mock.tearDown);
test('#getPurchaseWithVariants', () async {
// Create mock Variant data
final variant1 = Variant(
id: const Uuid().v4(),
name: 'Variant 1',
purchaseId: '1', // Link to the Purchase
taxPercentage: 18.0,
);
final variant2 = Variant(
id: const Uuid().v4(),
name: 'Variant 2',
purchaseId: '1', // Link to the Purchase
taxPercentage: 18.0,
);
// Create mock Purchase data with Variants
final purchase = Purchase(
id: '1',
spplrTin: '123456789',
spplrNm: 'Supplier Name',
spplrBhfId: 'BH123',
spplrInvcNo: 1001,
rcptTyCd: 'RCPT001',
pmtTyCd: 'PMT001',
cfmDt: '2023-10-01',
salesDt: '2023-10-01',
totItemCnt: 2,
taxblAmtA: 100.0,
taxblAmtB: 200.0,
taxblAmtC: 300.0,
taxblAmtD: 400.0,
taxRtA: 10.0,
taxRtB: 20.0,
taxRtC: 30.0,
taxRtD: 40.0,
taxAmtA: 10.0,
taxAmtB: 40.0,
taxAmtC: 90.0,
taxAmtD: 160.0,
totTaxblAmt: 1000.0,
totTaxAmt: 300.0,
totAmt: 1300.0,
variants: [variant1, variant2], // Include Variants in the Purchase
);
// Mock Supabase request and response
final req = SupabaseRequest<Purchase>();
final resp = SupabaseResponse([
await mock.serialize(purchase),
]);
// Stub the server with the mock data
mock.handle({req: resp});
// Initialize the provider
final provider = SupabaseProvider(mock.client,
modelDictionary: supabaseModelDictionary);
// Retrieve the Purchase
final retrieved = await provider.get<Purchase>();
// Assertions
expect(retrieved, hasLength(1)); // Ensure one Purchase is returned
expect(
retrieved.first.variants, isNotNull); // Ensure Variants are not null
expect(retrieved.first.variants,
hasLength(2)); // Ensure there are 2 Variants
expect(retrieved.first.variants?.first.name,
'Variant 1'); // Check Variant data
expect(retrieved.first.variants?.last.name,
'Variant 2'); // Check Variant data
});
}); |
@richard457 I think you may have it backwards. I think you need a @ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'purchases'),
)
class Purchase extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
final List<Variant> variants;
} You would then access variants by purchase.variants
In your model definition, you have |
@tshedor Thank you very much and sorry to trouble you, a small clarification to my model: I removed @supabase(ignore: true) from the Purchase model but then on upsert supabase complained that model variants do not exist on the table, I thought of adding to the table with JSONB TYPE but I think that is wrong. |
@richard457 In this case you may need to do @Supabase(ignoreTo: true)
final List<Variant> variants; and (assuming you have a final existingPurchaseVariants = await Repository().get<PurchaseVariant>(query: Query.where('purchaseId', purchase.id));
final variantsToKeep = <PurchaseVariant>{};
for (final variant in purchase.variants) {
final purchaseVariant = existingPurchaseVariants.firstWhere(
(p) => p.purchaseId == purchase.id && p.variantId == variant.id,
orElse: PurchaseVariant(purchaseId: purchase.id, variantId: variant.id),
);
if (purchaseVariant.id == null {
await Repository().upsert<PurchaseVariant>(purchaseVariant);
} else {
variantsToKeep.add(purchaseVariant);
}
}
final variantsToDelete = existingPurchaseVariants.where((p) => !variantsToKeep.contains(p));
for (final variant in variantsToDelete) {
await Repository().delete<PurchaseVariant>(variant);
} Untested code; your logic may differ |
Thank you, that really helped me, I now have progress final purchases = await repository.get<Purchase>();
for (final purchase in purchases) {
if (purchase.variants != null) {
for (final variant in purchase.variants!) {
print('Variant ID: ${variant.id}');
}
}
} but final purchases = await repository.get<Purchase>(
policy: OfflineFirstGetPolicy.alwaysHydrate,
query: brick.Query(
where: [brick.Where('spplrTin').isExactly("34cd7397f16918e")])); the purchase.variants will be null class Purchase extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
List<Variant>? variants;
} @ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'variants'),
)
class Variant extends OfflineFirstWithSupabaseModel {
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
@Supabase(foreignKey: 'purchase_id')
@Sqlite(index: true)
String? purchased;
} |
The problem happens when I add a query to await repository.get(); it will not populate the relationship |
I don't understand what the problem is, can you please consolidate your comments to a single message with relevant samples? |
@tshedor I will try to summarize my issue as much as I can though I must accept that I might have skill issue regarding brick I apologize for that: Question: I might be experiencing some challenges regarding how Brick handles one-to-many relationships. I'll summarize the issue and my expectations to make it clear: ContextI have two models:
Model B contains a list of Model A objects ( ExpectationsWhen querying for an instance of Model B using Brick (e.g., ObservationThis expectation is not consistently met. Specifically:
RequestI would like to understand:
|
@richard457 splitting your question to #533 . This problem is one-to-many joins instead of this issue's original many-to-many. @schvenk This has gotten a little muddy. I'd like to bring it back to your original questions. Does this comment help? |
I'm building a to-do list app backed by Supabase. Each to-do can have zero or more tags. So, in the DB I have a
tasks
table, atags
table, and atasks_tags
table so a user can have multiple tags, associate a task with as many of them as she likes, and it's easy to look up a task by its tags (e.g., to display all the tasks for a given tag). I'm not married to this particular schema, but it seemed like the most standard/straightforward.When fetching a task, Brick seems to do a great job of pulling down the tags as an array! But when updating a task, I run into errors like:
In other words, on upsert it's not recognizing that
tags
is synthesized from the relation. Is there a way Brick cane handle this for me?If not, maybe that's OK. Before starting to integrate Brick, I was talking to Supabase directly. I simply omitted
tags
from the JSON, then updated the tags separately. But I'm not sure how to do that either since thetask
model has atags
field and even if I set it to null, it's included in Brick's query to Supabase.Thanks!
The text was updated successfully, but these errors were encountered: