Getting Started

So You Want An API?

The easiest way to get started with Elide is to use the Spring Boot starter dependency. The starter bundles all of the dependencies you will need to stand up a web service. This tutorial uses the starter, and all of the code is available here.

You can deploy and play with this example on Heroku or locally. The landing page will let you toggle between the Swagger UI and Graphiql for the example service.

Deploy

Don’t like Spring/Spring Boot? - check out the same getting starting guide using Jetty/Jersey and Elide standalone. Don’t like Java? Here is an example project using Elide with Kotlin.

Contents

  1. So You Want An API?
  2. Contents
  3. Add Elide as a Dependency
  4. Create Models
  5. Spin up the API
    1. Classes
    2. Supporting Files
    3. Running
  6. Looking at more data
  7. Writing Data
    1. Inserting Data
    2. Modifying Data
  8. Running Analytic Queries

Add Elide as a Dependency

To include elide into your spring project, add the single starter dependency:

<dependency>
  <groupId>com.yahoo.elide</groupId>
  <artifactId>elide-spring-boot-starter</artifactId>
  <version>${elide.version}</version>
</dependency>

Create Models

Elide models are some of the most important code in any Elide project. Your models are the view of your data that you wish to expose. In this example we will be modeling a software artifact repository since most developers have a high-level familiarity with artifact repositories such as Maven, Artifactory, npm, and the like.

There will two kinds of models:

  • Models that we intend to both read & write. These models are created by definining Java classes. For this example, that includes ArtifactGroup, ArtifactProduct, and ArtifactVersion. For brevity we will omit package names and import statements.
  • Read-only models that we intend to run analytic queries against. These models can be created with Java classes or with a HJSON configuration language. For this example, we will use the latter to create a Downloads model.
@Include(rootLevel = true, name = "group")
@Entity
public class ArtifactGroup {
    @Id
    private String name = "";

    private String commonName = "";

    private String description = "";

    @OneToMany(mappedBy = "group")
    private List<ArtifactProduct> products = new ArrayList<>();
}
@Include(name = "product")
@Entity
public class ArtifactProduct {
    @Id
    private String name = "";

    private String commonName = "";

    private String description = "";

    @ManyToOne
    private ArtifactGroup group = null;

    @OneToMany(mappedBy = "artifact")
    private List<ArtifactVersion> versions = new ArrayList<>();
}
@Include(name = "version")
@Entity
public class ArtifactVersion {
    @Id
    private String name = "";

    private Date createdAt = new Date();

    @ManyToOne
    private ArtifactProduct artifact;
}
{
  tables: [
    {
      name: Downloads
      table: downloads
      description:
      '''
      Analytics for artifact downloads.
      '''
      joins: [
        {
          name: artifactGroup
          to: group
          kind: toOne
          type: left
          definition: '{{group_id}} = {{artifactGroup.name}}'
        },
        {
          name: artifactProduct
          to: product
          kind: toOne
          definition: '{{product_id}} = {{artifactProduct.name}}'
        }
      ]
      dimensions: [
        {
          name: group
          type: TEXT
          definition: '{{artifactGroup.name}}'
        }
        {
          name: product
          type: TEXT
          definition: '{{artifactProduct.name}}'
        }
        {
          name: date
          type: TIME
          definition: '{{date}}'
          grains: [{
            type: DAY
          }]
        }
      ]
      measures: [
        {
          name: downloads
          type: INTEGER
          definition: 'SUM({{downloads}})'
        }
      ]
    }
  ]
} 

Spin up the API

So now we have some models, but without an API it is not very useful. Before we add the API component, we need to create the schema in the database that our models will use. Our example uses liquibase to manage the schema. When Heroku releases the application, our example will execute the database migrations to configure the database with some test data automatically. This demo uses Postgres. Feel free to modify the migration script if you are using a different database provider.

You may notice the example liquibase migration script adds an extra table, AsyncQuery. This is only required if leveraging Elide’s asynchronous API to manage long running analytic queries.

There may be more tables in your database than models in your project or vice versa. Similarly, there may be more columns in a table than in a particular model or vice versa. Not only will our models work just fine, but we expect that models will normally expose only a subset of the fields present in the database. Elide is an ideal tool for building micro-services - each service in your system can expose only the slice of the database that it requires.

Classes

Bringing life to our API is trivially easy. We need a single Application class:

/**
 * Example app using elide-spring.
 */
@SpringBootApplication
public class App {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }
}

Supporting Files

The application is configured with a Spring application yaml file (broken into sections below).

The Elide Spring starter uses a JPA data store (the thing that talks to the database). This can be configured like any other Spring data source and JPA provider. The one below uses an H2 in-memory database:

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        dialect: 'org.hibernate.dialect.H2Dialect'
        '[default_batch_fetch_size]': 100
        '[use_scrollable_resultset]': true
    hibernate:
      naming:
        physical-strategy: 'org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl'
      ddl-auto: 'validate'
  datasource:
    url: 'jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1'
    username: 'sa'
    password: ''
    driver-class-name: 'org.h2.Driver'

Elide has its own configuration to turn on APIs and setup their URL paths:

elide:
  json-api:
    path: /api/v1
    enabled: true
  graphql:
    path: /graphql/api/v1
    enabled: true
  api-docs:
    path: /doc
    enabled: true
    version: openapi_3_0

The following configuration enables Elide’s asynchronous API for analytic queries:

  async:
    enabled: true
    thread-pool-size: 7
    cleanup:
      enabled: true
      query-max-run-time: 65s
      query-retention-duration: 7d

To enable analytic queries, we have to turn on the the aggregation data store. This example also enables HJSON configuration for analytic models:

  aggregation-store:
    enabled: true
    default-dialect: h2
    dynamic-config:
      path: src/main/resources/analytics
      enabled: true

