Low-Code Development: Leverage low and no code to streamline your workflow so that you can focus on higher priorities.
DZone Security Research: Tell us your top security strategies in 2024, influence our research, and enter for a chance to win $!
Also known as the build stage of the SDLC, coding focuses on the writing and programming of a system. The Zones in this category take a hands-on approach to equip developers with the knowledge about frameworks, tools, and languages that they can tailor to their own build needs.
A framework is a collection of code that is leveraged in the development process by providing ready-made components. Through the use of frameworks, architectural patterns and structures are created, which help speed up the development process. This Zone contains helpful resources for developers to learn about and further explore popular frameworks such as the Spring framework, Drupal, Angular, Eclipse, and more.
Java is an object-oriented programming language that allows engineers to produce software for multiple platforms. Our resources in this Zone are designed to help engineers with Java program development, Java SDKs, compilers, interpreters, documentation generators, and other tools used to produce a complete application.
JavaScript (JS) is an object-oriented programming language that allows engineers to produce and implement complex features within web browsers. JavaScript is popular because of its versatility and is preferred as the primary choice unless a specific function is needed. In this Zone, we provide resources that cover popular JS frameworks, server applications, supported data types, and other useful topics for a front-end engineer.
Programming languages allow us to communicate with computers, and they operate like sets of instructions. There are numerous types of languages, including procedural, functional, object-oriented, and more. Whether you’re looking to learn a new language or trying to find some tips or tricks, the resources in the Languages Zone will give you all the information you need and more.
Development and programming tools are used to build frameworks, and they can be used for creating, debugging, and maintaining programs — and much more. The resources in this Zone cover topics such as compilers, database management systems, code editors, and other software tools and can help ensure engineers are writing clean code.
Development at Scale
As organizations’ needs and requirements evolve, it’s critical for development to meet these demands at scale. The various realms in which mobile, web, and low-code applications are built continue to fluctuate. This Trend Report will further explore these development trends and how they relate to scalability within organizations, highlighting application challenges, code, and more.
You Can Shape Trend Reports: Participate in DZone Research Surveys + Enter the Prize Drawings!
Reactive programming has significantly altered how developers tackle modern application development, particularly in environments that demand top-notch performance and scalability. Quarkus, a Kubernetes-native Java framework specifically optimized for GraalVM and HotSpot, fully embraces the principles of reactive programming to craft applications that are responsive, resilient, and elastic. This article comprehensively explores the impact and effectiveness of reactive programming in Quarkus, providing detailed insights and practical examples in Java to illustrate its transformative capabilities. What Is Reactive Programming? Reactive programming is a programming paradigm that focuses on handling asynchronous data streams and the propagation of change. It provides developers with the ability to write code that responds to changes in real time, such as user inputs, data updates, or messages from other services. This approach is particularly well-suited for building applications that require real-time responsiveness and the ability to process continuous streams of data. By leveraging reactive programming, developers can create more interactive and responsive applications that can adapt to changing conditions and events. Key features of reactive programming include: Asynchronous: Non-blocking operations that allow multiple tasks to run concurrently Event-driven: Actions are triggered by events such as user actions or data changes Resilient: Systems remain responsive under load by handling failures gracefully Scalable: Efficient resource usage to handle a high number of requests Why Quarkus for Reactive Programming? Quarkus, a framework designed to harness the advantages of reactive programming, aims to provide a streamlined and efficient environment for developing reactive applications. There are several compelling reasons to consider Quarkus for such applications: Native support for Reactive frameworks: Quarkus seamlessly integrates with popular reactive libraries such as Vert.x, Mutiny, and Reactive Streams. This native support allows developers to leverage the full power of these frameworks within the Quarkus environment. Efficient resource usage: Quarkus's native image generation and efficient runtime result in lower memory consumption and faster startup times. This means that applications built with Quarkus can be more resource-efficient, leading to potential cost savings and improved performance. Developer productivity: Quarkus offers features like live coding, significantly improving the development experience. This means developers can iterate more quickly, leading to faster development cycles and ultimately more productive software development. Getting Started With Reactive Programming in Quarkus Let’s dive into a simple example to demonstrate reactive programming in Quarkus using Java. We’ll create a basic REST API that fetches data asynchronously. Step 1: Setting Up the Project First, create a new Quarkus project: Shell mvn io.quarkus:quarkus-maven-plugin:create \ -DprojectGroupId=com.example \ -DprojectArtifactId=reactive-quarkus \ -DclassName="com.example.GreetingResource" \ -Dpath="/greeting" cd reactive-quarkus Add the necessary dependencies in your pom.xml: XML <dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-mutiny</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-vertx</artifactId> </dependency> </dependencies> Step 2: Start Coding Now, create a simple REST endpoint using Mutiny, a reactive programming library designed for simplicity and performance: Java import io.smallrye.mutiny.Uni; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/greeting") public class GreetingResource { @GET @Produces(MediaType.APPLICATION_JSON) public Uni<Greeting> greeting() { return Uni.createFrom().item(() -> new Greeting("Hello, Reactive World!")) .onItem().delayIt().byMillis(1000); // Simulate delay } public static class Greeting { public String message; public Greeting(String message) { this.message = message; } } } In this example: We define a REST endpoint /greeting that produces JSON. The greeting method returns a Uni<Greeting> which represents a single value or failure, a concept from Mutiny. We simulate a delay using onItem().delayIt().byMillis(1000) to mimic an asynchronous operation Step 3: Running the Application To run the application, use the Quarkus development mode: Shell ./mvnw quarkus:dev Now, visit http://localhost:8080/greeting to see the response: JSON { "message": "Hello, Reactive World!" } Unit Testing Reactive Endpoints When testing reactive endpoints in Quarkus, it's important to verify that the application functions correctly in response to various conditions. Quarkus facilitates seamless integration with JUnit 5, allowing developers to effectively write and execute unit tests to ensure the proper functionality of their applications. Step 1: Adding Test Dependencies Ensure you have the following dependencies in your pom.xml for testing: XML <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> Step 2: Writing a Unit Test Create a test class to verify the behavior of the GreetingResource: Java import io.quarkus.test.junit.QuarkusTest; import io.rest-assured.RestAssured; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; @QuarkusTest public class GreetingResourceTest { @Test public void testGreetingEndpoint() { RestAssured.when().get("/greeting") .then() .statusCode(200) .body("message", is("Hello, Reactive World!")); } } In this test: We use the @QuarkusTest annotation to enable Quarkus testing features. We use RestAssured to send an HTTP GET request to the /greeting endpoint and verify the response status code and body. Step 3: Running the Tests To run the tests, use the Maven test command: Shell ./mvnw test The test will execute and verify that the /greeting endpoint returns the expected response. Advanced Usage: Integrating With Databases Let’s extend the example by integrating a reactive database client. We’ll use the reactive PostgreSQL client provided by Vert.x. Add the dependency for the reactive PostgreSQL client: XML <dependency> <groupId>io.quarkiverse.reactive</groupId> <artifactId>quarkus-reactive-pg-client</artifactId> </dependency> Configure the PostgreSQL client in application.properties: Shell quarkus.datasource.db-kind=postgresql quarkus.datasource.username=your_username quarkus.datasource.password=your_password quarkus.datasource.reactive.url=postgresql://localhost:5432/your_database Create a repository class to handle database operations: Java import io.smallrye.mutiny.Uni; import io.vertx.mutiny.pgclient.PgPool; import io.vertx.mutiny.sqlclient.Row; import io.vertx.mutiny.sqlclient.RowSet; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @ApplicationScoped public class GreetingRepository { @Inject PgPool client; public Uni<String> findGreeting() { return client.query("SELECT message FROM greetings WHERE id = 1") .execute() .onItem().transform(RowSet::iterator) .onItem().transform(iterator -> iterator.hasNext() ? iterator.next().getString("message") : "Hello, default!"); } } Update the GreetingResource to use the repository: Java import io.smallrye.mutiny.Uni; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/greeting") public class GreetingResource { @Inject GreetingRepository repository; @GET @Produces(MediaType.APPLICATION_JSON) public Uni<Greeting> greeting() { return repository.findGreeting() .onItem().transform(Greeting::new); } public static class Greeting { public String message; public Greeting(String message) { this.message = message; } } } This setup demonstrates how to perform asynchronous database operations using the reactive PostgreSQL client. The findGreeting method queries the database and returns a Uni<String> representing the greeting message. Handling Errors in Reactive Programming Handling errors gracefully is a critical aspect of building resilient reactive applications. Mutiny provides several operators to handle errors effectively. Update the GreetingRepository to include error handling: Java public Uni<String> findGreeting() { return client.query("SELECT message FROM greetings WHERE id = 1") .execute() .onItem().transform(RowSet::iterator) .onItem().transform(iterator -> iterator.hasNext() ? iterator.next().getString("message") : "Hello, default!") .onFailure().recoverWithItem("Hello, fallback!"); } In this updated method: We use onFailure().recoverWithItem("Hello, fallback!") to provide a fallback message in case of any failure during the database query. Reactive Event Bus With Vert.x Quarkus seamlessly integrates with Vert.x, a powerful reactive toolkit, to provide a high-performance event bus for developing sophisticated event-driven applications. This event bus allows various components of your application to communicate asynchronously, facilitating efficient and scalable interaction between different parts of the system. Add the necessary Vert.x dependencies: XML <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-vertx</artifactId> </dependency> Create a Vert.x consumer to handle events: Java import io.quarkus.vertx.ConsumeEvent; import io.smallrye.mutiny.Uni; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class GreetingService { @ConsumeEvent("greeting") public Uni<String> generateGreeting(String name) { return Uni.createFrom().item(() -> "Hello, " + name + "!") .onItem().delayIt().byMillis(500); // Simulate delay } } Now, Update the GreetingResource to send events to the event bus: Java import io.smallrye.mutiny.Uni; import io.vertx.mutiny.core.eventbus.EventBus; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @Path("/greeting") public class GreetingResource { @Inject EventBus eventBus; @GET @Produces(MediaType.APPLICATION_JSON) public Uni<Greeting> greeting(@QueryParam("name") String name) { return eventBus.<String>request("greeting", name) .onItem().transform(reply -> new Greeting(reply.body())); } public static class Greeting { public String message; public Greeting(String message) { this.message = message; } } } In this example: We define an event consumer GreetingService that listens for greeting events and generates a greeting message. The GreetingResource sends a greeting event to the event bus and waits for the response asynchronously. Comparison: Quarkus vs. Spring in Reactive Capabilities When building reactive applications, Quarkus and Spring offer robust frameworks, each with unique approaches and strengths. 1. Framework Integration Spring Spring Boot leverages Spring WebFlux for reactive programming and seamlessly integrates with the Spring ecosystem, supporting Project Reactor as its reactive library. Quarkus Quarkus utilizes Vert.x and Mutiny for reactive programming, providing native support from the ground up and optimizing for performance and efficiency. 2. Performance and Resource Efficiency Spring While Spring Boot with WebFlux offers good performance for reactive applications, it may be heavier in terms of resource usage compared to Quarkus. Quarkus Quarkus is designed to be lightweight and fast, showcasing lower memory consumption and faster startup times, especially when compiled to a native image with GraalVM. 3. Developer Experience Spring Spring Boot offers a mature ecosystem with extensive documentation and strong community support, making it easy for developers familiar with Spring to adopt reactive programming. Quarkus Quarkus provides an excellent developer experience with features like live coding and quick feedback loops. Its integration with reactive libraries like Mutiny makes it intuitive for developers new to reactive programming. 4. Cloud-Native and Microservices Spring Widely used for building microservices and cloud-native applications, Spring Boot provides a rich set of tools and integrations for deploying applications to the cloud. Quarkus Designed with cloud-native and microservices architectures in mind, Quarkus showcases efficient resource usage and strong support for Kubernetes, making it a compelling choice for cloud deployments. 5. Ecosystem and Community Spring Boasting a vast ecosystem with numerous extensions and integrations, Spring is supported by a large community of developers. Quarkus Rapidly gaining popularity, Quarkus offers a comprehensive set of extensions, and its community is also expanding, contributing to its ecosystem. Conclusion Reactive programming in Quarkus provides a cutting-edge approach to enhancing the performance and scalability of Java applications. By harnessing the capabilities of reactive streams and asynchronous operations, Quarkus empowers developers to build applications that are not only robust and high-performing, but also well-suited for modern cloud-native environments. The efficiency and power of Quarkus, combined with its rich ecosystem of reactive libraries, offer developers the tools they need to handle a wide range of tasks, from simple asynchronous operations to complex data streams, making Quarkus a formidable platform for reactive programming in Java.
In today's rapidly evolving technological landscape, it is crucial for any business or application to efficiently manage and utilize data. NoSQL databases have emerged as an alternative to traditional relational databases, offering flexibility, scalability, and performance advantages. These benefits become even more pronounced when combined with Java, a robust and widely-used programming language. This article explores three key benefits of understanding and learning NoSQL databases with Java, highlighting the polyglot philosophy and its efficiency in software architecture. Enhanced Flexibility and Scalability One significant benefit of NoSQL databases is their capability to handle various data models, such as key-value pairs, documents, wide-column stores, and graph databases. This flexibility enables developers to select the most suitable data model for their use case. When combined with Java, a language renowned for its portability and platform independence, the adaptability of NoSQL databases can be fully utilized. Improved Performance and Efficiency Performance is a crucial aspect of database management, and NoSQL databases excel in this area because of their distributed nature and optimized storage mechanisms. When developers combine these performance-enhancing features with Java, they can create applications that are not only efficient but also high-performing. Embracing the Polyglot Philosophy The polyglot philosophy in software development encourages using multiple languages, frameworks, and databases within a single application to take advantage of each one's strengths. Understanding and learning NoSQL databases with Java perfectly embodies this approach, offering several benefits for modern software architecture. Leveraging Eclipse JNoSQL for Success With NoSQL Databases and Java To fully utilize NoSQL databases with Java, developers can use Eclipse JNoSQL, a framework created to streamline the integration and management of NoSQL databases in Java applications. Eclipse JNoSQL supports over 30 databases and is aligned with Jakarta NoSQL and Jakarta Data specifications, providing a comprehensive solution for modern data handling needs. Eclipse JNoSQL: Bridging Java and NoSQL Databases Eclipse JNoSQL is a framework that simplifies the interaction between Java applications and NoSQL databases. With support for over 30 different NoSQL databases, Eclipse JNoSQL enables developers to work efficiently across various data stores without compromising flexibility or performance. Key features of Eclipse JNoSQL include: Support for Jakarta Data Query Language: This feature enhances the power and flexibility of querying across databases. Cursor pagination: Processes large datasets efficiently by utilizing cursor-based pagination rather than traditional offset-based pagination NoSQLRepository: Simplifies the creation and management of repository interfaces New column and document templates: Simplify data management with predefined templates Jakarta NoSQL and Jakarta Data Specifications Eclipse JNoSQL is designed to support Jakarta NoSQL and Jakarta Data specifications, standardizing and simplifying database interactions in Java applications. Jakarta NoSQL: This comprehensive framework offers a unified API and a set of powerful annotations, making it easier to work with various NoSQL data stores while maintaining flexibility and productivity. Jakarta Data: This specification provides an API for easier data access across different database types, enabling developers to create custom query methods on repository interfaces. Introducing Eclipse JNoSQL 1.1.1 The latest release, Eclipse JNoSQL 1.1.1, includes significant enhancements and new features, making it a valuable tool for Java developers working with NoSQL databases. Key updates include: Support to cursor pagination Support to Jakarta Data Query Fixes several bugs and enhances performance For more details, visit the Eclipse JNoSQL Release 1.1.1 notes. Practical Example: Java SE Application With Oracle NoSQL To illustrate the practical use of Eclipse JNoSQL, let's consider a Java SE application using Oracle NoSQL. This example showcases the effectiveness of cursor pagination and JDQL for querying. The first pagination method we will discuss is Cursor pagination, which offers a more efficient way to handle large datasets than traditional offset-based pagination. Below is a code snippet demonstrating cursor pagination with Oracle NoSQL. Java @Repository public interface BeerRepository extends OracleNoSQLRepository<Beer, String> { @Find @OrderBy("hop") CursoredPage<Beer> style(@By("style") String style, PageRequest pageRequest); @Query("From Beer where style = ?1") List<Beer> jpql(String style); } public class App4 { public static void main(String[] args) { var faker = new Faker(); try (SeContainer container = SeContainerInitializer.newInstance().initialize()) { BeerRepository repository = container.select(BeerRepository.class).get(); for (int index = 0; index < 100; index++) { Beer beer = Beer.of(faker); // repository.save(beer); } PageRequest pageRequest = PageRequest.ofSize(3); var page1 = repository.style("Stout", pageRequest); System.out.println("Page 1"); page1.forEach(System.out::println); PageRequest pageRequest2 = page1.nextPageRequest(); var page2 = repository.style("Stout", pageRequest2); System.out.println("Page 2"); page2.forEach(System.out::println); System.out.println("JDQL query: "); repository.jpql("Stout").forEach(System.out::println); } System.exit(0); } } In this example, BeerRepository efficiently retrieves and paginates data using cursor pagination. The style method employs cursor pagination, while the jpql method demonstrates a JDQL query. API Changes and Compatibility Breaks in Eclipse JNoSQL 1.1.1 The release of Eclipse JNoSQL 1.1.1 includes significant updates and enhancements aimed at improving functionality and aligning with the latest specifications. However, it's important to note that these changes may cause compatibility issues for developers, which need to be understood and addressed in their projects. 1. Annotations Moved to Jakarta NoSQL Specification Annotations like Embeddable and Inheritance were previously included in the Eclipse JNoSQL framework. In the latest version, however, they have been relocated to the Jakarta NoSQL specification to establish a more consistent approach across various NoSQL databases. As a result, developers will need to update their imports and references to these annotations. Java // Old import import org.jnosql.mapping.Embeddable; // New import import jakarta.nosql.Embeddable; The updated annotations can be accessed at the Jakarta NoSQL GitHub repository. 2. Unified Query Packages To simplify and unify the query APIs, SelectQuery and DeleteQuery have been consolidated into a single package. Consequently, specific query classes like DocumentQuery, DocumentDeleteQuery, ColumnQuery, and ColumnDeleteQuery have been removed. Impact: Any code using these removed classes will no longer compile and must be refactored to use the new unified classes. Solution: Refactor your code to use the new query classes in the org.eclipse.jnosql.communication.semistructured package. For example: Java // Old usage DocumentQuery query = DocumentQuery.select().from("collection").where("field").eq("value").build(); // New usage SelectQuery query = SelectQuery.select().from("collection").where("field").eq("value").build(); Similar adjustments will be needed for delete queries. 3. Migration of Templates Templates such as ColumnTemplate, KeyValueTemplate, and DocumentTemplate have been moved from the Jakarta Specification to Eclipse JNoSQL. Java // Old import import jakarta.nosql.document.DocumentTemplate; // New import import org.eclipse.jnosql.mapping.document.DocumentTemplate; 4. Default Query Language: Jakarta Data Query Language (JDQL) Another significant update in Eclipse JNoSQL 1.1.1 is the adoption of Jakarta Data Query Language (JDQL) as the default query language. JDQL provides a standardized way to define queries using annotations, making it simpler and more intuitive for developers. Conclusion The use of a NoSQL database is a powerful asset in modern applications. It allows software architects to employ polyglot persistence, utilizing the best persistence capability in each scenario. Eclipse JNoSQL assists Java developers in implementing these NoSQL capabilities into their applications.
Java ORM world is very steady and few libraries exist, but none of them brought any breaking change over the last decade. Meanwhile, application architecture evolved with some trends such as Hexagonal Architecture, CQRS, Domain Driven Design, or Domain Purity. Stalactite tries to be more suitable to these new paradigms by allowing to persist any kind of Class without the need to annotate them or use external XML files: its mapping is made of method reference. As a benefit, you get a better view of the entity graph since the mapping is made through a fluent API that chains your entity relations, instead of spreading annotations all over entities. This is very helpful to see the complexity of your entity graph, which would impact its load as well as the memory. Moreover, since Stalactite only fetches data eagerly, we can say that what you see is what you get. Here is a very small example: Java MappingEase.entityBuilder(Country.class, Long.class) .mapKey(Country::getId, IdentifierPolicy.afterInsert()) .mapOneToOne(Country::getCapital, MappingEase.entityBuilder(City.class, Long.class) .mapKey(City::getId, IdentifierPolicy.afterInsert()) .map(City::getName)) First Steps The release 2.0.0 is out for some weeks and is available as a Maven dependency, hereafter is an example with HSQLDB. For now, Stalactite is compatible with the following databases (mainly in their latest version): HSQLDB, H2, PostgreSQL, MySQL, and MariaDB. XML <dependency> <groupId>org.codefilarete.stalactite</groupId> <artifactId>orm-hsqldb-adapter</artifactId> <version>2.0.0</version> </dependency> If you're interested in a less database-vendor-dedicated module, you can use the orm-all-adapter module. Just be aware that it will bring you extra modules and extra JDBC drivers, heaving your artifact. After getting Statactite as a dependency, the next step is to have a JDBC DataSource and pass it to a org.codefilarete.stalactite.engine.PersistenceContext: Java org.hsqldb.jdbc.JDBCDataSource dataSource= new org.hsqldb.jdbc.JDBCDataSource(); dataSource.setUrl("jdbc:hsqldb:mem:test"); dataSource.setUser("sa"); dataSource.setPassword(""); PersistenceContext persistenceContext = new PersistenceContext(dataSource, new HSQLDBDialect()); Then comes the interesting part: the mapping. Supposing you get a Country, you can quickly set up its mapping through the Fluent API, starting with the org.codefilarete.stalactite.mapping.MappingEase class as such: Java EntityPersister<Country, Long> countryPersister = MappingEase.entityBuilder(Country.class, Long.class) .mapKey(Country::getId, IdentifierPolicy.afterInsert()) .map(Country::getName) .build(persistenceContext); the afterInsert() identifier policy means that the country.id column is an auto-increment one. Two other policies exist: the beforeInsert() for identifier given by a database Sequence (for example), and the alreadyAssigned() for entities that have a natural identifier given by business rules, any non-declared property is considered transient and not managed by Stalactite. The schema can be generated with the org.codefilarete.stalactite.sql.ddl.DDLDeployer class as such (it will generate it into the PersistenceContext dataSource): Java DDLDeployer ddlDeployer = new DDLDeployer(persistenceContext); ddlDeployer.deployDDL(); Finally, you can persist your entities thanks to the EntityPersister obtained previously, please find the example below. You might notice that you won't find JPA methods in Stalactite persister. The reason is that Stalactite is far different from JPA and doesn't aim at being compatible with it: no annotation, no attach/detach mechanism, no first-level cache, no lazy loading, and many more. Hence, the methods are quite straight to their goal: Java Country myCountry = new Country(); myCountry.setName("myCountry"); countryPersister.insert(myCountry); myCountry.setName("myCountry with a different name"); countryPersister.update(myCountry); Country loadedCountry = countryPersister.select(myCountry.getId()); countryPersister.delete(loadedCountry); Spring Integration There was a raw usage of Stalactite, meanwhile, you may be interested in its integration with Spring to benefit from the magic of its @Repository. Stalactite provides it, just be aware that it's still a work-in-progress feature. The approach to activate it is the same as for JPA: enable Stalactite repositories thanks to the @EnableStalactiteRepositories annotation on your Spring application. Then you'll declare the PersistenceContext and EntityPersister as @Bean : Java @Bean public PersistenceContext persistenceContext(DataSource dataSource) { return new PersistenceContext(dataSource); } @Bean public EntityPersister<Country, Long> countryPersister(PersistenceContext persistenceContext) { return MappingEase.entityBuilder(Country.class, long.class) .mapKey(Country::getId, IdentifierPolicy.afterInsert()) .map(Country::getName) .build(persistenceContext); } Then you can declare your repository as such, to be injected into your services : Java @Repository public interface CountryStalactiteRepository extends StalactiteRepository<Country, Long> { } As mentioned earlier, since the paradigm of Stalactite is not the same as JPA (no annotation, no attach/detach mechanism, etc), you won't find the same methods of JPA repository in Stalactite ones : save : Saves the given entity, either inserting it or updating it according to its persistence states saveAll : Same as the previous one, with a massive API findById : Try to find an entity by its id in the database findAllById : Same as the previous one, with a massive API delete : Delete the given entity from the database deleteAll : Same as the previous one, with a massive API Conclusion In these chapters we introduced the Stalactite ORM, more information about the configuration, the mapping, and all the documentation are available on the website. The project is open-source with the MIT license and shared through Github. Thanks for reading, any feedback is appreciated!
Dynamic query building is a critical aspect of modern application development, especially in scenarios where the search criteria are not known at compile time. In this publication, let's deep dive into the world of dynamic query building in Spring Boot applications using JPA criteria queries. We’ll explore a flexible and reusable framework that allows developers to construct complex queries effortlessly. Explanation of Components Criteria Interface The Criteria interface serves as the foundation for our framework. It extends Specification<T> and provides a standardized way to build dynamic queries. By implementing the toPredicate method, the Criteria interface enables the construction of predicates based on the specified criteria. Java package com.core.jpa; import java.util.ArrayList; import java.util.List; import org.springframework.data.jpa.domain.Specification; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; public class Criteria<T> implements Specification<T> { private static final long serialVersionUID = 1L; private transient List<Criterion> criterions = new ArrayList<>(); @Override public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) { if (!criterions.isEmpty()) { List<Predicate> predicates = new ArrayList<>(); for (Criterion c : criterions) { predicates.add(c.toPredicate(root, query, builder)); } if (!predicates.isEmpty()) { return builder.and(predicates.toArray(new Predicate[predicates.size()])); } } return builder.conjunction(); } public void add(Criterion criterion) { if (criterion != null) { criterions.add(criterion); } } } Criterion Interface The Criterion interface defines the contract for building individual predicates. It includes the toPredicate method, which is implemented by various classes to create specific predicates such as equals, not equals, like, etc. Java public interface Criterion { public enum Operator { EQ, IGNORECASEEQ, NE, LIKE, GT, LT, GTE, LTE, AND, OR, ISNULL } public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder); } LogicalExpression Class The LogicalExpression class facilitates the combination of multiple criteria using logical operators such as AND and OR. By implementing the toPredicate method, this class allows developers to create complex query conditions by chaining together simple criteria. Java public class LogicalExpression implements Criterion { private Criterion[] criterion; private Operator operator; public LogicalExpression(Criterion[] criterions, Operator operator) { this.criterion = criterions; this.operator = operator; } @Override public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder) { List<Predicate> predicates = new ArrayList<>(); for(int i=0;i<this.criterion.length;i++){ predicates.add(this.criterion[i].toPredicate(root, query, builder)); } if(null != operator && operator.equals(Criterion.Operator.OR)) { return builder.or(predicates.toArray(new Predicate[predicates.size()])); } return null; } } Restrictions Class The Restrictions class provides a set of static methods for creating instances of SimpleExpression and LogicalExpression. These methods offer convenient ways to build simple and complex criteria, making it easier for developers to construct dynamic queries. Java public class Restrictions { private Restrictions() { } public static SimpleExpression eq(String fieldName, Object value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value, Operator.EQ); } public static SimpleExpression ne(String fieldName, Object value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value, Operator.NE); } public static SimpleExpression like(String fieldName, String value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value.toUpperCase(), Operator.LIKE); } public static SimpleExpression gt(String fieldName, Object value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value, Operator.GT); } public static SimpleExpression lt(String fieldName, Object value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value, Operator.LT); } public static SimpleExpression gte(String fieldName, Object value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value, Operator.GTE); } public static SimpleExpression lte(String fieldName, Object value, boolean ignoreNull) { if (ignoreNull && (ObjectUtils.isEmpty(value))) return null; return new SimpleExpression(fieldName, value, Operator.LTE); } public static SimpleExpression isNull(String fieldName, boolean ignoreNull) { if (ignoreNull) return null; return new SimpleExpression(fieldName, null, Operator.ISNULL); } public static LogicalExpression and(Criterion... criterions) { return new LogicalExpression(criterions, Operator.AND); } public static LogicalExpression or(Criterion... criterions) { return new LogicalExpression(criterions, Operator.OR); } public static <E> LogicalExpression in(String fieldName, Collection<E> value, boolean ignoreNull) { if (ignoreNull && CollectionUtils.isEmpty(value)) return null; SimpleExpression[] ses = new SimpleExpression[value.size()]; int i = 0; for (Object obj : value) { if(obj instanceof String) { ses[i] = new SimpleExpression(fieldName, String.valueOf(obj), Operator.IGNORECASEEQ); } else { ses[i] = new SimpleExpression(fieldName, obj, Operator.EQ); } i++; } return new LogicalExpression(ses, Operator.OR); } public static Long convertToLong(Object o) { String stringToConvert = String.valueOf(o); if (!"null".equals(stringToConvert)) { return Long.parseLong(stringToConvert); } else { return Long.valueOf(0); } } } SimpleExpression Class The SimpleExpression class represents simple expressions with various operators such as equals, not equals, like, greater than, less than, etc. By implementing the toPredicate method, this class translates simple expressions into JPA criteria predicates, allowing for precise query construction. The SimpleExpression class represents simple expressions with various operators such as equals, not equals, like, greater than, less than, etc. By implementing the toPredicate method, this class translates simple expressions into JPA criteria predicates, allowing for precise query construction. Java public class SimpleExpression implements Criterion { private String fieldName; private Object value; private Operator operator; protected SimpleExpression(String fieldName, Object value, Operator operator) { this.fieldName = fieldName; this.value = value; this.operator = operator; } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public Predicate toPredicate(Root<?> root, CriteriaQuery<?> query, CriteriaBuilder builder) { Path expression = null; if (fieldName.contains(".")) { String[] names = StringUtils.split(fieldName, "."); if(names!=null && names.length>0) { expression = root.get(names[0]); for (int i = 1; i < names.length; i++) { expression = expression.get(names[i]); } } } else { expression = root.get(fieldName); } switch (operator) { case EQ: return builder.equal(expression, value); case IGNORECASEEQ: return builder.equal(builder.upper(expression), value.toString().toUpperCase()); case NE: return builder.notEqual(expression, value); case LIKE: return builder.like(builder.upper(expression), value.toString().toUpperCase() + "%"); case LT: return builder.lessThan(expression, (Comparable) value); case GT: return builder.greaterThan(expression, (Comparable) value); case LTE: return builder.lessThanOrEqualTo(expression, (Comparable) value); case GTE: return builder.greaterThanOrEqualTo(expression, (Comparable) value); case ISNULL: return builder.isNull(expression); default: return null; } } } Usage Example Suppose we have a User entity and a corresponding UserRepository interface defined in our Spring Boot application: Java @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int age; private double salary; // Getters and setters } public interface UserRepository extends JpaRepository<User, Long> { } With these entities in place, let’s demonstrate how to use our dynamic query-building framework to retrieve a list of users based on certain search criteria: Java Criteria<User> criteria = new Criteria<>(); criteria.add(Restrictions.eq("age", 25, true)); criteria.add(Restrictions.like("name", "John", true)); criteria.add(Restrictions.or( Restrictions.gt("salary", 50000, true), Restrictions.isNull("salary", null, false) )); List<User> users = userRepository.findAll(criteria); In this example, we construct a dynamic query using the Criteria interface and various Restrictions provided by our framework. We specify criteria such as age equals 25, name contains "John", and salary greater than 50000 or null. Finally, we use the UserRepository to execute the query and retrieve the matching users. Conclusion Dynamic query building with JPA criteria queries in Spring Boot applications empowers developers to create sophisticated queries tailored to their specific needs. By leveraging the framework outlined in this publication, developers can streamline the process of constructing dynamic queries and enhance the flexibility and efficiency of their applications. Additional Resources Spring Data JPA Documentation
Lately, I have been working with Polars and PySpark, which brings me back to the days when Spark fever was at its peak, and every data processing solution seemed to revolve around it. This prompts me to question: was it really necessary? Let’s delve into my experiences with various data processing technologies. Background During my final degree project on sentiment analysis, Pandas was just beginning to emerge as the primary tool for feature engineering. It was user-friendly and seamlessly integrated with several machine learning libraries, such as scikit-learn. Then, as I started working, Spark became a part of my daily routine. I used it for ETL processes in a nascent data lake to implement business logic, although I wondered if we were over-engineering the process. Typically, the data volumes we handled were not substantial enough to necessitate using Spark, yet it was employed every time new data entered the system. We would set up a cluster and proceed with processing using Spark. In only a few instances did I genuinely feel that Spark was not the right tool for the job. This experience pushed me to develop a lightweight ingestion framework using Pandas. However, this framework did not perform as expected, struggling with medium to large files. Recently, I've started using Polars for some tasks and I have been impressed by its performance in processing datasets with several million rows. This has led to me setting up a different benchmarking for all of these tools. Let's dive into it! A Little Bit of Context Pandas We don't have to forget that Pandas has been the dominant tool for data manipulation, exploration, and analysis. Pandas has risen in popularity among data scientists thanks to its similarities with the R grid view. Moreover, it is synchronized with other Python libraries related to the machine learning field: NumPy is a mathematical library for implementing linear algebra and standard calculations. Pandas is based on NumPy. Scikit-learn is the reference library for machine learning applications. Normally, all the data used for the model has been loaded, visualized, and analyzed with Pandas or NumPy. PySpark Spark is a free and distributed platform that transforms the paradigm of how big data processing is done, with PySpark as its Python library. It offers a unified computing engine with exceptional features: In-memory processing: Spark's major feature is its in-memory architecture, which is fast as it keeps the data in memory rather than on disk. Fault tolerance: The failure tolerance mechanisms that are built into the software ensure dependable data processing. Resilient Distributed Datasets do data tracking and allow for automatic recovery in case of failures such as failures. Scalability: Spark’s horizontally scalable architecture processes large datasets adaptively and distributes much faster to clusters. The data is distributed, using the massive power of all nodes in the cluster. Polars Polars is a Python library built on top of Rust, combining the flexibility and user-friendliness of Python with the speed and scalability of Rust. Rust is a low-level language that prioritizes performance, reliability, and productivity. It is memory efficient and gives performance par with C and C++. On the other hand, Polars uses Apache Arrow as its query engine to execute vectorized queries. Apache Arrow is a cross-language development platform for fast in-memory processing. Polars enable instantaneity in executing the operations of tabular data manipulation, analysis, and transformation, favoring its utilization with large datasets. Moreover, its syntax is like SQL, and the expressive complexity of data processing is easy to demonstrate. Another capability is its lazyness which evaluates queries and applies query optimization. Benchmarking Set up Here is a link to the GitHub project with all the information. There are four notebooks for each tool (two for polars for testing eager and lazy evaluation). The code will extract time execution for the following tasks: Reading Filtering Aggregations Joining Writing There are five datasets with multiple sizes, 50,000, 250,000, 1,000,000, 5,000,000, and 25,000,000 of rows. The idea is to test different scenarios and sizes. The data used for this test is a financial dataset from Kaggle. The tests were executed in: macOS Sonoma Apple M1 Pro 32 GB Table of Execution Times Row Size Pandas Polars Eager Polars Lazy PySpark 50,000 Rows 0.368 0.132 0.078 1.216 250,000 Rows 1.249 0.096 0.156 0.917 1,000,000 Rows 4.899 0.302 0.300 1.850 5,000,000 Rows 24.320 1.605 1.484 7.372 25,000,000 Rows 187.383 13.001 11.662 44.724 Analysis Pandas performed poorly, especially as dataset sizes increased. However, it could handle small datasets with decent performance time. PySpark, while being executed in a single machine, shows considerable improvement over Pandas when the dataset size grows. Polars, both in eager and lazy configurations, significantly outperforms the other tools, showing improvements up to 95-97% compared to Pandas and 70-75% compared to PySpark, confirming its efficiency in handling large datasets on a single machine. Visual Representations These visual aids help underline the relative efficiencies of the different tools across various test conditions. Conclusion The benchmarking results provided offer a clear insight into the performance scalability of four widely-used data processing tools across varying dataset sizes. From the analysis, several critical conclusions emerge: Pandas performance scalability: Popular for data manipulation in smaller datasets, it struggles significantly as the data volume increases indicating it is not the best for high-volume data. However, its integration over a lot of Machine Learning and stadistic libraries makes it indispensable for Data Science teams. Efficiency of Polars: Configurations of Polars (Eager and Lazy) demonstrate exceptional performance across all tested scales, outperforming both Pandas and PySpark by a wide margin, making Polars an efficient tool capable of processing large datasets. However, Polars has not released yet a major version of Python and until that, I don't recommend it for Production systems. Tool selection strategy: The findings underscore the importance of selecting the right tool based on the specific needs of the project and the available resources. For small to medium-sized datasets, Polars offers a significant performance advantage. For large-scale distributed processing, PySpark remains a robust option. Future considerations: As dataset sizes continue to grow and processing demands increase, the choice of data processing tools will become more critical. Tools like Polars built over Rust are emerging and the results have to be considered. Also, the tendency to use Spark as a solution for processing everything is disappearing and these tools are taking their place when there is no need for large-scale distributed systems. Use the right tool for the right job!
Logging is essential for any software system. Using logs, you can troubleshoot a wide range of issues, including debugging an application bug, security defect, system slowness, etc. In this article, we will discuss how to use Python logging effectively using custom attributes. Python Logging Before we delve in, I briefly want to explain a basic Python logging module with an example. #!/opt/bb/bin/python3.7 import logging import sys root = logging.getLogger() root.setLevel(logging.DEBUG) std_out_logger = logging.StreamHandler(sys.stdout) std_out_logger.setLevel(logging.INFO) std_out_formatter = logging.Formatter("%(levelname)s - %(asctime)s %(message)s") std_out_logger.setFormatter(std_out_formatter) root.addHandler(std_out_logger) logging.info("I love Dzone!") The above example prints the following when executed: INFO - 2024-03-09 19:49:07,734 I love Dzone! In the example above, we are creating the root logger and the logging format for log messages. On line 6, logging.getLogger() returns the logger if already created; if not, it goes one level above the hierarchy and returns the parent logger. We define our own StreamHandler to print the log message at the console. Whenever we log messages, it is essential to log the basic attributes of the LogRecord. On line 10, we define the basic format that includes level name, time in string format, and the actual message itself. The handler thus created is set at the root logger level. We could use any pre-defined log attribute name and the format from the LogRecord library. However, let's say you want to print some additional attributes like contextId, a custom logging adapter to the rescue. Logging Adapter class MyLoggingAdapter(logging.LoggerAdapter): def __init__(self, logger): logging.LoggerAdapter.__init__(self, logger=logger, extra={}) def process(self, msg, kwargs): return msg, kwargs We create our own version of Logging Adapter and pass "extra" parameters as a dictionary for the formatter. ContextId Filter import contextvars class ContextIdFilter(logging.Filter): context_id = contextvars.ContextVar('context_id', default='') def filter(self, record): # add a new UUID to the context. req_id = str(uuid.uuid4()) if not self.context_id.get(): self.context_id.set(req_id) record.context_id = self.context_id.get() return True We create our own filter that extends the logging filter, which returns True if the specified log record should be logged. We simply add our parameter to the log record and return True always, thus adding our unique id to the record. In our example above, a unique id is generated for every new context. For an existing context, we return already stored contextId from the contextVars. Custom Logger import logging root = logging.getLogger() root.setLevel(logging.DEBUG) std_out_logger = logging.StreamHandler(sys.stdout) std_out_logger.setLevel(logging.INFO) std_out_formatter = logging.Formatter("%(levelname)s - %(asctime)s ContextId:%(context_id)s %(message)s") std_out_logger.setFormatter(std_out_formatter) root.addHandler(std_out_logger) root.addFilter(ContextIdFilter()) adapter = MyLoggingAdapter(root) adapter.info("I love Dzone!") adapter.info("this is my custom logger") adapter.info("Exiting the application") Now let's put it together in our logger file. Add the contextId filter to the root. Please note that we are using our own adapter in place of logging wherever we need to log the message. Running the code above prints the following message: INFO - 2024-04-20 23:54:59,839 ContextId:c10af4e9-6ea4-4cdf-9743-ea24d0febab6 I love Dzone! INFO - 2024-04-20 23:54:59,842 ContextId:c10af4e9-6ea4-4cdf-9743-ea24d0febab6 this is my custom logger INFO - 2024-04-20 23:54:59,843 ContextId:c10af4e9-6ea4-4cdf-9743-ea24d0febab6 Exiting the application By setting root.propagate = False, events logged to this logger will be passed to the handlers of higher logging, aka parent logging class. Conclusion Python does not provide a built-in option to add custom parameters in logging. Instead, we create a wrapper logger on top of the Python root logger and print our custom parameters. This would be helpful at the time of debugging request-specific issues.
I love playing with new toys. For development, that translates to trying out new tools and frameworks. I’m always on the hunt for ways to speed up and streamline my app development workflow. And so, recently, I came across Remix, a JavaScript-based web framework that prioritizes fast page loads and is focused on running at the edge. Since I already have extensive experience with Node.js and React, I wanted to give Remix a try. I decided to go through the Remix tutorial to see just how quickly I could get through it while still understanding what I was doing. Then, I wanted to see how easy it would be to deploy the final app to the cloud. I used Heroku for that. So, let’s dive in together. Here we go! Introducing Remix This is what Remix says about itself: Remix is a full stack web framework that lets you focus on the user interface and work back through web standards to deliver a fast, slick, and resilient user experience. Most important for me, though, is how Remix aims to simplify the developer experience. I really enjoyed reading their take on why Remix isn’t just another framework but is — in addition to being a robust web dev framework — also a way to gain transferable knowledge that will serve you even long after you’ve stopped using Remix. Remix seems to be great for developers who have React experience but — like me — want something that helps them spin up an app faster, prettier, and easier. I found that Remix works to remove some of that slog, providing conveniences to help streamline the development experience. The Remix tutorial was very comprehensive, covering lots of different features. And I got through it in less than 30 minutes. Introducing Heroku At the end of the day, Remix was going to give me a full-fledged SPA. And that’s awesome. But I also wanted a way to deploy my application to run the code quickly and simply. Lately, I’ve been using Heroku more and more for this kind of thing. It’s fast, and it’s reliable. Remix Tutorial Highlights I encourage you to go through the tutorial yourself. I’m not going to rehash it all here, but I will provide some highlights of features. Built on React Router Remix is built on top of React Router, which is an easy-to-use routing library that integrates seamlessly into your React applications. React Router supports nested routes, so you can render the layout for child routes inside parent layouts. This is one of the things I love. Done this way, routing just makes sense. It’s easy and intuitive to implement. Note: In the Contact Route UI section of the tutorial, the code snippet for app/routes/contacts.$contactID.tsx (at line 10) references a URL for an avatar. It turns out that the URL for the avatar wasn’t returning any data. I needed to change it to https://placekitten.com/200/200 (removed the /g in the original path) instead. Client-Side Routing and Fast Data Loading Remix also features client-side routing through React Router. This means clicks that are meant to fetch data and render UI components won’t need to request and reload the entire document. Navigating between views is snappy, giving you a smooth experience with fast transitions. On top of this, Remix loads data in parallel with fetch requests, so all of the heavy lifting is happening in the background. The end user doesn’t feel it or notice it. Data just loads into components super fast. Remix does this through its loader API, which lets you define a function in each route that provides data to the route upon render. Submitting Forms Without Navigation This is a challenge that I’ve dealt with on many occasions. Sometimes, you want to submit a form to make some sort of data update on the backend, but you don’t want to trigger a navigation in your browser. The Remix tutorial describes the situation this way: We aren't creating or deleting a new record, and we don't want to change pages. We simply want to change the data on the page we're looking at. For this, Remix provides the useFetcher hook to work in conjunction with a route action. Forms and data manipulation actions on the pages felt smooth and fast. Here’s a quick screen vid of the final result of the tutorial app: They said that going through the tutorial would take about 30 minutes, and they were right. And I learned a lot along the way. Deploying to Heroku Was Blazing Fast Ahh, but then it came time to deploy. Running the tutorial app on my local machine was great. But how easy would it be to get this app — or any app I build with Remix — deployed to the cloud? The one thing I needed to add to my GitHub repo with all the completed code was a Procfile, which tells Heroku how to spin up my app. My Procfile is one line, and it looks like this: Shell web: npm run dev I logged into Heroku from the CLI. Next, I needed to create a Heroku app and deploy my code. How long would that take? And… it took 42 seconds. I challenge you to beat my time. Just like that, my Remix app was up and running! Production Deployment Remix provides the Remix App Server (@remix-run/serve) to serve up its applications. To deploy your application to production, just make sure you’ve added @remix-run/serve to your project dependencies. Heroku will automatically execute npm run build for you. So, the only other step is to change your Procfile to the following: Shell web: npm run start Then, push your updated code to Heroku. Your production deployment will be off and running! Conclusion I’m always looking out for newer, faster, and better ways to accomplish the tasks that I need to tackle regularly. I often find myself trying to spin up simple apps with clean and fast navigation, forms, and data handling. Usually, I’m building quick prototypes for prospective clients or some helpful mini-app for a friend. For deployment and running my code on the web, I keep going back to my tried-and-true, Heroku. But in stumbling upon Remix, I think I’ve found my go-to for easy single-page app development in the coming days. I know that by going through the tutorial, I’ve really only scratched the surface of what Remix has to offer. So I’m looking forward to playing with it a lot more. Happy coding!
Network graphs are a practical and effective tool in data visualization, particularly useful for illustrating the relationships and connections within complex systems. These charts are useful for understanding structures in various contexts, from social networks to corporate hierarchies. In this tutorial, we'll delve into a quick path to creating a compelling, interactive network graph using JavaScript. We'll use the Volkswagen Group as our example, mapping out its subsidiaries and product lines to showcase how network graphs can make complex organizational structures understandable and accessible. By the end of this step-by-step guide, you'll have a clear understanding of how to quickly construct and customize a JS-based network graph. Buckle up, as it's time to hit the road! Understanding Network Graphs Network graphs consist of nodes and edges — nodes represent entities such as individuals or organizations, while edges depict the relationships between them. These visuals are invaluable for dissecting and displaying the architecture of complex networks, revealing both overt and subtle connections. In practical terms, network graphs can help illustrate the hierarchy within a corporation, the interaction between different departments, or the flow of communication or resources. Visually, these graphs use various elements like node size, color, and edge thickness to convey information about the importance, type, and strength of relationships. Below is a preview of what we will create by the end of this tutorial — a fully interactive network graph that not only serves as a visual map of the Volkswagen Group but also utilizes the dynamic features of JavaScript for a deeper exploration of data. Step-By-Step Guide To Building a Network Graph Creating a network graph involves several key steps, each contributing to the final outcome. Here’s a brief overview of what we'll cover in this tutorial: Creating an HTML page: This is where we set up the structure for our visualization, providing a canvas on which our network graph will be displayed. Including the necessary JavaScript files: Essential for graph functionality, we'll incorporate scripts needed to build and manage our network graph. Preparing the data: Here, we'll organize the data into a format that can be smoothly visualized in a network graph, distinguishing between different types of nodes and their connections. Writing the JavaScript code for visualization: The final step involves scripting the logic that brings our graph to life, enabling interactivity to better understand the underlying data. Each of these steps will be detailed in the following sections, ensuring you have a clear roadmap to follow as you create your own network graph using JavaScript. Let’s dive in and start visualizing! Step 1: Setting Up Your HTML Start by creating the basic structure for your web page if you are building from scratch. This includes setting up an HTML document that will host your network graph. Here is how you can write your HTML: HTML <!DOCTYPE html> <html> <head> <title>Network Graph in JavaScript</title> <style type="text/css"> html, body, #container { width: 100%; height: 100%; margin: 0; padding: 0; } </style> </head> <body> <div id="container"></div> </body> </html> This simple HTML structure is foundational. The <div> tag identified by id="container" is where our network graph will be rendered. The accompanying CSS ensures the graph uses the entire screen, optimizing visual space and ensuring that the graph is both prominent and clear. Step 2: Summoning JavaScript Files To integrate our network graph into the web environment without much hassle, let’s incorporate a JavaScript charting library directly within the HTML framework. There are multiple libraries out there, although not all of them support network graphs. You can check out this comprehensive comparison of JavaScript charting libraries, which details some features of various libraries, including support for network graphs. Of those listed, libraries such as amCharts, AnyChart, D3.js, and HighCharts are popular options that support network graphs. For this tutorial, we'll utilize AnyChart. It's one of the libraries I've used extensively over the years, and I thought it would work well to illustrate the common logic of the process and be easy enough to get started for those of you who are new to JS charting. Whichever libraries you opt for, here's how the necessary JS scripts are woven into the HTML, positioned within the <head> section. Additionally, we prepare the <body> section to include our forthcoming JavaScript code using those scripts, which will dynamically render the network graph: HTML <html> <head> <title>Network Graph in JavaScript</title> <style type="text/css"> html, body, #container { width: 100%; height: 100%; margin: 0; padding: 0; } </style> <script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-core.min.js"></script> <script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-graph.min.js"></script> </head> <body> <div id="container"></div> <script> // JS code for the network graph will be here </script> </body> </html> Step 3: Sculpting Data With our HTML ready and JS files at hand, it's time to define our nodes and edges — the fundamental components of our network graph. This involves structuring the Volkswagen Group's data, from the parent company to each product line. JavaScript var data = { "nodes": [ {"id": "Volkswagen Group", "group": "CoreCompany"}, {"id": "Audi", "group": "ChildCompany"}, {"id": "Audi Cars", "group": "Product"}, // More nodes here... ], "edges": [ {"from": "Volkswagen Group", "to": "Audi"}, {"from": "Audi", "to": "Audi Cars"}, // More edges here... ] }; Step 4: Choreographing JavaScript To Visualize Network This crucial step transforms the structured data into a vibrant, interactive network graph within the provided HTML canvas. To ensure clarity and ease of understanding, I’ve divided this process into three intuitive sub-steps, each demonstrated with its own code snippet. 1. Initializing First, we ensure that our JavaScript visualization code executes only once the HTML document is fully loaded. This is critical as it prevents any DOM manipulation attempts before the HTML is fully prepared. JavaScript anychart.onDocumentReady(function () { // Initialization of the network graph will happen here }); 2. Creating Graph Instance Inside the function, we initialize our network graph. Here, we create a graph object and chart using our predefined data. This instance will serve as the basis for our visualization. var chart = anychart.graph(data); 3. Setting Container for Graph The next step is to specify where on the webpage our network graph should be visually rendered. This is linked to the HTML container we defined earlier. JavaScript chart.container("container"); 4. Rendering Graph The final step is to instruct the graph to draw itself within the designated container. This action brings our data to life, displaying the complex relationships within the Volkswagen Group. JavaScript chart.draw(); These sub-steps collectively ensure that our network graph is not only initialized with the correct data and configurations but also properly placed and rendered on the web page, providing a dynamic and informative visual exploration of corporate relationships. Network Graph Visualization Unfolded Now that our network graph is complete, you can see the resulting picture below, which showcases the complex structure of the Volkswagen Group. This interactive chart is not only informative but also a testament to the power of JavaScript when it comes to cross-platform interactive data visualization. For a hands-on experience, I invite you to explore this chart interactively on CodePen, where you can modify the code, experiment with different configurations, and better understand the intricacies of network graphs. The complete HTML/CSS/JavaScript code for this project is available below — use it as a reference or a starting point for your own visualization projects. HTML <html> <head> <title>Network Graph in JavaScript</title> <style type="text/css"> html, body, #container { width: 100%; height: 100%; margin: 0; padding: 0; } </style> <script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-core.min.js"></script> <script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-graph.min.js"></script> </head> <body> <div id="container"></div> <script> anychart.onDocumentReady(function () { // Create data const data = { "nodes": [ // parent company {"id": "Volkswagen Group", "group": "CoreCompany"}, // child companies {"id": "Audi", "group": "ChildCompany"}, {"id": "CUPRA", "group": "ChildCompany"}, {"id": "Ducati", "group": "ChildCompany"}, {"id": "Lamborghini", "group": "ChildCompany"}, {"id": "MAN", "group": "ChildCompany"}, {"id": "Porsche", "group": "ChildCompany"}, {"id": "Scania", "group": "ChildCompany"}, {"id": "SEAT", "group": "ChildCompany"}, {"id": "Škoda", "group": "ChildCompany"}, {"id": "Volkswagen", "group": "ChildCompany"}, // products {"id": "Audi Cars", "group": "Product"}, {"id": "Audi SUVs", "group": "Product"}, {"id": "Audi Electric Vehicles", "group": "Product"}, {"id": "CUPRA Performance Cars", "group": "Product"}, {"id": "CUPRA SUVs", "group": "Product"}, {"id": "Ducati Motorcycles", "group": "Product"}, {"id": "Lamborghini Sports Cars", "group": "Product"}, {"id": "Lamborghini SUVs", "group": "Product"}, {"id": "MAN Trucks", "group": "Product"}, {"id": "MAN Buses", "group": "Product"}, {"id": "Porsche Sports Cars", "group": "Product"}, {"id": "Porsche SUVs", "group": "Product"}, {"id": "Porsche Sedans", "group": "Product"}, {"id": "Scania Trucks", "group": "Product"}, {"id": "Scania Buses", "group": "Product"}, {"id": "SEAT Cars", "group": "Product"}, {"id": "SEAT SUVs", "group": "Product"}, {"id": "SEAT Electric Vehicles", "group": "Product"}, {"id": "Škoda Cars", "group": "Product"}, {"id": "Škoda SUVs", "group": "Product"}, {"id": "Škoda Electric Vehicles", "group": "Product"}, {"id": "Volkswagen Cars", "group": "Product"}, {"id": "Volkswagen SUVs", "group": "Product"}, {"id": "Volkswagen Vans", "group": "Product"}, {"id": "Volkswagen Trucks", "group": "Product"} ], "edges": [ // parent to child companies {"from": "Volkswagen Group", "to": "Audi"}, {"from": "Volkswagen Group", "to": "CUPRA"}, {"from": "Volkswagen Group", "to": "Ducati"}, {"from": "Volkswagen Group", "to": "Lamborghini"}, {"from": "Volkswagen Group", "to": "MAN"}, {"from": "Volkswagen Group", "to": "Porsche"}, {"from": "Volkswagen Group", "to": "Scania"}, {"from": "Volkswagen Group", "to": "SEAT"}, {"from": "Volkswagen Group", "to": "Škoda"}, {"from": "Volkswagen Group", "to": "Volkswagen"}, // child companies to products {"from": "Audi", "to": "Audi Cars"}, {"from": "Audi", "to": "Audi SUVs"}, {"from": "Audi", "to": "Audi Electric Vehicles"}, {"from": "CUPRA", "to": "CUPRA Performance Cars"}, {"from": "CUPRA", "to": "CUPRA SUVs"}, {"from": "Ducati", "to": "Ducati Motorcycles"}, {"from": "Lamborghini", "to": "Lamborghini Sports Cars"}, {"from": "Lamborghini", "to": "Lamborghini SUVs"}, {"from": "MAN", "to": "MAN Trucks"}, {"from": "MAN", "to": "MAN Buses"}, {"from": "Porsche", "to": "Porsche Sports Cars"}, {"from": "Porsche", "to": "Porsche SUVs"}, {"from": "Porsche", "to": "Porsche Sedans"}, {"from": "Scania", "to": "Scania Trucks"}, {"from": "Scania", "to": "Scania Buses"}, {"from": "SEAT", "to": "SEAT Cars"}, {"from": "SEAT", "to": "SEAT SUVs"}, {"from": "SEAT", "to": "SEAT Electric Vehicles"}, {"from": "Škoda", "to": "Škoda Cars"}, {"from": "Škoda", "to": "Škoda SUVs"}, {"from": "Škoda", "to": "Škoda Electric Vehicles"}, {"from": "Volkswagen", "to": "Volkswagen Cars"}, {"from": "Volkswagen", "to": "Volkswagen SUVs"}, {"from": "Volkswagen", "to": "Volkswagen Vans"}, {"from": "Volkswagen", "to": "Volkswagen Trucks"} ]}; // Initialize the network graph with the provided data structure const chart = anychart.graph(data); // Specify the HTML container ID where the chart will be rendered chart.container("container"); // Initiate the rendering of the chart chart.draw(); }); </script> </body> </html> Customizing JavaScript Network Graph After establishing a basic network graph of the Volkswagen Group, let's enhance its functionality and aesthetics. This part of our tutorial will guide you through some of the various customization options, showing you how to evolve your basic JavaScript network graph into a more informative and visually appealing visualization. Each customization step builds upon the previous code, introducing new features and modifications, and providing the viewer with a deeper understanding of the relationships within the Volkswagen corporate structure. Displaying Node Labels Understanding what each node represents is crucial in a network graph. By default, node labels might not be displayed, but we can easily enable them to make our graph more informative. JavaScript chart.nodes().labels().enabled(true); Enabling labels on nodes ensures that each node is clearly identified, making it easier for users to understand the data at a glance without needing to interact with each node individually. Configuring Edge Tooltips To enhance user interaction, tooltips can provide additional information when hovering over connections (edges) between nodes. This step involves configuring a tooltip format that shows the relationship between connected nodes. JavaScript chart.edges().tooltip().format("{%from} owns {%to}"); This tooltip configuration helps to clarify the connections within the graph, showing direct ownership or affiliation between the parent company and its subsidiaries, enhancing the user's comprehension and interaction with the graph. Customizing Node Appearance Visual differentiation helps to quickly identify types of nodes. We can customize the appearance of nodes based on their group classification, such as distinguishing between the core company, child companies, and products. JavaScript // 1) configure settings for nodes representing the core company chart.group('CoreCompany') .stroke('none') .height(45) .fill('red') .labels().fontSize(15); // 2) configure settings for nodes representing child companies chart.group('ChildCompany') .stroke('none') .height(25) .labels().fontSize(12); // 3) configure settings for nodes representing products chart.group('Product') .shape('square') .stroke('black', 1) .height(15) .labels().enabled(false); These settings enhance the visual hierarchy of the graph. The core company node is more prominent, child companies are easily distinguishable, and product nodes are less emphasized but clearly structured, aiding in the quick visual processing of the graph's structure. Setting Chart Title Adding a title to the chart provides context and introduces the visual content. It's a simple but effective way to inform viewers about the purpose of the network graph. JavaScript chart.title("Volkswagen Group Network"); The title "Volkswagen Group Network" immediately informs the viewer of the graph's focus, adding a professional touch and enhancing the overall clarity. Final Network Graph Visualization With these customizations, our network graph is now a detailed and interactive visualization, ready for in-depth exploration. Below is the complete code incorporating all the enhancements discussed. This version of the JS-based network graph is not only a tool for displaying static data but also a dynamic map of the Volkswagen Group's complex structure. I invite you to view and interact with this chart on CodePen to see it in action and to tweak the code further to suit your specific needs. For your convenience, the full network graph code is also provided below: HTML <html> <head> <title>Network Graph in JavaScript</title> <style type="text/css"> html, body, #container { width: 100%; height: 100%; margin: 0; padding: 0; } </style> <script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-core.min.js"></script> <script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-graph.min.js"></script> </head> <body> <div id="container"></div> <script> anychart.onDocumentReady(function () { // Create data const data = { "nodes": [ // parent company {"id": "Volkswagen Group", "group": "CoreCompany"}, // child companies {"id": "Audi", "group": "ChildCompany"}, {"id": "CUPRA", "group": "ChildCompany"}, {"id": "Ducati", "group": "ChildCompany"}, {"id": "Lamborghini", "group": "ChildCompany"}, {"id": "MAN", "group": "ChildCompany"}, {"id": "Porsche", "group": "ChildCompany"}, {"id": "Scania", "group": "ChildCompany"}, {"id": "SEAT", "group": "ChildCompany"}, {"id": "Škoda", "group": "ChildCompany"}, {"id": "Volkswagen", "group": "ChildCompany"}, // products {"id": "Audi Cars", "group": "Product"}, {"id": "Audi SUVs", "group": "Product"}, {"id": "Audi Electric Vehicles", "group": "Product"}, {"id": "CUPRA Performance Cars", "group": "Product"}, {"id": "CUPRA SUVs", "group": "Product"}, {"id": "Ducati Motorcycles", "group": "Product"}, {"id": "Lamborghini Sports Cars", "group": "Product"}, {"id": "Lamborghini SUVs", "group": "Product"}, {"id": "MAN Trucks", "group": "Product"}, {"id": "MAN Buses", "group": "Product"}, {"id": "Porsche Sports Cars", "group": "Product"}, {"id": "Porsche SUVs", "group": "Product"}, {"id": "Porsche Sedans", "group": "Product"}, {"id": "Scania Trucks", "group": "Product"}, {"id": "Scania Buses", "group": "Product"}, {"id": "SEAT Cars", "group": "Product"}, {"id": "SEAT SUVs", "group": "Product"}, {"id": "SEAT Electric Vehicles", "group": "Product"}, {"id": "Škoda Cars", "group": "Product"}, {"id": "Škoda SUVs", "group": "Product"}, {"id": "Škoda Electric Vehicles", "group": "Product"}, {"id": "Volkswagen Cars", "group": "Product"}, {"id": "Volkswagen SUVs", "group": "Product"}, {"id": "Volkswagen Vans", "group": "Product"}, {"id": "Volkswagen Trucks", "group": "Product"} ], "edges": [ // parent to child companies {"from": "Volkswagen Group", "to": "Audi"}, {"from": "Volkswagen Group", "to": "CUPRA"}, {"from": "Volkswagen Group", "to": "Ducati"}, {"from": "Volkswagen Group", "to": "Lamborghini"}, {"from": "Volkswagen Group", "to": "MAN"}, {"from": "Volkswagen Group", "to": "Porsche"}, {"from": "Volkswagen Group", "to": "Scania"}, {"from": "Volkswagen Group", "to": "SEAT"}, {"from": "Volkswagen Group", "to": "Škoda"}, {"from": "Volkswagen Group", "to": "Volkswagen"}, // child companies to products {"from": "Audi", "to": "Audi Cars"}, {"from": "Audi", "to": "Audi SUVs"}, {"from": "Audi", "to": "Audi Electric Vehicles"}, {"from": "CUPRA", "to": "CUPRA Performance Cars"}, {"from": "CUPRA", "to": "CUPRA SUVs"}, {"from": "Ducati", "to": "Ducati Motorcycles"}, {"from": "Lamborghini", "to": "Lamborghini Sports Cars"}, {"from": "Lamborghini", "to": "Lamborghini SUVs"}, {"from": "MAN", "to": "MAN Trucks"}, {"from": "MAN", "to": "MAN Buses"}, {"from": "Porsche", "to": "Porsche Sports Cars"}, {"from": "Porsche", "to": "Porsche SUVs"}, {"from": "Porsche", "to": "Porsche Sedans"}, {"from": "Scania", "to": "Scania Trucks"}, {"from": "Scania", "to": "Scania Buses"}, {"from": "SEAT", "to": "SEAT Cars"}, {"from": "SEAT", "to": "SEAT SUVs"}, {"from": "SEAT", "to": "SEAT Electric Vehicles"}, {"from": "Škoda", "to": "Škoda Cars"}, {"from": "Škoda", "to": "Škoda SUVs"}, {"from": "Škoda", "to": "Škoda Electric Vehicles"}, {"from": "Volkswagen", "to": "Volkswagen Cars"}, {"from": "Volkswagen", "to": "Volkswagen SUVs"}, {"from": "Volkswagen", "to": "Volkswagen Vans"}, {"from": "Volkswagen", "to": "Volkswagen Trucks"} ]}; // Initialize the network graph with the provided data structure const chart = anychart.graph(data); // Customization step #1: // display chart node labels chart.nodes().labels().enabled(true); // Customization step #2: // configure edge tooltips chart.edges().tooltip().format("{%from} owns {%to}"); // Customization step #3: // customizing node appearance: // 1) configure settings for nodes representing the core company chart.group('CoreCompany') .stroke('none') .height(45) .fill('red') .labels().fontSize(15); // 2) configure settings for nodes representing child companies chart.group('ChildCompany') .stroke('none') .height(25) .labels().fontSize(12); // 3) configure settings for nodes representing products chart.group('Product') .shape('square') .stroke('black', 1) .height(15) .labels().enabled(false); // Customization step #4: // set the title of the chart for context chart.title("Volkswagen Group Network"); // Specify the HTML container ID where the chart will be rendered chart.container("container"); // Initiate the rendering of the chart chart.draw(); }); </script> </body> </html> Conclusion Congratulations on completing this tutorial on crafting a dynamic JavaScript network graph! You've not only learned to visualize complex structures but also how to customize the graph to enhance its clarity and interactivity. As you continue to explore the possibilities within network graph visualizations, I encourage you to delve deeper, experiment with further customization, and look for some inspiring chart examples out there. The skills you've acquired today are just the beginning. Keep experimenting, tweaking, and learning to unlock the full potential of data visualization in your projects.
This guide is a valuable resource for Java developers seeking to create robust and efficient GraphQL API servers. This detailed guide will take you through all the steps for implementing GraphQL in Java for real-world applications. It covers the fundamental concepts of GraphQL, including its query language and data model, and highlights its similarities to programming languages and relational databases. It also offers a practical step-by-step process for building a GraphQL API server in Java utilizing Spring Boot, Spring for GraphQL, and a relational database. The design emphasizes persistence, flexibility, efficiency, and modernity. Additionally, the blog discusses the trade-offs and challenges involved in the process. Finally, it presents an alternative path beyond the conventional approach, suggesting the potential benefits of a "GraphQL to SQL compiler" and exploring the option of acquiring a GraphQL API instead of building one. What Is GraphQL and Why Do People Want It? GraphQL is a significant evolution in the design of Application Performance Interfaces (API). Still, even today, it can be challenging to know how to get started with GraphQL, how to progress, and how to move beyond the conventional wisdom of GraphQL. This is especially true for Java. This guide attempts to cover all these bases in three steps. First, I'll tell you what GraphQL is, and as a bonus, I'll let you know what GraphQL really is. Second, I'll show you how to implement state-of-the-art GraphQL in Java for an actual application. Third, I'll offer you an alternative path beyond the state-of-the-art that may suit your needs better in every dimension. So, what is GraphQL? Well, GraphQL.org says: "GraphQL is a query language for your API and a server-side runtime for executing queries using a type system you define for your data. GraphQL isn’t tied to any specific database or storage engine and is instead backed by your existing code and data." That's correct, but let's look at it from different directions. Sure, GraphQL is "a query language for your API," but you might as well just say that it is an API or a way of building an API. That contrasts it with REST, which GraphQL is an evolution from and an alternative to. GraphQL offers several improvements over REST: Expressivity: A client can say what data they need from a server, no more and no less. Efficiency: Expressivity leads to efficiency gains, reducing network chatter and wasted bandwidth. Discoverability: To know what to say to a server, a client needs to know what can be said to a server. Discoverability allows data consumers to know exactly what's available from data producers. Simplicity: GraphQL puts clients in the driver's seat, so good ergonomics for driving should exist. GraphQL's highly-regular machine-readable syntax, simple execution model, and simple specifications lend themselves to inter-operable and composable tools: Query tools Schema registries Gateways Code generators Client libraries But GraphQL is also a data model for its query language, and despite the name, neither the query language nor the data model is very "graphy." The data model is essentially just JSON. The query language looks like JSON and can be boiled down to a few simple features: Types: A type is a simple value (a scalar) or a set of fields (an object). While you naturally introduce new types for your own problem domain, there are a few special types (called Operations). One of these is Query, which is the root of requests for data (setting aside Subscription for now, for the sake of simplicity). A type essentially is a set of rules for determining if a piece of data–or a request for that piece of data–validly conforms to the given type. A GraphQL type is very much like a user-defined type in programming languages like C++, Java, and Typescript, and is very much like a table in a relational database. Field: A field within one type contains one or more pieces of data that validly conform to another type, thus establishing relationships among types. A GraphQL field is very much like a property of a user-defined type in a programming language and is very much like a column in a relational database. Relationships between GraphQL types are very much like pointers or references in programming languages and are very much like foreign key constraints in relational databases. There's more to GraphQL, but that's pretty much the essence. Note the similarities between concepts in GraphQL and programming languages, and especially between concepts in GraphQL and relational databases. OK, we’ve covered what GraphQL is, but what is GraphQL for? Why should we consider it as an alternative to REST? I listed above some of GraphQL's improvements over typical REST–expressivity, efficiency, discoverability, simplicity–but another perhaps more concise way to put it is this: GraphQL's expressivity, efficiency, discoverability, and simplicity make life easier for data consumers. However, there's a corollary: GraphQL's expressivity, efficiency, discoverability, and simplicity make life harder for data producers. That's you! If you're a Java programmer working with GraphQL, your job is probably to produce GraphQL API servers for clients to consume (there are relatively few Java settings on the client). Offering all that expressivity, discoverability, etc. is not easy, so how do you do it? How Do I Provide the GraphQL That People Want, Especially as a Java Developer? On the journey to providing a GraphQL API, we confront a series of interdependent choices that can make life easier (or harder) for data producers. One choice concerns just how "expressive, efficient, discoverable, and simple" our API is, but let's set that aside for a moment and treat it as an emergent property of the other choices we make. Life is about trade-offs, after all. Another choice is over build-versus-buy [PDF], but let's also set that aside for a moment, accept that we're building a GraphQL API server (in Java), explore how that is done, and evaluate the consequences. If you’re building a GraphQL API server in Java, another choice is whether to build it completely from scratch or to use libraries and frameworks and if the latter, then which libraries and frameworks to use. Let's set aside a complete DIY solution as pointless masochism, and survey the landscape of Java libraries and frameworks for GraphQL. As of writing (May 2024) there are three important interdependent players in this space: Graphql-java:graphql-java is a lower-level foundational library for working with GraphQL in Java, which began in 2015. Since the other players depend on and use graphql-java, consider graphql-java to be non-optional. Another crucial choice is whether you are or are not using the Spring Boot framework. If you're not using Spring Boot then stop here! Since this is a prerequisite, in the parlance of the ThoughtWorks Radar this is unavoidably adopt. Netflix DGS: DGS is a higher-level library for working with GraphQL in Java with Spring Boot, which began in 2021. If you're using DGS then you will also be using graphql-java under the hood, but typically you won't come into contact with graphql-java. Instead, you will be sprinkling annotations throughout the Java code to identify the code segments called "resolvers" or "data fetchers” that execute GraphQL requests. ThoughtWorks said Trial as of 2023 for DGS but this is a dynamic space and their opinion may have changed. I say Hold for the reasons given below. Spring for GraphQL: Spring for GraphQL is another higher-level library for working with GraphQL in Java with Spring Boot, which began around 2023, and is also based on annotations. It may be too new for ThoughtWorks, but it's not too new for me. I say Adopt and read on for why. The makers of Spring for GraphQL say: "It is a joint collaboration between the GraphQL Java team and Spring engineering…It aims to be the foundation for all Spring, GraphQL applications." Translation: The Spring team has a privileged collaboration with the makers of the foundational library for GraphQL in Java, and intends to "win" in this space. Moreover, the makers of Netflix DGS have much to say about that library's relationship to Spring for GraphQL. "Soon after we open-sourced the DGS framework, we learned about parallel efforts by the Spring team to develop a GraphQL framework for Spring Boot. The Spring GraphQL project was in the early stages at the time and provided a low level of integration with graphql-java. Over the past year, however, Spring GraphQL has matured and is mostly at feature parity with the DGS Framework. We now have 2 competing frameworks that solve the same problems for our users. Today, new users must choose between the DGS Framework or Spring GraphQL, thus missing out on features available in one framework but not the other. This is not an ideal situation for the GraphQL Java community. For the maintainers of DGS and Spring GraphQL, it would be far more effective to collaborate on features and improvements instead of having to solve the same problem independently. Finally, a unified community would provide us with better channels for feedback. The DGS framework is widely used and plays a vital role in the architecture of many companies, including Netflix. Moving away from the framework in favor of Spring-GraphQL would be a costly migration without any real benefits. From a Spring Framework perspective, it makes sense to have an out-of-the-box GraphQL offering, just like Spring supports REST." Translation: If you're a Spring Boot shop already using DGS, go ahead and keep using it for now. If you're a Spring Boot shop starting afresh, you should probably just use Spring for GraphQL. In this guide, I've explained GraphQL in detail, setting the stage by providing some background on the relevant libraries and frameworks in Java. Now, let me show you how to implement state-of-the-art GraphQL in Java for a real application. Since we're starting afresh, we'll take the advice from DGS and just use Spring for GraphQL. How Exactly Do I Build a GraphQL API Server in Java for a Real Application? Opinions are free to differ on what it even means to be a "real application." For the purpose of this guide, what I mean by "real application" in this setting is an application that has at least these features: Persistence: Many tutorials, getting-started guides, and overviews only address in-memory data models, stopping well short of interacting with a database. This guide shows you some ways to cross this crucial chasm and discusses some of the consequences, challenges, and trade-offs involved. This is a vast topic so I barely scratch the surface, but it's a start. The primary goal is to support Query operations. A stretch goal is to support Mutation operations. Subscription operations are thoroughly off-the-table for now. Flexibility: I wrote above that just how expressive, efficient, discoverable, and simple we make our GraphQL API is technically a choice we make, but is practically a property that emerges from other choices we make. I also wrote that building GraphQL API servers is difficult for data producers. Consequently, many data producers cope with that difficulty by dialing way back on those other properties of the API. Many GraphQL API servers in the real world are inflexible, superficial, shallow, and are, in many ways, "GraphQL-in-name-only." This guide shows some of what's involved in going beyond the status quo and how that comes into tension with other properties, like efficiency. Spoiler Alert: It isn't pretty. Efficiency: In fairness, many GraphQL API servers in the real world achieve decent efficiency, albeit at the expense of flexibility, by essentially encoding REST API endpoints into a shallow GraphQL schema. The standard approach in GraphQL is the data-loader pattern, but few tutorials really show how this is used even with an in-memory data model let alone with a database. This guide offers one implementation of the data loader pattern to combat the N+1 problem. Again, we see how that comes into tension with flexibility and simplicity. Modernity: Anyone writing a Java application that accesses a database will have to make choices about how to access a database. That could involve just JDBC and raw SQL (for a relational database) but arguably the current industry standard is still to use an Object-Relational Mapping (ORM) layer like Hibernate, jooq, or the standard JPA. Getting an ORM to play nice with GraphQL is a tall order, may not be prudent, and may not even be possible. Few if any other guides touch this with a ten-foot pole. This guide at least will make an attempt with an ORM in the future! The recipe I follow in this guide for building a GraphQL API server in Java for a relational database is the following: Choose Spring Boot for the overall server framework. Choose Spring for GraphQL for the GraphQL-specific parts. Choose Spring Data for JDBC for data access in lieu of an ORM for now. Choose Maven over Gradle because I prefer the former. If you choose the latter, you're on your own. Choose PostgreSQL for the database. Most of the principles should apply to pretty much any relational database, but you've got to start somewhere. Choose Docker Compose for orchestrating a development database server. There are other ways of bringing in a database, but again, you've got to start somewhere. Choose the Chinook data model. Naturally, you will have your own data model, but Chinook is a good choice for illustration purposes because it's fairly rich, has quite a few tables and relationships, goes well beyond the ubiquitous but trivial To-Do apps, is available for a wide variety of databases, and is generally well-understood. Choose the Spring Initializr for bootstrapping the application. There's so much ceremony in Java, any way to race through some of it is welcomed. Create a GraphQL schema file. This is a necessary step for graphql-java, for DGS, and for Spring for GraphQL. Weirdly, the Spring for GraphQL overview seems to overlook this step, but the DGS "Getting Started" guide is there to remind us. Many "thought leaders" will exhort you to isolate your underlying data model from your API. Theoretically, you could do this by having different GraphQL types from your database tables. Practically, this is a source of busy work. Write Java model classes, one for every GraphQL type in the schema file and every table in the database. You're free to make other choices for this data model or for any other data model, and you can even write code or SQL views to isolate your underlying data model from your API but do ask how important this really is when the number of tables/classes/types grows to the hundreds or thousands. Write Java controller classes, with one method at least for every root field. In practice, this is the bare minimum. There probably will be many more. By the way, these methods are your "resolvers". Annotate every controller class with @Controller to tell Spring to inject it as a Java Bean that can serve network traffic. Annotate every resolver/data-fetcher method with @SchemaMapping or QueryMapping to tell Spring for GraphQL how to execute the parts of a GraphQL operation. Implement those resolver/data-fetcher methods by whatever means necessary to mediate interactions with the database. In version 0, this will be just simple raw SQL statements. Upgrade some of those resolver/data-fetcher methods by replacing @SchemaMapping or @QueryMapping with @BatchMapping. This latter annotation signals to Spring for GraphQL that we want to make the execution more efficient by combating the N+1 problem, and we're prepared to pay the price in more code in order to do it. Refactor those @BatchMapping annotated methods to support the data loader pattern by accepting (and processing) a list of identifiers for related entities rather than a single identifier for a single related entity. Write copious test-cases for every possible interaction. Just use a fuzz-tester on the API and call it a day. But Really, How Exactly Do I Build a GraphQL API Server in Java for a Real Application? That is a long recipe above! Instead of going into chapter and verse for every single step, in this guide, I do two things. First, I provide a public repository (Steps 1-5) with working code that is easy to use, easy to run, easy to read, and easy to understand. Second, I highlight some of the important steps, put them in context, discuss the choices involved, and offer some alternatives. Step 6: Choose Docker Compose for Orchestrating a Development Database Server Again, there are other ways to pull this off, but this is one good way. YAML version: "3.6" services: postgres: image: postgres:16 ports: - ${PGPORT:-5432}:5432 restart: always environment: POSTGRES_PASSWORD: postgres PGDATA: /var/lib/pgdata volumes: - ./initdb.d-postgres:/docker-entrypoint-initdb.d:ro - type: tmpfs target: /var/lib/pg/data Set an environment variable for PGPORT to expose PostgreSQL on a host port, or hard-code it to whatever value you like. Step 7: Choose the Chinook Data Model The Chinook files from YugaByte work out-of-the-box for PostgreSQL and are a good choice. Just make sure that there is a sub-directory initdb.d-postgres and download the Chinook DDL and DML files into that directory, taking care to give them numeric prefixes so that they're run by the PostgreSQL initialization script in the proper order. Shell mkdir -p ./initdb.d-postgres wget -O ./initdb.d-postgres/04_chinook_ddl.sql https://raw.githubusercontent.com/YugaByte/yugabyte-db/master/sample/chinook_ddl.sql wget -O ./initdb.d-postgres/05_chinook_genres_artists_albums.sql https://raw.githubusercontent.com/YugaByte/yugabyte-db/master/sample/chinook_genres_artists_albums.sql wget -O ./initdb.d-postgres/06_chinook_songs.sql https://raw.githubusercontent.com/YugaByte/yugabyte-db/master/sample/chinook_songs.sql Now, you can start the database service using Docker Compose. docker compose up -d Or docker-compose up -d There are many ways to spot-check the database's validity. If the Docker Compose service seems to have started correctly, here's one way using psql. psql "postgresql://postgres:postgres@localhost:5432/postgres" -c '\d' SQL List of relations Schema | Name | Type | Owner --------+-----------------+-------+---------- public | Album | table | postgres public | Artist | table | postgres public | Customer | table | postgres public | Employee | table | postgres public | Genre | table | postgres public | Invoice | table | postgres public | InvoiceLine | table | postgres public | MediaType | table | postgres public | Playlist | table | postgres public | PlaylistTrack | table | postgres public | Track | table | postgres public | account | table | postgres public | account_summary | view | postgres public | order | table | postgres public | order_detail | table | postgres public | product | table | postgres public | region | table | postgres (17 rows) You should at least see Chinook-specific tables like Album, Artist, and Track. Step 8: Choose the Spring Initializr for Bootstrapping the Application The important thing with this form is to make these choices: Project: Maven Language: Java Spring Boot: 3.2.5 Packaging: Jar Java: 21 Dependencies: Spring for GraphQL PostgreSQL Driver You can make other choices (e.g., Gradle, Java 22, MySQL, etc.), but bear in mind that this guide has only been tested with the choices above. Step 9: Create a GraphQL Schema File Maven projects have a standard directory layout and a standard place within that layout for resource files to be packaged into the build artifact (a JAR file) is ./src/main/java/resources. Within that directory, create a sub-directory graphql and deposit a schema.graphqls file. There are other ways to organize the GraphQL schema files needed by graphql-java, DGS, and Spring for GraphQL, but they all are rooted in ./src/main/java/resources (for a Maven project). Within the schema.graphqls file (or its equivalent), first there will be a definition for the root Query object, with root-level fields for every GraphQL type that we want in our API. As a starting point, there will be a root-level field under Query for every table, and a corresponding type for every table. For example, for Query: Java type Query { Artist(limit: Int): [Artist] ArtistById(id: Int): Artist Album(limit: Int): [Album] AlbumById(id: Int): Album Track(limit: Int): [Track] TrackById(id: Int): Track Playlist(limit: Int): [Playlist] PlaylistById(id: Int): Playlist PlaylistTrack(limit: Int): [PlaylistTrack] PlaylistTrackById(id: Int): PlaylistTrack Genre(limit: Int): [Genre] GenreById(id: Int): Genre MediaType(limit: Int): [MediaType] MediaTypeById(id: Int): MediaType Customer(limit: Int): [Customer] CustoemrById(id: Int): Customer Employee(limit: Int): [Employee] EmployeeById(id: Int): Employee Invoice(limit: Int): [Invoice] InvoiceById(id: Int): Invoice InvoiceLine(limit: Int): [InvoiceLine] InvoiceLineById(id: Int): InvoiceLine } Note the parameters on these fields. I have written it so that every root-level field that has a List return type accepts one optional limit parameter which accepts an Int. The intention is to support limiting the number of entries that should be returned from a root-level field. Note also that every root-level field that has a Scalar object return type accepts one optional id parameter which also accepts an Int. The intention is to support fetching a single entry by its identifier (which happens to all be integer primary keys in the Chinook data model). Next, here is an illustration of some of the corresponding GraphQL types: Java type Album { AlbumId : Int Title : String ArtistId : Int Artist : Artist Tracks : [Track] } type Artist { ArtistId: Int Name: String Albums: [Album] } type Customer { CustomerId : Int FirstName : String LastName : String Company : String Address : String City : String State : String Country : String PostalCode : String Phone : String Fax : String Email : String SupportRepId : Int SupportRep : Employee Invoices : [Invoice] } Fill out the rest of the schema.graphqls file as you see fit, exposing whatever table (and possibly views, if you create them) you like. Or, just use the complete version from the shared repository. Step 10: Write Java Model Classes Within the standard Maven directory layout, Java source code goes into ./src/main/java and its sub-directories. Within an appropriate sub-directory for whatever Java package you use, create Java model classes. These can be Plain Old Java Objects (POJOs). They can be Java Record classes. They can be whatever you like, so long as they have "getter" and "setter" property methods for the corresponding fields in the GraphQL schema. In this guide's repository, I choose Java Record classes just for the minimal amount of boilerplate. Java package com.graphqljava.tutorial.retail.models; public class ChinookModels { public static record Album ( Integer AlbumId, String Title, Integer ArtistId ) {} public static record Artist ( Integer ArtistId, String Name ) {} public static record Customer ( Integer CustomerId, String FirstName, String LastName, String Company, String Address, String City, String State, String Country, String PostalCode, String Phone, String Fax, String Email, Integer SupportRepId ) {} ... } Steps 11-14: Write Java Controller Classes, Annotate Every Controller, Annotate Every Resolver/Data-Fetcher, and Implement Those Resolver/Data-Fetcher These are the Spring @Controller classes, and within them are the Spring for GraphQL QueryMapping and @SchemaMapping resolver/data-fetcher methods. These are the real workhorses of the application, accepting input parameters, mediating interaction with the database, validating data, implementing (or delegating) to business logic code segments, arranging for SQL and DML statements to be sent to the database, returning the data, processing the data, and sending it along to the GraphQL libraries (graphql-java, DGS, Spring for GraphQL) to package up and send off to the client. There are so many choices one can make in implementing these and I can't go into every detail. Let me just illustrate how I have done it, highlight some things to look out for, and discuss some of the options that are available. For reference, we will look at a section of the ChinookControllers file from the example repository. Java package com.graphqljava.tutorial.retail.controllers; // It's got to go into a package somewhere. import java.sql.ResultSet; // There's loads of symbols to import. import java.sql.SQLException; // This is Java and there's no getting around that. import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.graphql.data.ArgumentValue; import org.springframework.graphql.data.method.annotation.BatchMapping; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.graphql.data.method.annotation.SchemaMapping; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.jdbc.core.simple.JdbcClient.StatementSpec; import org.springframework.stereotype.Controller; import com.graphqljava.tutorial.retail.models.ChinookModels.Album; import com.graphqljava.tutorial.retail.models.ChinookModels.Artist; import com.graphqljava.tutorial.retail.models.ChinookModels.Customer; import com.graphqljava.tutorial.retail.models.ChinookModels.Employee; import com.graphqljava.tutorial.retail.models.ChinookModels.Genre; import com.graphqljava.tutorial.retail.models.ChinookModels.Invoice; import com.graphqljava.tutorial.retail.models.ChinookModels.InvoiceLine; import com.graphqljava.tutorial.retail.models.ChinookModels.MediaType; import com.graphqljava.tutorial.retail.models.ChinookModels.Playlist; import com.graphqljava.tutorial.retail.models.ChinookModels.PlaylistTrack; import com.graphqljava.tutorial.retail.models.ChinookModels.Track; public class ChinookControllers { // You don't have to nest all your controllers in one file. It's just what I do. @Controller public static class ArtistController { // Tell Spring about this controller class. @Autowired JdbcClient jdbcClient; // Lots of ways to get DB access from the container. This is one way in Spring Data. RowMapper<Artist> // I'm not using an ORM, and only a tiny bit of help from Spring Data. mapper = new RowMapper<>() { // Consequently, there are these RowMapper utility classes involved. public Artist mapRow (ResultSet rs, int rowNum) throws SQLException { return new Artist(rs.getInt("ArtistId"), rs.getString("Name"));}; @SchemaMapping Artist Artist (Album album) { // @QueryMapping when we can, @SchemaMapping when we have to return // Here, we're getting an Artist for a given Album. jdbcClient .sql("select * from \"Artist\" where \"ArtistId\" = ? limit 1") // Simple PreparedStatement wrapper .param(album.ArtistId()) // Fish out the relating field ArtistId and pass it into the PreparedStatement .query(mapper) // Use our RowMapper to turn the JDBC Row into the desired model class object. .optional() // Use optional to guard against null returns! .orElse(null);} @QueryMapping(name = "ArtistById") Artist // Another resolver, this time to get an Artist by its primary key identifier artistById (ArgumentValue<Integer> id) { // Note the annotation "name" parameter, when the GraphQL field name doesn't match exactly the method name for (Artist a : jdbcClient.sql("select * from \"Artist\" where \"ArtistId\" = ?").param(id.value()).query(mapper).list()) return a; return null;} @QueryMapping(name = "Artist") List<Artist> // Yet another resolver, this time to get a List of Artists. artist (ArgumentValue<Integer> limit) { // Note the one "limit" parameter. ArgumentValue<T> is the way you do this with GraphQL for Java. StatementSpec spec = limit.isOmitted() ? // Switch SQL on whether we did or did not get the limit parameter. jdbcClient.sql("select * from \"Artist\"") : jdbcClient.sql("select * from \"Artist\" limit ?").param(limit.value()); return // Run the SQL, map the results, return the List. spec .query(mapper) .list();} ... There's a lot to unpack here, so let's go through it step by step. First, I included the package and import statements in the example because all too often, tutorials and guides that you find online elide these details for brevity. The problem with that, however, is that it's not compilable or runnable code. You don't know where these symbols are coming from, what packages they're in, and what libraries they're coming from. Any decent editor like IntelliJ, VSCode, or even Emacs will help sort this out for you when you're writing code, but you don't have that when reading a blog article. Moreover, there can be name conflicts and ambiguities among symbols across libraries, so even with a smart editor it can leave the reader scratching their head. Next, please forgive the nested inner classes. Feel free to explode your classes into their own individual files as you see fit. This is just how I do it, largely for pedagogical purposes like this one, to promote Locality of Behavior, which is just a fancy way of saying, "Let's not make the reader jump through a lot of hoops to understand the code." Now for the meat of the code. Aside from niggling details like "How do I get a database connection?", "How do I map data?", etc., the patterns I want you to see through the forest of code are these: Every field in our schema file (schema.graphqls) which isn't a simple scalar field (e.g., Int, String, Boolean) will probably need a resolver/data-fetcher. Every resolver is implemented with a Java method. Every resolver method gets annotated with @SchemaMapping, @QueryMapping, or @BatchMapping. Use @QueryMapping when you can because it's simpler. Use @SchemaMapping when you have to (your IDE should nag you). If you keep the Java method names in sync with the GraphQL field names, it's a little less code, but don't make a federal case out of it. You can fix it with a name parameter in the annotations. Unless you do something different (such as adding filtering, sorting, and pagination), you probably will be fetching either a single entry by its primary key or a list of entries. You won't be fetching "child" entries; that's handled by the GraphQL libraries and the recursive divide-and-conquer way they process GraphQL operations. Note: This has implications for performance, efficiency, and code complexity. The "something different" in the above item refers to the richness that you want to add to your GraphQL API. Want limit operations? Filter predicates? Aggregations? Supporting those cases will involve more ArgumentValue<> parameters, more SchemaMapping resolver methods, and more combinations thereof. Deal with it. You will experience the urge to be clever, to create abstractions that dynamically respond to more and more complex combinations of parameters, filters, and other conditions. Step 15: Upgrade Some of Those Resolver/Data-Fetcher Methods With the Data Loader Pattern You will quickly realize that this can lead to overly chatty interaction with the database, sending too many small SQL statements and impacting performance and availability. This is the proverbial "N+1" problem. In a nutshell, the N+1 problem can be illustrated by our Chinook data model. Suppose we have this GraphQL query. query { Artist(limit: 10) { ArtistId Album { AlbumId Track { TrackId } } } } Get up to 10 Artist entry. For each Artist, get all of the related Album entries. For each Album, get all of the related Track entries. For each entry, just get its identifier field: ArtistId, AlbumId, TrackId. This query is nested 2 levels below Artist. Let n=2. Albumis a List wrapping type on Artist, as is Track is a List wrapping type on Album. Suppose the typical cardinality is m. How many SQL statements will typically be involved 1 to fetch 10 Artist entries. 10*m to fetch the Album entries. 10*m^m to fetch the Track entries. In general, we can see that the number of queries scales as m^n, which is exponential in n. Of course, observe that the amount of data retrieved also scales as m^n. In any case, on its face, this seems like an alarmingly inefficient way to go about fetching this data. There is another way, and it is the standard answer within the GraphQL community for combating this N+1 problem: the data loader pattern (aka "batching"). This encompasses three ideas: Rather than fetch the related child entities (e.g., Album) for a single parent entity (e.g., Artist) using one identifier, fetch the related entities for all of the parent entities in one go, using a list of identifiers. Group the resulting child entities according to their respective parent entities (in code). While we're at it, we might as well cache the entities for the lifetime of executing the one GraphQL operation, in case a given entity appears in more than one place in the graph. Now, for some code. Here's how this looks in our example. Java @BatchMapping(field = "Albums") public Map<Artist, List<Album>> // Switch to @BatchMapping albumsForArtist (List<Artist> artists) { // Take in a List of parents rather than a single parent return jdbcClient .sql("select * from \"Album\" where \"ArtistId\" in (:ids)") // Use a SQL "in" predicate taking a list of identifiers .param("ids", artists.stream().map(x -> x.ArtistId()).toList()) // Fish the list of identifiers out of the list of parent objects .query(mapper) // Can re-use our usual mapper .list() .stream().collect(Collectors.groupingBy(x -> artists.stream().collect(Collectors.groupingBy(Artist::ArtistId)).get(x.ArtistId()).getFirst())); // ^ Java idiom for grouping child Albums according to their parent Albums } Like before, let's unpack this. First, we switch from either the @QueryMapping or @SchemaMapping annotation to @BatchMapping to signal to Spring for GraphQL that we want to use the data loader pattern. Second, we switch from a single Artist parameter to a List<Artist> parameter. Third, we somehow have to arrange the necessary SQL (with an in predicate in this case) and the corresponding parameter (a List<Integer> extracted from the List<Album> parameter). Fourth, we somehow have to arrange for the child entries (Album in this case) to get sorted to the right parent entries (Album in this case). There are many ways to do it, and this is just one way. The important point is that however it's done, it has to be done in Java. One last thing: note the absence of the limit parameter. Where did that go? It turns out that InputValue<T> is not supported by Spring for GraphQL for @BatchMapping. Oh well! In this case, it's no great loss because arguably these limit parameters make little sense. How often does one really need a random subset of an artist's albums? It would be a more serious issue if we had filtering and sorting, however. Filtering and sorting parameters are more justified, and if we had them we would somehow have to find a way to sneak them into the data loader pattern. Presumably, it can be done, but it will not be so easy as just slapping a @BatchMapping annotation onto the method and tinkering with Java streams. This raises an important point about the "N+1 problem" that is never addressed, and that neglect just serves to exaggerate the scale of the problem in a real-world setting. If we have limits and/or filtering, then we have a way of reducing the cardinality of related child entities below m (recall that we took m to be the typical cardinality of a child entity). In the real world, setting limits or, more precisely filtering are necessary for usability. GraphQL APIs are meant for humans, in that at the end of the day, the data are being painted onto a screen or in some other way presented to a human user who then has to absorb and process those data. Humans have severe limits in perception, cognition, and memory, for the quantity of data we can process. Only another machine (i.e., computers) could possibly process a large volume of data, but if you're extracting large volumes of data from one machine to another, then you are building an ETL pipeline. If you are using GraphQL for ETL, then you are doing it wrong and should stop immediately! In any event, in a real-world setting, with human users, both m and n will be very small. The number of SQL queries will not scale as m^n to very large numbers. Effectively, the N+1 problem will inflate the number of SQL queries not by an arbitrarily large factor, but by approximately a constant factor. In a well-designed application, it probably will be a constant factor well below 100. Consider this when balancing the trade-offs in developer time, complexity, and hardware scaling when confronting the N+1 problem. Is This the Only Way To Build a GraphQL API Server? We saw that the "easy way" of building GraphQL servers is the one typically offered in tutorials and "Getting Started" guides, and is over tiny unrealistic in-memory data models, without a database. We saw that the "real way" of building GraphQL servers (in Java) described in some detail above, regardless of library or framework, involves: Writing schema file entries, possibly for every table Writing Java model classes, possibly for every table Writing Java resolver methods, possibly for every field in every table Eventually writing code to solve arbitrarily complex compositions of input parameters Writing code to budget SQL operations efficiently We also observe that GraphQL lends itself to a "recursive divide-and-conquer with an accumulator approach": a GraphQL query is recursively divided and sub-divided along type and field boundaries into a "graph," internal nodes in the graph are processed individually by resolvers, but the data are passed up the graph dataflow style, accumulating into a JSON envelope that is returned to the user. The GraphQL libraries decompose the incoming queries into something like an Abstract Syntax Tree (AST), firing SQL statements for all the internal nodes (ignoring the data loader pattern for a moment), and then re-composing the data. And, we are its willing accomplices! We also observe that building GraphQL servers according to the above recipes leads to other outcomes: Lots of repetition Lots of boilerplate code Bespoke servers Tied to a particular data model Build a GraphQL server more than once according to the above recipes and you will make these observations and will naturally feel a powerful urge to build more sophisticated abstractions that reduce the repetition, reduce the boilerplate, generalize the servers, and decouple them from any particular data model. This is what I call the "natural way" of building a GraphQL API, as it's a natural evolution from the trivial "easy way" of tutorials and "Getting Started" guides, and from the cumbersome "real way" of resolvers and even data loaders. Building a GraphQL server with a network of nested resolvers offers some flexibility and dynamism, and requires a lot of code. Adding in more flexibility and dynamism with limits, pagination, filtering, and sorting, requires more code still. And while it may be dynamic, it will also be very chatty with the database, as we saw. Reducing the chattiness necessitates composing the many fragmentary SQL statements into fewer SQL statements which individually do more work. That's what the data loader pattern does: it reduces the number of SQL statements from "a few tens" to "less than 10 but more than 1." In practice, that may not be a huge win and it comes at the cost of developer time and lost dynamism, but it is a step down the path of generating fewer, more sophisticated queries. The terminus of that path is "1": the optimal number of SQL statements (ignoring caching) is 1. Generate one giant SQL statement that does all the work of fetching the data, teach it to generate JSON while you're at it, and this is the best you will ever do with a GraphQL server (for a relational database). It will be hard work, but you can take solace in having done it once, it need not ever be done again if you do it right, by introspecting the database to generate the schema. Do that, and what you will build won't be so much a "GraphQL API server" as a "GraphQL to SQL compiler." Acknowledge that building a GraphQL to SQL compiler is what you have been doing all along, embrace that fact, and lean into it. You may never need to build another GraphQL server again. What could be better than that? One thing that could be better than building your last GraphQL server, or your only GraphQL server, is never building a GraphQL server in the first place. After all, your goal wasn't to build a GraphQL API but rather to have a GraphQL API. The easiest way to have a GraphQL API is just to go get one. Get one for free if you can. Buy one if the needs justify it. This is the final boss on the journey of GraphQL maturity. How To Choose "Build" Over "Buy" Of course, "buy" in this case is really just a stand-in for the general concept, which is to "acquire" an existing solution rather than building one. That doesn't necessarily require purchasing software, since it could be free and open-source. The distinction that I want to draw here is over whether or not to build a custom solution. When it's possible to acquire an existing solution (whether commercial or open-source), there are several options: Apollo Hasura PostGraphile Prisma If you do choose to build GraphQL servers with Java, I hope you will find this article helpful in breaking out of the relentless tutorials, "Getting Started" guides, and "To-Do" apps. These are vast topics in a shifting landscape that require an iterative approach and a modest amount of repetition.
Do you need to write a lot of mapping code in order to map between different object models? MapStruct simplifies this task by generating mapping code. In this blog, you will learn some basic features of MapStruct. Enjoy! Introduction In a multi-layered application, one often has to write boilerplate code in order to map different object models. This can be a tedious and an error-prone task. MapStruct simplifies this task by generating the mapping code for you. It generates code during compile time and aims to generate the code as if it was written by you. This blog will only give you a basic overview of how MapStruct can aid you, but it will be sufficient to give you a good impression of which problem it can solve for you. If you are using IntelliJ as an IDE, you can also install the MapStruct Support Plugin which will assist you in using MapStruct. Sources used in this blog can be found on GitHub. Prerequisites Prerequisites for this blog are: Basic Java knowledge, Java 21 is used in this blog Basic Spring Boot knowledge Basic Application The application used in this blog is a basic Spring Boot project. By means of a Rest API, a customer can be created and retrieved. In order to keep the API specification and source code in line with each other, you will use the openapi-generator-maven-plugin. First, you write the OpenAPI specification and the plugin will generate the source code for you based on the specification. The OpenAPI specification consists out of two endpoints, one for creating a customer (POST) and one for retrieving the customer (GET). The customer consists of its name and some address data. YAML Customer: type: object properties: firstName: type: string description: First name of the customer minLength: 1 maxLength: 20 lastName: type: string description: Last name of the customer minLength: 1 maxLength: 20 street: type: string description: Street of the customer minLength: 1 maxLength: 20 number: type: string description: House number of the customer minLength: 1 maxLength: 5 postalCode: type: string description: Postal code of the customer minLength: 1 maxLength: 5 city: type: string description: City of the customer minLength: 1 maxLength: 20 The CustomerController implements the generated Controller interface. The OpenAPI maven plugin makes use of its own model. In order to transfer the data to the CustomerService, DTOs are created. These are Java records. The CustomerDto is: Java public record CustomerDto(Long id, String firstName, String lastName, AddressDto address) { } The AddressDto is: Java public record AddressDto(String street, String houseNumber, String zipcode, String city) { } The domain itself is used within the Service and is a basic Java POJO. The Customer domain is: Java public class Customer { private Long customerId; private String firstName; private String lastName; private Address address; // Getters and setters left out for brevity } The Address domain is: Java public class Address { private String street; private int houseNumber; private String zipcode; private String city; // Getters and setters left out for brevity } In order to connect everything together, you will need to write mapper code for: Mapping between the API model and the DTO Mapping between the DTO and the domain Mapping Between DTO and Domain Add Dependency In order to make use of MapStruct, it suffices to add the MapStruct Maven dependency and to add some configuration to the Maven Compiler plugin. XML <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> ... <build> <plugins> ... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> ... </plugins> </build> Create Mapper The CustomerDto, AddressDto and the Customer, Address domains do not differ very much from each other. CustomerDto has an id while Customer has a customerId. AddressDto has a houseNumber of the type String while Address has a houseNumber of the type integer. In order to create a mapper for this using MapStruct, you create an interface CustomerMapper, annotate it with @Mapper, and specify the component model with the value spring. Doing this will ensure that the generated mapper is a singleton-scoped Spring bean that can be retrieved via @Autowired. Because both models are quite similar to each other, MapStruct will be able to generate most of the code by itself. Because the customer id has a different name in both models, you need to help MapStruct a bit. Using the @Mapping annotation, you specify the source and target mapping. For the type conversion, you do not need to do anything, MapStruct can sort this out based on the implicit type conversions. The corresponding mapper code is the following: Java @Mapper(componentModel = "spring") public interface CustomerMapper { @Mapping(source = "customerId", target = "id") CustomerDto transformToCustomerDto(Customer customer); @Mapping(source = "id", target = "customerId") Customer transformToCustomer(CustomerDto customerDto); } Generate the code: Shell $ mvn clean compile In the target/generated-sources/annotations directory, you can find the generated CustomerMapperImpl class. Java @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2024-04-21T13:38:51+0200", comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21 (Eclipse Adoptium)" ) @Component public class CustomerMapperImpl implements CustomerMapper { @Override public CustomerDto transformToCustomerDto(Customer customer) { if ( customer == null ) { return null; } Long id = null; String firstName = null; String lastName = null; AddressDto address = null; id = customer.getCustomerId(); firstName = customer.getFirstName(); lastName = customer.getLastName(); address = addressToAddressDto( customer.getAddress() ); CustomerDto customerDto = new CustomerDto( id, firstName, lastName, address ); return customerDto; } @Override public Customer transformToCustomer(CustomerDto customerDto) { if ( customerDto == null ) { return null; } Customer customer = new Customer(); customer.setCustomerId( customerDto.id() ); customer.setFirstName( customerDto.firstName() ); customer.setLastName( customerDto.lastName() ); customer.setAddress( addressDtoToAddress( customerDto.address() ) ); return customer; } protected AddressDto addressToAddressDto(Address address) { if ( address == null ) { return null; } String street = null; String houseNumber = null; String zipcode = null; String city = null; street = address.getStreet(); houseNumber = String.valueOf( address.getHouseNumber() ); zipcode = address.getZipcode(); city = address.getCity(); AddressDto addressDto = new AddressDto( street, houseNumber, zipcode, city ); return addressDto; } protected Address addressDtoToAddress(AddressDto addressDto) { if ( addressDto == null ) { return null; } Address address = new Address(); address.setStreet( addressDto.street() ); if ( addressDto.houseNumber() != null ) { address.setHouseNumber( Integer.parseInt( addressDto.houseNumber() ) ); } address.setZipcode( addressDto.zipcode() ); address.setCity( addressDto.city() ); return address; } } As you can see, the code is very readable and it has taken into account the mapping of Customer and Address. Create Service The Service will create a domain Customer taken the CustomerDto as an input. The customerMapper is injected into the Service and is used for converting between the two models. The other way around, when a customer is retrieved, the mapper converts the domain Customer to a CustomerDto. In the Service, the customers are persisted in a basic list in order to keep things simple. Java @Service public class CustomerService { private final CustomerMapper customerMapper; private final HashMap<Long, Customer> customers = new HashMap<>(); private Long index = 0L; CustomerService(CustomerMapper customerMapper) { this.customerMapper = customerMapper; } public CustomerDto createCustomer(CustomerDto customerDto) { Customer customer = customerMapper.transformToCustomer(customerDto); customer.setCustomerId(index); customers.put(index, customer); index++; return customerMapper.transformToCustomerDto(customer); } public CustomerDto getCustomer(Long customerId) { if (customers.containsKey(customerId)) { return customerMapper.transformToCustomerDto(customers.get(customerId)); } else { return null; } } } Test Mapper The mapper can be easily tested by using the generated CustomerMapperImpl class and verify whether the mappings are executed successfully. Java class CustomerMapperTest { @Test void givenCustomer_whenMaps_thenCustomerDto() { CustomerMapperImpl customerMapper = new CustomerMapperImpl(); Customer customer = new Customer(); customer.setCustomerId(2L); customer.setFirstName("John"); customer.setLastName("Doe"); Address address = new Address(); address.setStreet("street"); address.setHouseNumber(42); address.setZipcode("zipcode"); address.setCity("city"); customer.setAddress(address); CustomerDto customerDto = customerMapper.transformToCustomerDto(customer); assertThat( customerDto ).isNotNull(); assertThat(customerDto.id()).isEqualTo(customer.getCustomerId()); assertThat(customerDto.firstName()).isEqualTo(customer.getFirstName()); assertThat(customerDto.lastName()).isEqualTo(customer.getLastName()); AddressDto addressDto = customerDto.address(); assertThat(addressDto.street()).isEqualTo(address.getStreet()); assertThat(addressDto.houseNumber()).isEqualTo(String.valueOf(address.getHouseNumber())); assertThat(addressDto.zipcode()).isEqualTo(address.getZipcode()); assertThat(addressDto.city()).isEqualTo(address.getCity()); } @Test void givenCustomerDto_whenMaps_thenCustomer() { CustomerMapperImpl customerMapper = new CustomerMapperImpl(); AddressDto addressDto = new AddressDto("street", "42", "zipcode", "city"); CustomerDto customerDto = new CustomerDto(2L, "John", "Doe", addressDto); Customer customer = customerMapper.transformToCustomer(customerDto); assertThat( customer ).isNotNull(); assertThat(customer.getCustomerId()).isEqualTo(customerDto.id()); assertThat(customer.getFirstName()).isEqualTo(customerDto.firstName()); assertThat(customer.getLastName()).isEqualTo(customerDto.lastName()); Address address = customer.getAddress(); assertThat(address.getStreet()).isEqualTo(addressDto.street()); assertThat(address.getHouseNumber()).isEqualTo(Integer.valueOf(addressDto.houseNumber())); assertThat(address.getZipcode()).isEqualTo(addressDto.zipcode()); assertThat(address.getCity()).isEqualTo(addressDto.city()); } } Mapping Between API and DTO Create Mapper The API model looks a bit different than the CustomerDto because it has no Address object and number and postalCode have different names in the CustomerDto. Java public class Customer { private String firstName; private String lastName; private String street; private String number; private String postalCode; private String city; // Getters and setters left out for brevity } In order to create a mapper, you need to add a bit more @Mapping annotations, just like you did before for the customer ID. Java @Mapper(componentModel = "spring") public interface CustomerPortMapper { @Mapping(source = "street", target = "address.street") @Mapping(source = "number", target = "address.houseNumber") @Mapping(source = "postalCode", target = "address.zipcode") @Mapping(source = "city", target = "address.city") CustomerDto transformToCustomerDto(Customer customerApi); @Mapping(source = "id", target = "customerId") @Mapping(source = "address.street", target = "street") @Mapping(source = "address.houseNumber", target = "number") @Mapping(source = "address.zipcode", target = "postalCode") @Mapping(source = "address.city", target = "city") CustomerFullData transformToCustomerApi(CustomerDto customerDto); } Again, the generated CustomerPortMapperImpl class can be found in the target/generated-sources/annotations directory after invoking the Maven compile target. Create Controller The mapper is injected in the Controller and the corresponding mappers can easily be used. Java @RestController class CustomerController implements CustomerApi { private final CustomerPortMapper customerPortMapper; private final CustomerService customerService; CustomerController(CustomerPortMapper customerPortMapper, CustomerService customerService) { this.customerPortMapper = customerPortMapper; this.customerService = customerService; } @Override public ResponseEntity<CustomerFullData> createCustomer(Customer customerApi) { CustomerDto customerDtoIn = customerPortMapper.transformToCustomerDto(customerApi); CustomerDto customerDtoOut = customerService.createCustomer(customerDtoIn); return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut)); } @Override public ResponseEntity<CustomerFullData> getCustomer(Long customerId) { CustomerDto customerDtoOut = customerService.getCustomer(customerId); return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut)); } } Test Mapper A unit test is created in a similar way as the one for the Service and can be viewed here. In order to test the complete application, an integration test is created for creating a customer. Java @SpringBootTest @AutoConfigureMockMvc class CustomerControllerIT { @Autowired private MockMvc mockMvc; @Test void whenCreateCustomer_thenReturnOk() throws Exception { String body = """ { "firstName": "John", "lastName": "Doe", "street": "street", "number": "42", "postalCode": "1234", "city": "city" } """; mockMvc.perform(post("/customer") .contentType("application/json") .content(body)) .andExpect(status().isOk()) .andExpect(jsonPath("firstName", equalTo("John"))) .andExpect(jsonPath("lastName", equalTo("Doe"))) .andExpect(jsonPath("customerId", equalTo(0))) .andExpect(jsonPath("street", equalTo("street"))) .andExpect(jsonPath("number", equalTo("42"))) .andExpect(jsonPath("postalCode", equalTo("1234"))) .andExpect(jsonPath("city", equalTo("city"))); } } Conclusion MapStruct is an easy-to-use library for mapping between models. If the basic mapping is not sufficient, you are even able to create your own custom mapping logic (which is not demonstrated in this blog). It is advised to read the official documentation to get a comprehensive list of all available features.