question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Relationship loading with federation

See original GitHub issue

I’m trying to use federation where the second instance extends on the data provided by the first. Let’s imagine a simple setup where you have two instances, each with a data source (via JPA): A for companies and B for employees. A provides only the data about the companies and is unaware of the employees. B on the other hand has methods that extend the Company model to return all the Employees underneath that company, while Employee also has a relation back to Company to return their current employer.

Several classes on instance B:

// Company

@KeyDirective(fields = FieldSet("id"))
@ExtendsDirective
data class Company(
    @ExternalDirective val id: Int
) {
    @OneToMany(mappedBy = "company")
    lateinit var employees: List<Employee>
}

// Employee

@Entity
@Table(name = "employees")
@KeyDirective(fields = FieldSet("id"))
data class Employee(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int = 0,

    @Column(name = "company_id")
    val companyId: Int = 0
) {
    @Transient
    lateinit var company: Company
}

// Data fetcher

class CompanyToEmployeesDataFetcher : DataFetcher<CompletableFuture<List<Employee>>>, BeanFactoryAware {
    private lateinit var beanFactory: BeanFactory

    override fun setBeanFactory(beanFactory: BeanFactory) {
        this.beanFactory = beanFactory
    }

    override fun get(environment: DataFetchingEnvironment): CompletableFuture<List<Employee>> {
        val id = environment.getSource<Company>().id
        return environment
            .getDataLoader<Int, List<Employee>>("employeesForCompany")
            .load(id)
    }
}

// Data loader configuration

@Configuration
open class DataLoaderConfiguration(private val employeeRepository: EmployeeRepository) {

    @Bean
    open fun dataLoaderRegistryFactory(): DataLoaderRegistryFactory {
        return object : DataLoaderRegistryFactory {
            override fun generate(): DataLoaderRegistry {
                val registry = DataLoaderRegistry()

                registry.register("employeesForCompany", DataLoader<Int, List<Employee>> { ids ->
                    CompletableFuture.supplyAsync {
                        val employees = employeeRepository.findEmployeesForCompany(ids)
                        ids.map { id -> employees.filter { it.companyId == id } }
                    }
                })

                return registry
            }
        }
    }

}

Querying the company -> employees relationship works. Please let me know if the approach I used above is not the correct one.

The other way around (employee -> company) I can’t even seem to figure out. I figured it would use the resolver registered in the federated type registry but it seems to be asking for a data fetcher. So I’ve tried creating a data fetcher, connecting it to the data loader in which I create a new Company with the ID. Result: Unable to resolve federated type, representation={__typename=Company, id=1} on instance A.

{
  # Company -> Employees works
  company(id: 1) {
    name
    employees {
      id
    }
  }

  # Employee -> Company doesn't work
  employee(id: 100) {
    id
    company {
      id
      name
    }
  }
}

Can you point me in the right direction for implementing relationships across instances? I’ve looked at several examples but without luck. Any help would be greatly appreciated, thanks!

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

2reactions
idrmcommented, Jun 26, 2020

@dariuszkuc After a bit of digging into the library source code, here’s what I came up with (no modification to graphql-kotlin):

import com.expediagroup.graphql.execution.FunctionDataFetcher
import com.expediagroup.graphql.execution.KotlinDataFetcherFactoryProvider
import com.expediagroup.graphql.execution.SimpleKotlinDataFetcherFactoryProvider
import com.fasterxml.jackson.databind.ObjectMapper
import graphql.schema.DataFetcherFactory
import graphql.schema.DataFetchingEnvironment
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import kotlin.reflect.*
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaType

@Configuration
class DataFetcherConfiguration {

    @Bean
    fun dataFetcherFactoryProvider(objectMapper: ObjectMapper, 
                                   appContext: ApplicationContext): KotlinDataFetcherFactoryProvider =
        CustomDataFetcherFactoryProvider(objectMapper, appContext)
}

class CustomFunctionDataFetcher(target: Any?, fn: KFunction<*>, objectMapper: ObjectMapper, 
                                private val appContext: ApplicationContext) :
    FunctionDataFetcher(target, fn, objectMapper) {

    override fun mapParameterToValue(param: KParameter, environment: DataFetchingEnvironment): Any? =
        when {
            param.findAnnotation<Autowired>() != null -> {
                val qualifierAnnotation = param.findAnnotation<Qualifier>()
                if (qualifierAnnotation != null) {
                    appContext.getBean(qualifierAnnotation.value)
                } else {
                    appContext.getBean(param.type.javaType as Class<*>)
                }
            }
            else -> super.mapParameterToValue(param, environment)
        }

}

class CustomDataFetcherFactoryProvider(
    private val objectMapper: ObjectMapper,
    private val appContext: ApplicationContext
) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) {

    override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any?> {
        return DataFetcherFactory {
            CustomFunctionDataFetcher(
                target = target,
                fn = kFunction,
                objectMapper = objectMapper,
                appContext = appContext)
        }
    }
}

This allows one to inject beans by decorating a type method’s arguments with the @Autowired and, optionally, @Qualifier annotations. The @GraphQLIgnore annotation is required as well, in order to prevent the parameter from being considered for the schema (further tweaks to the beans and underlying processors may allow skipping this annotation, the same way a DataFetchingEnvironment param doesn’t require it). Here’s a usage example:

data class Product(val name: String) {
  fun getOrders(@GraphQLIgnore @Autowired orderRepository: OrderRepository) : List<Order> =
    orderRepository.findByProduct(name)
}

P.S. This being a closed issue, I’m not sure how useful posting the above code snippets here is going to be. Feel free to publish and/or integrate them as you wish.

0reactions
dariuszkuccommented, Jun 27, 2020

@idrm yep that integrations looks good. I’m thinking we can add it as something you have to opt in in 3.x.x branch but we probably we could make it a default one in 4.x.x.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Relationship Loading Techniques — SQLAlchemy 1.4 ...
A central concept of joined eager loading when applied to collections is that the Query object must de-duplicate rows against the leading entity ......
Read more >
Resolve M:M (many to many) using Federation - Stack Overflow
How to resolve many to many relationships while using GraphQL Federation Micro-Services? Lets say Users & Events. They have m:m relationship. " ...
Read more >
Best practices for using workload identity federation
Workload identity federation lets you create more than one provider per workload identity pool. Using multiple providers can be useful if identities are...
Read more >
Federation issues - Documentation for BMC CMDB 20.02
When creating a federated class that brings in data from a DB2 database, you create a federated relationship class to retrieve the data....
Read more >
Federated Queues - RabbitMQ
This feature provides a way of balancing the load of a single logical queue across nodes or clusters. It does so by moving...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found