Running

With these new classes, you have two options for running your project. You can either run the App class using your favorite IDE, or you can run the service from the command line:

java -jar target/elide-spring-boot-1.0.jar

Our example requires the following environment variables to be set to work correctly with Heroku and Postgres.

  1. JDBC_DATABASE_URL
  2. JDBC_DATABASE_USERNAME
  3. JDBC_DATABASE_PASSWORD

If running inside a Heroku dyno, Heroku sets these variables for us. If you don’t set them, the example will use the H2 in memory database.

With the App class and application yaml file, we can now run our API.

You can now run the following curl commands to see some of the sample data that the liquibase migrations added for us. Don’t forget to replace localhost:8080 with your Heroku URL if running from Heroku!

curl http://localhost:8080/api/v1/group
curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \
    "http://localhost:8080/graphql/api/v1" \
    -d'{ 
           "query" : "{ group { edges { node { name commonName description } } } }"
       }'

Here are the respective responses:

  {
      "data": [
      {
          "attributes": {
          "commonName": "Example Repository",
          "description": "The code for this project"
          },
          "id": "com.example.repository",
          "relationships": {
          "products": {
              "data": [
              {
                  "id": "elide-demo",
                  "type": "product"
              }
              ]
          }
          },
          "type": "group"
      },
      {
          "attributes": {
          "commonName": "Elide",
          "description": "The magical library powering this project"
          },
          "id": "com.yahoo.elide",
          "relationships": {
          "products": {
              "data": [
              {
                  "id": "elide-core",
                  "type": "product"
              },
              {
                  "id": "elide-standalone",
                  "type": "product"
              },
              {
                  "id": "elide-datastore-hibernate5",
                  "type": "product"
              }
              ]
          }
          },
          "type": "group"
      }
      ]
  }
  {
      "data": {
          "group": {
              "edges": [
              {
                  "node": {
                  "commonName": "Example Repository",
                  "description": "The code for this project",
                  "name": "com.example.repository"
                  }
              },
              {
                  "node": {
                  "commonName": "Elide",
                  "description": "The magical library powering this project",
                  "name": "com.yahoo.elide"
                  }
              }
              ]
          }
      }
  }

Looking at more data

You can navigate through the entity relationship graph defined in the models and explore relationships:

List groups:                 group/
Show a group:                group/<group id>
List a group's products:     group/<group id>/products/
Show a product:              group/<group id>/products/<product id>
List a product's versions:   group/<group id>/products/<product id>/versions/
Show a version:              group/<group id>/products/<product id>/versions/<version id>

Writing Data

So far we have defined our views on the database and exposed those views over HTTP. This is great progress, but so far we have only read data from the database.

Inserting Data

Fortunately for us adding data is just as easy as reading data. For now let’s use cURL to put data in the database.

curl -X POST http://localhost:8080/api/v1/group/com.example.repository/products -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" -d '{"data": {"type": "product", "id": "elide-demo"}}'
curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" "http://localhost:8080/graphql/api/v1" -d'{ "query" : "mutation { group(ids: [\"com.example.repository\"]) { edges { node { products(op: UPSERT, data: {name: \"elide-demo\"}) { edges { node { name } } } } } } }" }'

When you run that cURL call you should see a bunch of json returned, that is our newly inserted object!

{   
  "data": [
    {   
      "attributes": {
        "commonName": "",
        "description": ""
      },
      "id": "elide-demo",
      "relationships": {
        "group": {
          "data": {
            "id": "com.example.repository",
            "type": "group"
          }
        },
        "versions": {
          "data": []
        }
      },
      "type": "product"
    }
  ]
}
  {
    "data": {
      "group": {
        "edges": [
          {
            "node": {
              "products": {
              "edges": [
                {
                  "node": {
                    "name": "elide-demo"
                  }
                }
              ]
            }
          }
        }
      ]
    }
  }
}

Modifying Data

Notice that, when we created it, we did not set any of the attributes of our new product record. Updating our data to help our users is just as easy as it is to add new data. Let’s update our model with the following cURL call.

  curl -X PATCH http://localhost:8080/api/v1/group/com.example.repository/products/elide-demo \
    -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" \
    -d '{
      "data": {
        "type": "product",
        "id": "elide-demo",
        "attributes": {
          "commonName": "demo application",
          "description": "An example implementation of an Elide web service that showcases many Elide features"
        }
      }
    }'
 curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \
    "http://localhost:8080/graphql/api/v1" \
    -d'{   
           "query" : "mutation { group(ids: [\"com.example.repository\"]) { edges { node { products(op: UPDATE, data: { name: \"elide-demo\", commonName: \"demo application\", description: \"An example implementation of an Elide web service that showcases many Elide features\" }) { edges { node { name } } } } } } }"
       }'

Running Analytic Queries

Analytic queries leverage the same API as reading any other Elide model. Note that Elide will aggregate the measures selected by the dimensions requested. Learn more about analytic queries here.

curl -g "http://localhost:8080/api/v1/downloads?fields[downloads]=downloads,group,product"
curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \
    "http://localhost:8080/graphql/api/v1" \
    -d'{ 
           "query" : "{ downloads { edges { node { downloads group product } } } }"
       }'

Here are the respective responses:

{
  "data": [
    {
      "attributes": {
        "downloads": 35,
        "group": "com.example.repository",
        "product": "elide-core"
      },
      "id": "0",
      "type": "downloads"
    }
  ]
}
{
  "data": {
    "downloads": {
      "edges": [
        {
          "node": {
            "downloads": 35,
            "group": "com.example.repository",
            "product": "elide-core"
          }
        }
      ]
    }
  }
}