Skip to main content

Basic Usage

Introduction

tip

One statement to save complex data of arbitrary shape, find DIFF to change database, like React/Vue

  • Upper right corner: The user passes in a data structure of any shape, and asks Jimmer to save it.

    There is an essential difference between this and the save method of other ORM frameworks. Taking JPA/Hibernate as an example, whether the scalar properties of the entity need to be saved is controlled byColumn.insertable andColumn.updatable, and whether association properties need to be saved is controlled byOneToOne.cascade,ManyToOne.cascade,OenToMany.cascade andManyToOne.cascade. However, no matter how the developer configures it, the shape of the data structure that JPA/Hibernate can save for you is fixed.

    Jimmer adopts a completely different approach. Although the saved jimmer object is strongly typed, it is dynamic (that is, not setting the object property and setting the object property to null are completely different things), Properties that are set are saved and properties that are not set are ignored, so that data structures of any shapes can be saved.

  • Upper left corner: Query the existing data structure from the database for comparison with the new data structure specified by the user.

    The shape of the data structure queried from database is same with the shape of new data structure give by user. Therefore, the query cost and comparison cost are determined by the complexity of the data structure specified by the user.

  • Below: Compare the old and new data structures, find DIFF and execute the corresponding SQL operations:

    • Orange part: For entity objects that exist in both old and new data structures, if some scalar properties change, modify the data
    • Blue part: For entity objects that exist in both old and new data structures, if some associations change, modify the association
    • Green part: For entity objects that exist in the new data structure but do not exist in the old data structure, insert data and create the association
    • Red part: For entity objects that exist in the old data structure but not in the new data structure, dissociate this object, clear the association and possibly delete the data
tip

The purpose of this function: take the data structure of any shape as a whole, and use one line of code to write it into the database, no matter how complicated the intermediate details are, you don't have to care.

If you know React or Vue in the web field, it is not difficult to see that this function is very similar to `Virtual DOM diff`.

     

Basic Concepts

A save command persists an arbitrary shaped data structure to the database, for example:

  • Saving a simple object

    Book simpleBook = Immutables.createBook(draft -> {
    draft.setName("SQL in Action");
    draft.setEdition(1);
    draft.setPrice(new BigDecimal("59.9"));
    });
    sqlClient.save(simpleBook);
  • Saving a complex data structure

    Book complexBook = Immutables.createBook(draft -> {
    draft.setName("SQL in Action");
    draft.setEdition(1);
    draft.setPrice(new BigDecimal("59.9"));
    draft.applyStore(store -> {
    store.setName("MANNING");
    })
    draft.addIntoAuthors(author -> {
    author.setFirstName("Dmitry");
    author.setLastName("Jamerov");
    author.setGender(Gender.MALE);
    });
    draft.addIntoAuthors(author -> {
    author.setFirstName("Svetlana");
    author.setLastName("Isakova");
    author.setGender(Gender.FEMALE);
    })
    });
    sqlClient.save(simpleBook);

APIs

Save commands provide multiple APIs for different languages and coding patterns, but with the same functionality:

