Fix many-to-many

This commit is contained in:
Tobe O 2019-10-09 14:05:53 -04:00
parent 2948304df1
commit 5442ba6f2f
8 changed files with 162 additions and 89 deletions

View file

@ -31,7 +31,12 @@ class JoinBuilder {
} }
String compile(Set<String> trampoline) { String compile(Set<String> trampoline) {
if (to == null) return null; var compiledTo = to();
if (compiledTo == null) {
print(
'NULLLLL $to; from $from; key: $key, value: $value, addl: $additionalFields');
}
if (compiledTo == null) return null;
var b = StringBuffer(); var b = StringBuffer();
var left = '${from.tableName}.$key'; var left = '${from.tableName}.$key';
var right = fieldName; var right = fieldName;
@ -54,7 +59,7 @@ class JoinBuilder {
break; break;
} }
b.write(' ${to()}'); b.write(' $compiledTo');
if (alias != null) b.write(' $alias'); if (alias != null) b.write(' $alias');
b.write(' ON $left$op$right'); b.write(' ON $left$op$right');
return b.toString(); return b.toString();

View file

@ -219,6 +219,8 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
b.write(' '); b.write(' ');
List<String> f; List<String> f;
var compiledJoins = <JoinBuilder, String>{};
if (fields == null) { if (fields == null) {
f = ['*']; f = ['*'];
} else { } else {
@ -229,10 +231,16 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
return ss; return ss;
})); }));
_joins.forEach((j) { _joins.forEach((j) {
var additional = j.additionalFields.map(j.nameFor).toList(); var c = compiledJoins[j] = j.compile(trampoline);
// if (!additional.contains(j.fieldName)) if (c != null) {
// additional.insert(0, j.fieldName); var additional = j.additionalFields.map(j.nameFor).toList();
f.addAll(additional); f.addAll(additional);
} else {
// If compilation failed, fill in NULL placeholders.
for (var i = 0; i < j.additionalFields.length; i++) {
f.add('NULL');
}
}
}); });
} }
if (withFields) b.write(f.join(', ')); if (withFields) b.write(f.join(', '));
@ -243,7 +251,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
if (preamble == null) { if (preamble == null) {
if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
for (var join in _joins) { for (var join in _joins) {
var c = join.compile(trampoline); var c = compiledJoins[join];
if (c != null) b.write(' $c'); if (c != null) b.write(' $c');
} }
} }

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/constant/value.dart';
import 'package:angel_model/angel_model.dart'; import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart'; import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize/angel_serialize.dart'; import 'package:angel_serialize/angel_serialize.dart';

View file

