Updated orm generator

This commit is contained in:
thomashii 2021-08-15 16:05:22 +08:00
parent 1f57b79c6d
commit 6f15e76d65
5 changed files with 196 additions and 100 deletions

View file

@ -1,8 +1,14 @@
# Change Log
## 4.1.1
* Fixed `NumericSqlExpressionBuilder` to handle nullable field
* Fixed `@belongsTo` code generation
* Fixed `copyFrom` to handle nullable relationship
## 4.1.0
* Upgraded to support major `analyzer` 2.0.0 release
* Upgraded to support `analyzer` 2.0.0 major release
## 4.0.2

View file

@ -1,6 +1,6 @@
# Angel3 ORM Generator
[![version](https://img.shields.io/badge/pub-v4.1.0-brightgreen)](https://pub.dartlang.org/packages/angel3_orm_generator)
[![version](https://img.shields.io/badge/pub-v4.1.1-brightgreen)](https://pub.dartlang.org/packages/angel3_orm_generator)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)

View file

@ -12,24 +12,21 @@ import 'package:angel3_serialize_generator/context.dart';
import 'package:build/build.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:inflection3/inflection3.dart';
import 'package:logging/logging.dart';
import 'package:recase/recase.dart';
import 'package:source_gen/source_gen.dart';
import 'readers.dart';
var _log = Logger('orm_build_context');
bool isHasRelation(Relationship r) =>
r.type == RelationshipType.hasOne || r.type == RelationshipType.hasMany;
bool isSpecialId(OrmBuildContext? ctx, FieldElement field) {
bool isSpecialId(OrmBuildContext ctx, FieldElement field) {
return
// field is ShimFieldImpl &&
field is! RelationFieldImpl &&
(field.name == 'id' &&
const TypeChecker.fromRuntime(Model)
.isAssignableFromType(ctx!.buildContext.clazz.thisType));
.isAssignableFromType(ctx.buildContext.clazz.thisType));
}
Element _findElement(FieldElement field) {
@ -37,7 +34,7 @@ Element _findElement(FieldElement field) {
}
FieldElement? findPrimaryFieldInList(
OrmBuildContext? ctx, Iterable<FieldElement> fields) {
OrmBuildContext ctx, Iterable<FieldElement> fields) {
for (var field_ in fields) {
var field = field_ is RelationFieldImpl ? field_.originalField : field_;
var element = _findElement(field);
@ -93,7 +90,7 @@ Future<OrmBuildContext?> buildOrmContext(
// print(
// 'tableName (${annotation.objectValue.type.name}) => ${ormAnnotation.tableName} from ${clazz.name} (${annotation.revive().namedArguments})');
if (buildCtx == null) {
_log.severe('BuildContext is null');
log.severe('BuildContext is null');
return null;
}
@ -175,38 +172,45 @@ Future<OrmBuildContext?> buildOrmContext(
}
var modelType = firstModelAncestor(refType) ?? refType;
var modelTypeElement = modelType.element;
foreign = await buildOrmContext(
cache,
modelType.element as ClassElement,
ConstantReader(const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelType.element!)),
buildStep,
resolver,
autoSnakeCaseNames);
// Resolve throughType as well
if (through != null && through is InterfaceType) {
throughContext = await buildOrmContext(
if (modelTypeElement != null) {
foreign = await buildOrmContext(
cache,
through.element,
ConstantReader(const TypeChecker.fromRuntime(Serializable)
.firstAnnotationOf(modelType.element!)),
modelTypeElement as ClassElement,
ConstantReader(const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelTypeElement)),
buildStep,
resolver,
autoSnakeCaseNames);
// Resolve throughType as well
if (through != null && through is InterfaceType) {
throughContext = await buildOrmContext(
cache,
through.element,
ConstantReader(const TypeChecker.fromRuntime(Serializable)
.firstAnnotationOf(modelTypeElement)),
buildStep,
resolver,
autoSnakeCaseNames);
}
var ormAnn = const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelTypeElement);
if (ormAnn != null) {
foreignTable =
ConstantReader(ormAnn).peek('tableName')?.stringValue;
}
if (foreign != null) {
foreignTable ??= pluralize(
foreign.buildContext.modelClassNameRecase.snakeCase);
}
} else {
log.warning('Ancestor model type for [${field.name}] is null');
}
var ormAnn = const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelType.element!);
if (ormAnn != null) {
foreignTable =
ConstantReader(ormAnn).peek('tableName')?.stringValue;
}
foreignTable ??=
pluralize(foreign!.buildContext.modelClassNameRecase.snakeCase);
} on StackOverflowError {
throw UnsupportedError(
'There is an infinite cycle between ${clazz.name} and ${field.type.getDisplayString(withNullability: true)}. This triggered a stack overflow.');
@ -272,8 +276,8 @@ Future<OrmBuildContext?> buildOrmContext(
joinType: joinType,
);
// print('Relation on ${buildCtx.originalClassName}.${field.name} => '
// 'foreignKey=$foreignKey, localKey=$localKey');
log.fine('Relation on ${buildCtx.originalClassName}.${field.name} => '
'foreignKey=$foreignKey, localKey=$localKey');
if (relation.type == RelationshipType.belongsTo) {
var localKey = relation.localKey;
@ -285,9 +289,12 @@ Future<OrmBuildContext?> buildOrmContext(
var foreignField = relation.findForeignField(ctx);
var foreign = relation.throughContext ?? relation.foreign;
var type = foreignField.type;
if (isSpecialId(foreign, foreignField)) {
//type = field.type.element.context.typeProvider.intType;
type = field.type;
if (foreign != null) {
if (isSpecialId(foreign, foreignField)) {
//type = field.type.element.context.typeProvider.intType;
type = field.type;
}
}
var rf = RelationFieldImpl(name, relation, type, field);
ctx.effectiveFields.add(rf);

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel3_orm/angel3_orm.dart';
import 'package:angel3_serialize_generator/angel3_serialize_generator.dart';
@ -29,7 +30,7 @@ TypeReference futureOf(String type) {
..types.add(refer(type)));
}
/// Builder that generates `.orm.g.dart`, with an abstract `FooOrm` class.
/// Builder that generates `<Model>.g.dart` from an abstract `Model` class.
class OrmGenerator extends GeneratorForAnnotation<Orm> {
final bool? autoSnakeCaseNames;
@ -44,6 +45,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
if (ctx == null) {
throw 'Invalid ORM build context';
}
var lib = buildOrmLibrary(buildStep.inputId, ctx);
return lib.accept(DartEmitter(useNullSafetySyntax: true)).toString();
@ -65,6 +67,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
});
}
/// Generate <Model>Query class
Class buildQueryClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
@ -136,6 +139,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
// Add fields getter
clazz.methods.add(Method((m) {
//log.fine('Field: $name');
m
..name = 'fields'
..returns = TypeReference((b) => b
@ -225,7 +229,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
}
b.statements
.add(Code('if (row.every((x) => x == null)) return null;'));
.add(Code('if (row.every((x) => x == null)) { return null; }'));
b.addExpression(ctx.buildContext.modelClassType
.newInstance([], args).assignVar('model'));
@ -234,13 +238,23 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
RelationshipType.hasOne,
RelationshipType.belongsTo,
RelationshipType.hasMany
].contains(relation.type)) return;
var foreign = relation.foreign!;
].contains(relation.type)) {
return;
}
//log.fine('Process relationship');
var foreign = relation.foreign;
if (foreign == null) {
log.warning('Foreign is null');
return;
}
//log.fine('Detected relationship ${RelationshipType.belongsTo}');
var skipToList = refer('row')
.property('skip')
.call([literalNum(i)])
.property('take')
.call([literalNum(relation.foreign!.effectiveFields.length)])
.call([literalNum(foreign.effectiveFields.length)])
.property('toList')
.call([]);
var parsed = refer(
@ -261,7 +275,8 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
block.accept(DartEmitter(useNullSafetySyntax: true));
var ifStr = 'if (row.length > $i) { $blockStr }';
b.statements.add(Code(ifStr));
i += relation.foreign!.effectiveFields.length;
i += foreign.effectiveFields.length;
});
b.addExpression(refer('model').returned);
@ -322,24 +337,43 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
);
// Note: this is where subquery fields for relations are added.
ctx.relations.forEach((fieldName, relation) {
//var name = ctx.buildContext.resolveFieldName(fieldName);
if (relation.type == RelationshipType.belongsTo ||
relation.type == RelationshipType.hasOne ||
relation.type == RelationshipType.hasMany) {
var foreign = relation.throughContext ?? relation.foreign;
//
// @ManyToMany(_RoleUser) => relation.throughContext
// List<_User> get users; => relation.foreign
//
var relationForeign = relation.foreign;
if (relationForeign == null) {
log.warning('$fieldName has no relationship in the context');
return;
}
var relationContext =
relation.throughContext ?? relation.foreign;
log.fine(
'$fieldName relation.throughContext => ${relation.throughContext?.tableName} relation.foreign => ${relation.foreign?.tableName}');
// If this is a many-to-many, add the fields from the other object.
var additionalStrs = relationForeign.effectiveFields.map((f) =>
relationForeign.buildContext.resolveFieldName(f.name));
var additionalStrs = relation.foreign!.effectiveFields.map(
(f) => relation.foreign!.buildContext
.resolveFieldName(f.name));
var additionalFields = additionalStrs
.map(literalString as Expression Function(String?));
var additionalFields = <Expression>[];
additionalStrs.forEach((element) {
if (element != null) {
additionalFields.add(literalString(element));
}
});
var joinArgs = [relation.localKey, relation.foreignKey]
.map(literalString as Function(String?))
.toList() as List<Expression>;
var joinArgs = <Expression>[];
[relation.localKey, relation.foreignKey].forEach((element) {
if (element != null) {
joinArgs.add(literalString(element));
}
});
// In the case of a many-to-many, we don't generate a subquery field,
// as it easily leads to stack overflows.
@ -350,10 +384,10 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
// FROM users
// LEFT JOIN role_users ON role_users.user_id=users.id)
var foreignFields = additionalStrs
.map((f) => '${relation.foreign!.tableName}.$f');
.map((f) => '${relationForeign.tableName}.$f');
var b = StringBuffer('(SELECT ');
// role_users.role_id
b.write('${relation.throughContext!.tableName}');
b.write('${relationContext?.tableName}');
b.write('.${relation.foreignKey}');
// , <user_fields>
b.write(foreignFields.isEmpty
@ -361,28 +395,31 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
: ', ' + foreignFields.join(', '));
// FROM users
b.write(' FROM ');
b.write(relation.foreign!.tableName);
b.write(relationForeign.tableName);
// LEFT JOIN role_users
b.write(' LEFT JOIN ${relation.throughContext!.tableName}');
// Figure out which field on the "through" table points to users (foreign).
b.write(' LEFT JOIN ${relationContext?.tableName}');
// Figure out which field on the "through" table points to users (foreign)
log.fine('$fieldName query => ${b.toString()}');
var throughRelation =
relation.throughContext!.relations.values.firstWhere((e) {
return e.foreignTable == relation.foreign!.tableName;
relationContext?.relations.values.firstWhere((e) {
log.fine(
'ForeignTable(Rel) => ${e.foreignTable}, ${relationForeign.tableName}');
return e.foreignTable == relationForeign.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(' has a many-to-many relationship to ');
b.write(relationForeign.buildContext.modelClassName);
b.write(' through ');
b.write(
relation.throughContext!.buildContext.modelClassName);
b.write(relationContext.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(relationContext.buildContext.modelClassName);
b.write(' has no relation pointing to ');
b.write(ctx.buildContext.modelClassName);
b.write('.');
throw b.toString();
});
@ -391,11 +428,11 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
b.write(' ON ');
b.write('${relation.throughContext!.tableName}');
b.write('.');
b.write(throughRelation.localKey);
b.write(throughRelation?.localKey);
b.write('=');
b.write(relation.foreign!.tableName);
b.write(relationForeign.tableName);
b.write('.');
b.write(throughRelation.foreignKey);
b.write(throughRelation?.foreignKey);
b.write(')');
joinArgs.insert(0, literalString(b.toString()));
@ -408,12 +445,15 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
//
// 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');
var foreignQueryType = refer(relationForeign
.buildContext.modelClassNameRecase.pascalCase +
'Query');
clazz
..fields.add(Field((b) => b
..name = '_$fieldName'
..late = true
..type = foreignQueryType))
..methods.add(Method((b) => b
..name = fieldName
@ -492,7 +532,10 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
var field =
ctx.buildContext.fields.firstWhere((f) => f.name == name);
var typeLiteral = convertTypeReference(field.type)
.accept(DartEmitter(useNullSafetySyntax: true));
.accept(DartEmitter(useNullSafetySyntax: true))
.toString()
.replaceAll('?', '');
merge.add('''
$name: $typeLiteral.from(l.$name ?? [])..addAll(model.$name ?? [])
''');
@ -528,6 +571,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
});
}
/// Generate <Model>QueryWhere class
Class buildWhereClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
@ -552,8 +596,10 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
var initializers = <Code>[];
// Add builders for each field
for (var field in ctx.effectiveNormalFields) {
String? name = field.name;
var args = <Expression>[];
DartType type;
Reference builderType;
@ -567,11 +613,14 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
if (const TypeChecker.fromRuntime(int).isExactlyType(type) ||
const TypeChecker.fromRuntime(double).isExactlyType(type) ||
isSpecialId(ctx, field)) {
var typeName = type.getDisplayString(withNullability: false);
if (isSpecialId(ctx, field)) {
typeName = 'int';
}
//log.fine('$name type = [$typeName]');
builderType = TypeReference((b) => b
..symbol = 'NumericSqlExpressionBuilder'
..types.add(refer(isSpecialId(ctx, field)
? 'int'
: type.getDisplayString(withNullability: true))));
..types.add(refer('$typeName')));
} else if (type is InterfaceType && type.element.isEnum) {
builderType = TypeReference((b) => b
..symbol = 'EnumSqlExpressionBuilder'
@ -590,35 +639,58 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(type)) {
builderType = refer('ListSqlExpressionBuilder');
} else if (ctx.relations.containsKey(field.name)) {
var relation = ctx.relations[field.name]!;
if (relation.type != RelationshipType.belongsTo) {
continue;
} else {
// Detect relationship
} else if (name.endsWith('Id')) {
log.fine('Relationship detected = $name');
var relation = ctx.relations[name.replaceAll('Id', '')];
if (relation != null) {
//if (relation?.type != RelationshipType.belongsTo) {
// continue;
//} else {
builderType = TypeReference((b) => b
..symbol = 'NumericSqlExpressionBuilder'
..types.add(refer('int')));
name = relation.localKey;
//name = relation?.localKey;
//}
} else {
log.warning(
'Cannot generate ORM code for field ${field.name} of type ${field.type}');
continue;
}
} else {
throw UnsupportedError(
'Cannot generate ORM code for field of type ${field.type.getDisplayString(withNullability: false)}.');
log.warning(
'Cannot generate ORM code for field ${field.name} of type ${field.type}');
//ctx.relations.forEach((key, value) {
// log.fine('key: $key, value: $value');
//});
//throw UnsupportedError(
// 'Cannot generate ORM code for field of type ${field.type.getDisplayString(withNullability: false)}.');
continue;
}
clazz.fields.add(Field((b) {
//log.fine('Field: $name, BuilderType: $builderType');
b
..name = name
..modifier = FieldModifier.final$
..type = builderType;
initializers.add(
refer(field.name)
.assign(builderType.newInstance([
refer('query'),
literalString(ctx.buildContext.resolveFieldName(field.name)!)
].followedBy(args)))
.code,
);
var literal = ctx.buildContext.resolveFieldName(field.name);
if (literal != null) {
//log.fine('Literal = ${field.name} $literal');
initializers.add(
refer(field.name)
.assign(builderType.newInstance([
refer('query'),
literalString(literal)
].followedBy(args)))
.code,
);
} else {
log.warning('Literal ${field.name} is null');
}
}));
}
@ -633,6 +705,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
});
}
/// Generate <Model>QueryValues class
Class buildValuesClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
@ -748,16 +821,26 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
// Add only if present
var target = refer('values').index(literalString(
ctx.buildContext.resolveFieldName(field.name)!));
var foreign = field.relationship.throughContext ??
field.relationship.foreign;
var foreignField = field.relationship.findForeignField(ctx);
// Added null safety check
var parsedId = prop.property(foreignField.name);
if (isSpecialId(foreign, field)) {
parsedId =
(refer('int').property('tryParse').call([parsedId]));
log.fine('Foreign field => ${foreignField.name}');
if (foreignField.type.nullabilitySuffix ==
NullabilitySuffix.question) {
parsedId = prop.nullSafeProperty(foreignField.name);
}
if (foreign != null) {
if (isSpecialId(foreign, field)) {
parsedId =
(refer('int').property('tryParse').call([parsedId]));
}
}
var cond = prop.notEqualTo(literalNull);
var condStr =
cond.accept(DartEmitter(useNullSafetySyntax: true));

View file

@ -1,5 +1,5 @@
name: angel3_orm_generator
version: 4.1.0
version: 4.1.1
description: Code generators for Angel3 ORM. Generates query builder classes.
homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/orm/angel_orm_generator