Spring Data APILow-level API
APIs focused on concisenessAPIs focused on configurability
Java
  • JRepository.save
  • JRepository.saveCommand
  • JRepository.saveAll
  • JRepository.saveAllCommand
  • JRepository.insert
  • JRepository.update
  • JRepository.merge
  • JRepository.append
  • JSqlClient.save
  • JSqlClient.insert
  • JSqlClient.update
  • JSqlClient.merge
  • JSqlClient.append
  • JSqlClient.getEntities().save
  • JSqlClient.getEntities().saveCommand
  • JSqlClient.getEntities().saveAll
  • JSqlClient.getEntities().saveAllCommand
  • Kotlin
    • KRepository.save
    • KRepository.saveAll
    • KRepository.insert
    • KRepository.update
    • KRepository.merge
    • KRepository.append
    • KSqlClient.save
    • KSqlClient.insert
    • KSqlClient.update
    • KSqlClient.merge
    • KSqlClient.append
  • KSqlClient.entities.save
  • KSqlClient.entities.saveAll
  • Among them:

    • Java methods containing Command are special. Unlike other methods that execute save commands immediately, these methods only create the command without immediate execution. The user can configure them and finally call execute to execute the command. For example:

      BookStore store = ...
      sqlClient
      .getEntities()
      .saveCommand(store)
      .setPessimisticLock()
      .execute();
      • ❶ Create save command, do not execute immediately

      • ❷ Make some configurations

      • ❸ After configuration is done, finally call execute to execute the save command

      Kotlin does not need saveCommand methods, it uses a different syntax:

      val store = ...
      sqlClient.save(store) {
      setPessimisticLock()
      }
    • Methods containing saveAll indicate saving multiple objects rather than one object

    • For the aggregate root being saved, there are 3 save modes: UPSERT (default), INSERT_ONLY, UPDATE_ONLY (see Save Modes for more details). This is configured like:

      BookStore store = ...
      sqlClient.save(store, SaveMode.INSERT_ONLY);

      The insert and update methods are shorthand for INSERT_ONLY and UPDATE_ONLY. The above can be simplified to:

      BookStore store = ...
      sqlClient.insert(store);

    Important Concepts: Full Replacement vs Incremental Change

    The UI design for modifying data in an application can be divided into two styles:

    • Fully Commit

      This type of UI often has complex forms and provides a final button. After editing, the user submits all the information in the form at once.

    • Incremental Commit

      This type of UI does not have a submit button. Each time the user completes a local operation, the page automatically submits the changed part, which is a fragmented commit mode.

    The greatest value of the Save Command lies in simplifying the development of fully commit mode functionality. For the two different modes, the usage is different.

    Fully CommitIncremental Commit

    Jimmer automatically handles the internal details, comparing the new and old data to find all differences and executing the relevant modification operations (Jimmer's unique perspective)

    Use the Save Command (parameters are often complex data structures)

    Business code uses a combination of multiple simple operations to implement complex operations, and the user handles the internal details (the same as traditional methods).

    Comprehensively use multiple methods:

    Developers need to analyze their business scenarios to determine whether the current modification operation is a fully commit or an incremental commit, and make the right choice accordingly, without abuse.

    TreeNode treeNode = Immutables.createTreeNode(food -> {
    food
    .setParent(null)
    .setName("Food")
    .addIntoChildNodes(drink -> {
    drink
    .setName("Drink")
    .addIntoChildNodes(cocacola -> {
    cocacola.setName("Cocacola");
    })
    .addIntoChildNodes(fanta -> {
    fanta.setName("Fanta");
    });
    ;
    })
    .addIntoAuthors(bread -> {
    bread
    .setName("Bread")
    .addIntoChildNodes(baguette -> {
    baguette.setName("Baguette");
    })
    .addIntoChildNodes(ciabatta -> {
    ciabatta.setName("Ciabatta");
    })
    });
    ;
    });
    sqlClient.save(treeNode);

    This code tries to save a tree:

    +-Food
    |
    +---+-Drinks
    | |
    | +-----Cocacola
    | |
    | \-----Fanta
    |
    \---+-Bread
    |
    +-----Baguette
    |
    \-----Ciabatta

    Where Food is the aggregate root, and all other associated objects are child nodes.

    • Aggregate root node

      Corresponds to incremental change, ultimately generating one INSERT or UPDATE statement.

    • Child nodes (or associated objects)

      By default correspond to full replacement operations.

      Take Food as an example. It has two child nodes Drinks and Bread. This does not simply mean inserting or updating Drinks and Bread.
      When Food already exists in the database, we also need to consider whether it has other child nodes besides Drinks and Bread, and dissociate these child nodes from the parent (e.g. deleting them).

      For example:

      Existing data structure in databaseData structure user wants to save
      +-Food
      |
      |
      |
      +-----Meat(ignore child nodes)
      |
      \-----Bread(ignore child nodes)
      +-Food
      |
      +-----Drinks(ignore child nodes)
      |
      |
      |
      \-----Bread(ignore child nodes)

      Ultimately, for the child nodes, the operations are:

      • Drinks does not exist in old data structure but exists in new data structure, so insert Drinks.

      • Both old and new data structures have Bread, so update Bread.

      • Old data structure has Meat while new data structure does not, so delete Meat.

        There are multiple ways to dissociate child nodes. Here we assume delete operations are used.

    As you can see, by default, operations on child nodes other than the aggregate root correspond to full replacement rather than incremental modification.

    Q & A

    As discussed above, by default, associated objects other than the aggregate root correspond to full replacement operations rather than incremental modification.

    Q:

    Why do all associated objects other than the aggregate root default to full replacement operations?

    A:

    INSERT, UPDATE, and DELETE statements in relational databases are incremental operations themselves. Even using the simplest SQL scheme with just basic CRUD capabilities, incrementally modifying the database is never the real difficulty in application development.

    The truly complex problem is to save an entire complex data structure in one go. If Jimmer does not provide such capabilities, developers have to write complex code to compare new and old data to find differences and determine what needs to change. This makes saving complex data structure very difficult.

    There is another benefit - ensuring idempotency.

    Q:

    What are the use cases?

    A:

    Any scenario requiring saving an entire complex data structure in one go. One typical case is complex forms, for example:

    CommodityQuantityUnit priceItem priceDelete
    14.6929.38
    3030
    Total price:59.379999999999995
     

    In this example, the form embeds child object tables. The user can perform arbitrarily complex operations on the form, including the embedded child tables, and finally submit the entire form as a whole to the server.

    With save commands, the server can persist this data structure in one line of code, without considering how the data structure submitted by the client differs from the database.

    tip

    No matter how complex the form, or how deeply nested the associations, the entire structure can be saved in one line of code.