/// <summary> /// Generates association property in entity class for one side of association. /// </summary> /// <param name="entityAssociations">Lookup for association properties group in each entity.</param> /// <param name="owner">Assocation property owner entity.</param> /// <param name="type">Association property type.</param> /// <param name="propertyModel">Association property model.</param> /// <param name="metadata">Association property metadata.</param> private void BuildAssociationProperty( Dictionary <EntityModel, PropertyGroup> entityAssociations, EntityModel owner, IType type, PropertyModel propertyModel, AssociationMetadata metadata) { // if entity class doesn't have assocation properties group yet // (not created yet by previous associations) - create it if (!entityAssociations.TryGetValue(owner, out var associations)) { entityAssociations.Add( owner, associations = _entityBuilders[owner] .Regions() .New(ENTITY_ASSOCIATIONS_REGION) .Properties(false)); } // by default property type will be null here, but user could override it manually // and we should respect it if (propertyModel.Type == null) { propertyModel.Type = type; } // declare property var propertyBuilder = DefineProperty(associations, propertyModel); // and it's metadata _metadataBuilder.BuildAssociationMetadata(metadata, propertyBuilder); }
public AssociationModel( AssociationMetadata sourceMetadata, AssociationMetadata tagetMetadata, EntityModel source, EntityModel target, bool manyToOne) { SourceMetadata = sourceMetadata; TargetMetadata = tagetMetadata; Source = source; Target = target; ManyToOne = manyToOne; }
/// <summary> /// Generates association extension method in extensions class for one side of association. /// </summary> /// <param name="extensionEntityAssociations">Association methods groupsfor each entity.</param> /// <param name="extensionAssociations">Association extensions region provider.</param> /// <param name="thisEntity">Entity class for this side of assocation (used for extension <c>this</c> parameter).</param> /// <param name="resultEntity">Entity class for other side of assocation (used for extension result type).</param> /// <param name="type">Association result type.</param> /// <param name="extensionModel">Extension method model.</param> /// <param name="metadata">Association methodo metadata.</param> /// <param name="associationModel">Association model.</param> /// <param name="backReference">Identifies current side of assocation.</param> private void BuildAssociationExtension( Dictionary <EntityModel, MethodGroup> extensionEntityAssociations, Func <RegionGroup> extensionAssociations, ClassBuilder thisEntity, ClassBuilder resultEntity, IType type, MethodModel extensionModel, AssociationMetadata metadata, AssociationModel associationModel, bool backReference) { // create (if missing) assocations region for specific owner (this) entity var key = backReference ? associationModel.Target : associationModel.Source; if (!extensionEntityAssociations.TryGetValue(key, out var associations)) { extensionEntityAssociations.Add( key, associations = extensionAssociations() .New(string.Format(EXTENSIONS_ENTITY_ASSOCIATIONS_REGION, thisEntity.Type.Name.Name)) .Methods(false)); } // define extension method var methodBuilder = DefineMethod(associations, extensionModel).Returns(type); // and it's metadata _metadataBuilder.BuildAssociationMetadata(metadata, methodBuilder); // build method parameters... var thisParam = AST.Parameter(thisEntity.Type.Type, AST.Name(EXTENSIONS_ENTITY_THIS_PARAMETER), CodeParameterDirection.In); var ctxParam = AST.Parameter(WellKnownTypes.LinqToDB.IDataContext, AST.Name(EXTENSIONS_ENTITY_CONTEXT_PARAMETER), CodeParameterDirection.In); methodBuilder.Parameter(thisParam); methodBuilder.Parameter(ctxParam); // ... and body if (associationModel.FromColumns == null || associationModel.ToColumns == null) { // association doesn't specify relation columns (e.g. defined usign expression method) // so we should generate exception for non-query execution methodBuilder .Body() .Append( AST.Throw( AST.New( WellKnownTypes.System.InvalidOperationException, AST.Constant(EXCEPTION_QUERY_ONLY_ASSOCATION_CALL, true)))); } else { // generate association query for non-query invocation // As method body here could conflict with custom return type for many-to-one assocation // we forcebly override return type here to IQueryable<T> if (associationModel.ManyToOne && backReference) { methodBuilder.Returns(WellKnownTypes.System.Linq.IQueryable(resultEntity.Type.Type)); } var lambdaParam = AST.LambdaParameter(AST.Name(EXTENSIONS_ASSOCIATION_FILTER_PARAMETER), resultEntity.Type.Type); // generate assocation key columns filter, which compare // `this` entity parameter columns with return table entity columns var fromObject = backReference ? lambdaParam : thisParam; var toObject = !backReference ? lambdaParam : thisParam; ICodeExpression?filter = null; for (var i = 0; i < associationModel.FromColumns.Length; i++) { var fromColumn = _columnProperties[associationModel.FromColumns[i]]; var toColumn = _columnProperties[associationModel.ToColumns[i]]; var cond = AST.Equal( AST.Member(fromObject.Reference, fromColumn.Reference), AST.Member(toObject.Reference, toColumn.Reference)); filter = filter == null ? cond : AST.And(filter, cond); } // generate filter lambda function var filterLambda = AST .Lambda( WellKnownTypes.System.Linq.Expressions.Expression( WellKnownTypes.System.Func(WellKnownTypes.System.Boolean, resultEntity.Type.Type)), true) .Parameter(lambdaParam); filterLambda.Body().Append(AST.Return(filter !)); // ctx.GetTable<ResultEntity>() var body = AST.ExtCall( WellKnownTypes.LinqToDB.DataExtensions, WellKnownTypes.LinqToDB.DataExtensions_GetTable, WellKnownTypes.LinqToDB.ITable(resultEntity.Type.Type), new[] { resultEntity.Type.Type }, false, ctxParam.Reference); // append First/FirstOrDefault (for optional association) // for non-many relation if (!backReference || !associationModel.ManyToOne) { var optional = backReference ? associationModel.TargetMetadata.CanBeNull : associationModel.SourceMetadata.CanBeNull; // .First(t => t.PK == thisEntity.PK) // or // .FirstOrDefault(t => t.PK == thisEntity.PK) body = AST.ExtCall( WellKnownTypes.System.Linq.Queryable, optional ? WellKnownTypes.System.Linq.Queryable_FirstOrDefault : WellKnownTypes.System.Linq.Queryable_First, resultEntity.Type.Type.WithNullability(optional), new[] { resultEntity.Type.Type }, true, body, filterLambda.Method); } else { // .Where(t => t.PK == thisEntity.PK) body = AST.ExtCall( WellKnownTypes.System.Linq.Queryable, WellKnownTypes.System.Linq.Queryable_Where, WellKnownTypes.System.Linq.IQueryable(resultEntity.Type.Type), new [] { resultEntity.Type.Type }, true, body, filterLambda.Method); } methodBuilder.Body().Append(AST.Return(body)); } }
/// <summary> /// Defines association from foreign key. /// </summary> /// <param name="fk">Foreign key schema object.</param> /// <param name="defaultSchemas">List of default database schema names.</param> /// <returns>Assocation model. /// Could return <c>null</c> if any of foreign key relation tables were not loaded into model.</returns> private AssociationModel?BuildAssociations(ForeignKey fk, ISet <string> defaultSchemas) { if (!_entities.TryGetValue(fk.Source, out var source) || !_entities.TryGetValue(fk.Target, out var target)) { return(null); } var fromColumns = new ColumnModel[fk.Relation.Count]; var toColumns = new ColumnModel[fk.Relation.Count]; var sourceColumns = _columns[source.Entity]; var targetColumns = _columns[target.Entity]; // identify cardinality of relation (one-to-one vs one-to-many) and nullability using following information: // - nullability of foreign key columns (from/source side of association will be nullable in this case) // same for to/target side of relation // - wether source/target columns used as PK or not (possible cardinality) // TODO: for now we don't have information about unique constraints (except PK), which also affects cardinality var fromOptional = true; // if PK(and UNIQUE in future) and FK sizes on target table differ - this is definitely not one-to-one relation var manyToOne = fk.Relation.Count != sourceColumns.Values.Count(c => c.Metadata.IsPrimaryKey); for (var i = 0; i < fk.Relation.Count; i++) { var mapping = fk.Relation[i]; fromColumns[i] = sourceColumns[mapping.SourceColumn]; toColumns[i] = targetColumns[mapping.TargetColumn]; // if at least one column in foreign key is not nullable, we mark it as required if (fromOptional && !fromColumns[i].Metadata.CanBeNull) { fromOptional = false; } // if at least one column in foreign key is not a part of PK (or unique constraint in future), association has many-to-one cardinality // TODO: when adding unique constrains support make sure to validate that all FK columns are part of same PK/UNIQUE constrain // in case of composite key if (!manyToOne && !fromColumns[i].Metadata.IsPrimaryKey) { manyToOne = true; } } var sourceMetadata = new AssociationMetadata() { CanBeNull = fromOptional }; // back-reference is always optional var targetMetadata = new AssociationMetadata() { CanBeNull = true }; var association = new AssociationModel(sourceMetadata, targetMetadata, source.Entity, target.Entity, manyToOne); association.FromColumns = fromColumns; association.ToColumns = toColumns; // use foreign key name for xml-doc comment generation var summary = fk.Name; var backreferenceSummary = $"{fk.Name} backreference"; // use foreign key column name for association name generation var sourceColumnName = fk.Relation.Count == 1 ? fk.Relation[0].SourceColumn : null; var fromAssociationName = GenerateAssociationName( fk.Target, fk.Source, sourceColumnName, fk.Name, _options.DataModel.SourceAssociationPropertyNameOptions, defaultSchemas); var toAssocationName = GenerateAssociationName( fk.Source, fk.Target, null, fk.Name, manyToOne ? _options.DataModel.TargetMultipleAssociationPropertyNameOptions : _options.DataModel.TargetSingularAssociationPropertyNameOptions, defaultSchemas); // define association properties on on entities if (_options.DataModel.GenerateAssociations) { association.Property = new PropertyModel(fromAssociationName) { Modifiers = Modifiers.Public, IsDefault = true, HasSetter = true, Summary = summary }; association.BackreferenceProperty = new PropertyModel(toAssocationName) { Modifiers = Modifiers.Public, IsDefault = true, HasSetter = true, Summary = backreferenceSummary }; } // define association extension methods if (_options.DataModel.GenerateAssociationExtensions) { association.Extension = new MethodModel(fromAssociationName) { Modifiers = Modifiers.Public | Modifiers.Static | Modifiers.Extension, Summary = summary }; association.BackreferenceExtension = new MethodModel(toAssocationName) { Modifiers = Modifiers.Public | Modifiers.Static | Modifiers.Extension, Summary = backreferenceSummary }; } return(association); }