@ -281,6 +281,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
.assign(queryWhereType.newInstance([refer('this')])), .assign(queryWhereType.newInstance([refer('this')])),
); );
// Note: this is where subquery fields for relations are added.
ctx.relations.forEach((fieldName, relation) { ctx.relations.forEach((fieldName, relation) {
//var name = ctx.buildContext.resolveFieldName(fieldName); //var name = ctx.buildContext.resolveFieldName(fieldName);
if (relation.type == RelationshipType.belongsTo || if (relation.type == RelationshipType.belongsTo ||
@ -289,43 +290,103 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
var foreign = relation.throughContext ?? relation.foreign; var foreign = relation.throughContext ?? relation.foreign;
// If this is a many-to-many, add the fields from the other object. // If this is a many-to-many, add the fields from the other object.
var additionalFields = relation.foreign.effectiveFields
// .where((f) => f.name != 'id' || !isSpecialId(ctx, f)) var additionalStrs = relation.foreign.effectiveFields.map((f) =>
.map((f) => literalString(relation.foreign.buildContext relation.foreign.buildContext.resolveFieldName(f.name));
.resolveFieldName(f.name))); var additionalFields = additionalStrs.map(literalString);
var joinArgs = [relation.localKey, relation.foreignKey] var joinArgs = [relation.localKey, relation.foreignKey]
.map(literalString) .map(literalString)
.toList(); .toList();
// In the past, we would either do a join on the table name // In the case of a many-to-many, we don't generate a subquery field,
// itself, or create an instance of a query. // as it easily leads to stack overflows.
// if (relation.isManyToMany) {
// From this point on, however, we will create a field for each // We can't simply join against the "through" table; this itself must
// join, so that users can customize the generated query. // be a join.
// // (SELECT role_users.role_id, <user_fields>
// There'll be a private `_field`, and then a getter, named `field`, // FROM users
// that returns the subqueryb object. // LEFT JOIN role_users ON role_users.user_id=users.id)
var foreignQueryType = refer( var foreignFields = additionalStrs
foreign.buildContext.modelClassNameRecase.pascalCase + .map((f) => '${relation.foreign.tableName}.$f');
'Query'); var b = StringBuffer('(SELECT ');
clazz // role_users.role_id
..fields.add(Field((b) => b b.write('${relation.throughContext.tableName}');
..name = '_$fieldName' b.write('.${relation.foreignKey}');
..type = foreignQueryType)) // , <user_fields>
..methods.add(Method((b) => b b.write(foreignFields.isEmpty
..name = fieldName ? ''
..type = MethodType.getter : ', ' + foreignFields.join(', '));
..returns = foreignQueryType // FROM users
..body = refer('_$fieldName').returned.statement)); b.write(' FROM ');
b.write(relation.foreign.tableName);
// LEFT JOIN role_users
b.write(' LEFT JOIN ${relation.throughContext.tableName}');
// Figure out which field on the "through" table points to users (foreign).
var throughRelation =
relation.throughContext.relations.values.firstWhere((e) {
return e.foreignTable == relation.foreign.tableName;
}, orElse: () {
// _Role has a many-to-many to _User through _RoleUser, but
// _RoleUser has no relation pointing to _User.
var b = StringBuffer();
b.write(ctx.buildContext.modelClassName);
b.write('has a many-to-many relationship to ');
b.write(relation.foreign.buildContext.modelClassName);
b.write(' through ');
b.write(
relation.throughContext.buildContext.modelClassName);
b.write(', but ');
b.write(
relation.throughContext.buildContext.modelClassName);
b.write('has no relation pointing to ');
b.write(relation.foreign.buildContext.modelClassName);
b.write('.');
throw b.toString();
});
// Assign a value to `_field`. // ON role_users.user_id=users.id)
var queryInstantiation = foreignQueryType.newInstance([], { b.write(' ON ');
'trampoline': refer('trampoline'), b.write('${relation.throughContext.tableName}');
'parent': refer('this') b.write('.');
}); b.write(throughRelation.localKey);
joinArgs.insert( b.write('=');
0, refer('_$fieldName').assign(queryInstantiation)); b.write(relation.foreign.tableName);
b.write('.');
b.write(throughRelation.foreignKey);
b.write(')');
joinArgs.insert(0, literalString(b.toString()));
} else {
// In the past, we would either do a join on the table name
// itself, or create an instance of a query.
//
// From this point on, however, we will create a field for each
// join, so that users can customize the generated query.
//
// There'll be a private `_field`, and then a getter, named `field`,
// that returns the subquery object.
var foreignQueryType = refer(
foreign.buildContext.modelClassNameRecase.pascalCase +
'Query');
clazz
..fields.add(Field((b) => b
..name = '_$fieldName'
..type = foreignQueryType))
..methods.add(Method((b) => b
..name = fieldName
..type = MethodType.getter
..returns = foreignQueryType
..body = refer('_$fieldName').returned.statement));
// Assign a value to `_field`.
var queryInstantiation = foreignQueryType.newInstance([], {
'trampoline': refer('trampoline'),
'parent': refer('this')
});
joinArgs.insert(
0, refer('_$fieldName').assign(queryInstantiation));
}
var joinType = relation.joinTypeString; var joinType = relation.joinTypeString;
b.addExpression(refer(joinType).call(joinArgs, { b.addExpression(refer(joinType).call(joinArgs, {

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:angel_orm/angel_orm.dart'; import 'package:angel_orm/angel_orm.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'models/user.dart'; import 'models/user.dart';
import 'util.dart';
manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor, manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor,
{FutureOr<void> Function(QueryExecutor) close}) { {FutureOr<void> Function(QueryExecutor) close}) {
@ -61,6 +62,7 @@ manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor,
print('=== THOSAKWE: ${thosakwe?.toJson()}'); print('=== THOSAKWE: ${thosakwe?.toJson()}');
// Allow thosakwe to publish... // Allow thosakwe to publish...
printSeparator('Allow thosakwe to publish');
var thosakwePubQuery = RoleUserQuery(); var thosakwePubQuery = RoleUserQuery();
thosakwePubQuery.values thosakwePubQuery.values
..userId = int.parse(thosakwe.id) ..userId = int.parse(thosakwe.id)
@ -68,6 +70,7 @@ manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor,
await thosakwePubQuery.insert(executor); await thosakwePubQuery.insert(executor);
// Allow thosakwe to subscribe... // Allow thosakwe to subscribe...
printSeparator('Allow thosakwe to subscribe');
var thosakweSubQuery = RoleUserQuery(); var thosakweSubQuery = RoleUserQuery();
thosakweSubQuery.values thosakweSubQuery.values
..userId = int.parse(thosakwe.id) ..userId = int.parse(thosakwe.id)
@ -78,8 +81,8 @@ manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor,
// await dumpQuery('select * from users;'); // await dumpQuery('select * from users;');
// await dumpQuery('select * from roles;'); // await dumpQuery('select * from roles;');
// await dumpQuery('select * from role_users;'); // await dumpQuery('select * from role_users;');
var query = RoleQuery()..where.id.equals(canPub.idAsInt); // var query = RoleQuery()..where.id.equals(canPub.idAsInt);
await dumpQuery(query.compile(Set())); // await dumpQuery(query.compile(Set()));
print('\n'); print('\n');
print('=================================================='); print('==================================================');
@ -95,6 +98,7 @@ manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor,
} }
test('fetch roles for user', () async { test('fetch roles for user', () async {
printSeparator('Fetch roles for user test');
var user = await fetchThosakwe(); var user = await fetchThosakwe();
expect(user.roles, hasLength(2)); expect(user.roles, hasLength(2));
expect(user.roles, contains(canPub)); expect(user.roles, contains(canPub));
@ -108,4 +112,21 @@ manyToManyTests(FutureOr<QueryExecutor> Function() createExecutor,
expect(r.users.toList(), [thosakwe]); expect(r.users.toList(), [thosakwe]);
} }
}); });
test('only fetches linked', () async {
// Create a new user. The roles list should be empty,
// be there are no related rules.
var userQuery = UserQuery();
userQuery.values
..username = 'Prince'
..password = 'Rogers'
..email = 'Nelson';
var user = await userQuery.insert(executor);
expect(user.roles, isEmpty);
// Fetch again, just to be doubly sure.
var query = UserQuery()..where.id.equals(user.idAsInt);
var fetched = await query.getOne(executor);
expect(fetched.roles, isEmpty);
});
} }

View file

@ -64,8 +64,10 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = RoleQueryWhere(this); _where = RoleQueryWhere(this);
leftJoin(_users = RoleUserQuery(trampoline: trampoline, parent: this), leftJoin(
'role', 'role_role', '(SELECT role_users.role_role , users.email, users.name, users.password FROM users LEFT JOIN role_users ON role_users.user_email=users.email)',
'role',
'role_role',
additionalFields: const ['email', 'name', 'password'], additionalFields: const ['email', 'name', 'password'],
trampoline: trampoline); trampoline: trampoline);
} }
@ -75,8 +77,6 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
RoleQueryWhere _where; RoleQueryWhere _where;
RoleUserQuery _users;
@override @override
get casts { get casts {
return {}; return {};
@ -119,10 +119,6 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
return parseRow(row); return parseRow(row);
} }
RoleUserQuery get users {
return _users;
}
@override @override
bool canCompile(trampoline) { bool canCompile(trampoline) {
return (!(trampoline.contains('roles') && return (!(trampoline.contains('roles') &&
@ -338,9 +334,12 @@ class UserQuery extends Query<User, UserQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = UserQueryWhere(this); _where = UserQueryWhere(this);
leftJoin(_roles = RoleUserQuery(trampoline: trampoline, parent: this), leftJoin(
'email', 'user_email', '(SELECT role_users.user_email , roles.role FROM roles LEFT JOIN role_users ON role_users.role_role=roles.role)',
additionalFields: const ['role'], trampoline: trampoline); 'email',
'user_email',
additionalFields: const ['role'],
trampoline: trampoline);
} }
@override @override
@ -348,8 +347,6 @@ class UserQuery extends Query<User, UserQueryWhere> {
UserQueryWhere _where; UserQueryWhere _where;
RoleUserQuery _roles;
@override @override
get casts { get casts {
return {}; return {};
@ -395,10 +392,6 @@ class UserQuery extends Query<User, UserQueryWhere> {
return parseRow(row); return parseRow(row);
} }
RoleUserQuery get roles {
return _roles;
}
@override @override
bool canCompile(trampoline) { bool canCompile(trampoline) {
return (!(trampoline.contains('users') && return (!(trampoline.contains('users') &&

View file

@ -208,9 +208,12 @@ class WeirdJoinQuery extends Query<WeirdJoin, WeirdJoinQueryWhere> {
leftJoin(_numbas = NumbaQuery(trampoline: trampoline, parent: this), 'id', leftJoin(_numbas = NumbaQuery(trampoline: trampoline, parent: this), 'id',
'parent', 'parent',
additionalFields: const ['i', 'parent'], trampoline: trampoline); additionalFields: const ['i', 'parent'], trampoline: trampoline);
leftJoin(_foos = FooPivotQuery(trampoline: trampoline, parent: this), 'id', leftJoin(
'(SELECT foo_pivots.weird_join_id , foos.bar FROM foos LEFT JOIN foo_pivots ON foo_pivots.foo_bar=foos.bar)',
'id',
'weird_join_id', 'weird_join_id',
additionalFields: const ['bar'], trampoline: trampoline); additionalFields: const ['bar'],
trampoline: trampoline);
} }
@override @override
@ -224,8 +227,6 @@ class WeirdJoinQuery extends Query<WeirdJoin, WeirdJoinQueryWhere> {
NumbaQuery _numbas; NumbaQuery _numbas;
FooPivotQuery _foos;
@override @override
get casts { get casts {
return {}; return {};
@ -294,10 +295,6 @@ class WeirdJoinQuery extends Query<WeirdJoin, WeirdJoinQueryWhere> {
return _numbas; return _numbas;
} }
FooPivotQuery get foos {
return _foos;
}
@override @override
bool canCompile(trampoline) { bool canCompile(trampoline) {
return (!(trampoline.contains('weird_joins') && return (!(trampoline.contains('weird_joins') &&
@ -612,9 +609,12 @@ class FooQuery extends Query<Foo, FooQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = FooQueryWhere(this); _where = FooQueryWhere(this);
leftJoin(_weirdJoins = FooPivotQuery(trampoline: trampoline, parent: this), leftJoin(
'bar', 'foo_bar', '(SELECT foo_pivots.foo_bar , weird_joins.id, weird_joins.join_name FROM weird_joins LEFT JOIN foo_pivots ON foo_pivots.weird_join_id=weird_joins.id)',
additionalFields: const ['id', 'join_name'], trampoline: trampoline); 'bar',
'foo_bar',
additionalFields: const ['id', 'join_name'],
trampoline: trampoline);
} }
@override @override
@ -622,8 +622,6 @@ class FooQuery extends Query<Foo, FooQueryWhere> {
FooQueryWhere _where; FooQueryWhere _where;
FooPivotQuery _weirdJoins;
@override @override
get casts { get casts {
return {}; return {};
@ -666,10 +664,6 @@ class FooQuery extends Query<Foo, FooQueryWhere> {
return parseRow(row); return parseRow(row);
} }
FooPivotQuery get weirdJoins {
return _weirdJoins;
}
@override @override
bool canCompile(trampoline) { bool canCompile(trampoline) {
return (!(trampoline.contains('foos') && return (!(trampoline.contains('foos') &&

View file

@ -66,7 +66,9 @@ class UserQuery extends Query<User, UserQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = UserQueryWhere(this); _where = UserQueryWhere(this);
leftJoin(_roles = RoleUserQuery(trampoline: trampoline, parent: this), 'id', leftJoin(
'(SELECT role_users.user_id , roles.id, roles.created_at, roles.updated_at, roles.name FROM roles LEFT JOIN role_users ON role_users.role_id=roles.id)',
'id',
'user_id', 'user_id',
additionalFields: const ['id', 'created_at', 'updated_at', 'name'], additionalFields: const ['id', 'created_at', 'updated_at', 'name'],
trampoline: trampoline); trampoline: trampoline);
@ -77,8 +79,6 @@ class UserQuery extends Query<User, UserQueryWhere> {
UserQueryWhere _where; UserQueryWhere _where;
RoleUserQuery _roles;
@override @override
get casts { get casts {
return {}; return {};
@ -134,10 +134,6 @@ class UserQuery extends Query<User, UserQueryWhere> {
return parseRow(row); return parseRow(row);
} }
RoleUserQuery get roles {
return _roles;
}
@override @override
bool canCompile(trampoline) { bool canCompile(trampoline) {
return (!(trampoline.contains('users') && return (!(trampoline.contains('users') &&
@ -405,7 +401,9 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = RoleQueryWhere(this); _where = RoleQueryWhere(this);
leftJoin(_users = RoleUserQuery(trampoline: trampoline, parent: this), 'id', leftJoin(
'(SELECT role_users.role_id , users.id, users.created_at, users.updated_at, users.username, users.password, users.email FROM users LEFT JOIN role_users ON role_users.user_id=users.id)',
'id',
'role_id', 'role_id',
additionalFields: const [ additionalFields: const [
'id', 'id',
@ -423,8 +421,6 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
RoleQueryWhere _where; RoleQueryWhere _where;
RoleUserQuery _users;
@override @override
get casts { get casts {
return {}; return {};
@ -471,10 +467,6 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
return parseRow(row); return parseRow(row);
} }
RoleUserQuery get users {
return _users;
}
@override @override
bool canCompile(trampoline) { bool canCompile(trampoline) {
return (!(trampoline.contains('roles') && return (!(trampoline.contains('roles') &&