Entity basics, selecting and modifying.

package is.codion.framework.demos.chinook.tutorial;

import is.codion.common.db.database.Database;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnection.Select;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.DomainModel;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.OrderBy;
import is.codion.framework.domain.entity.StringFactory;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ColumnDefinition;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.framework.domain.entity.attribute.ForeignKeyDefinition;

import java.util.List;

import static is.codion.framework.demos.chinook.tutorial.EntitiesTutorial.Chinook.Album;
import static is.codion.framework.demos.chinook.tutorial.EntitiesTutorial.Chinook.Artist;
import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.KeyGenerator.identity;

/**
 * When running this make sure the chinook demo module directory is the
 * working directory, due to a relative path to a db init script
 */
public final class EntitiesTutorial {

  // The domain class, containing the domain model definition
  public static final class Chinook extends DomainModel {

    // DomainType for identifying this domain model
    static final DomainType DOMAIN = domainType(Chinook.class);

    // EntityType constant for the table entityType
    // and a Column for each column
    public interface Artist {
      EntityType TYPE = DOMAIN.entityType("chinook.artist");

      Column<Long> ID = TYPE.longColumn("artistid");
      Column<String> NAME = TYPE.stringColumn("name");
    }

    // EntityType constant for the table entityType and a Column
    // for each column and one for the foreign key relation
    public interface Album {
      EntityType TYPE = DOMAIN.entityType("chinook.album");

      Column<Long> ID = TYPE.longColumn("albumid");
      Column<String> TITLE = TYPE.stringColumn("title");
      Column<Long> ARTIST_ID = TYPE.longColumn("artistid");

      // create a foreign key attribute referencing the Artist.TYPE, via the Album.ARTIST_ID attribute
      ForeignKey ARTIST_FK = TYPE.foreignKey("artist_fk", ARTIST_ID, Artist.ID);
    }

    public Chinook() {
      super(DOMAIN);
      // Note that the below demo code is unusual since the
      // builders are exposed for illustration purposes.

      // create columns for the table 'chinook.artist'
      ColumnDefinition.Builder<Long, ?> artistId =
              Artist.ID.define()
                      .primaryKey();

      ColumnDefinition.Builder<String, ?> artistName =
              Artist.NAME.define()
                      .column()
                      .caption("Name")
                      .nullable(false)
                      .maximumLength(120);

      // define an entity based on the table 'chinook.artist', with the above columns
      EntityDefinition artist = Artist.TYPE.define(artistId, artistName)
              .keyGenerator(identity())
              .stringFactory(Artist.NAME)
              .smallDataset(true)
              .caption("Artist")
              .build();

      // add the artist definition to this domain model
      add(artist);

      // create columns and foreign key for the table 'chinook.album'
      ColumnDefinition.Builder<Long, ?> albumId =
              Album.ID.define()
                      .primaryKey();

      ColumnDefinition.Builder<String, ?> albumTitle =
              Album.TITLE.define()
                      .column()
                      .caption("Title")
                      .nullable(false)
                      .maximumLength(160);

      ColumnDefinition.Builder<Long, ?> albumArtistId =
              Album.ARTIST_ID.define()
                      .column()
                      .nullable(false);

      ForeignKeyDefinition.Builder albumArtist =
              Album.ARTIST_FK.define()
                      .foreignKey()
                      .caption("Artist");

      // define an entity based on the table 'chinook.album', with the above columns and foreign key
      EntityDefinition album = Album.TYPE.define(albumId, albumTitle, albumArtistId, albumArtist)
              .keyGenerator(identity())
              .stringFactory(StringFactory.builder()
                      .value(Album.ARTIST_FK)
                      .text(" - ")
                      .value(Album.TITLE)
                      .build())
              .caption("Album")
              .build();

      // add the album definition to this domain model
      add(album);
    }
  }

  /**
   * Demonstrates how to use a {@link EntityConnection} to select Entity instances.
   * @throws DatabaseException in case of an exception
   */
  private static void selectingEntities(EntityConnectionProvider connectionProvider) throws DatabaseException {
    // fetch the connection from the provider, note that the provider returns
    // the same connection until the previous one has been disconnected or
    // has become invalid for some reason
    EntityConnection connection = connectionProvider.connection();

    // select the artist Metallica by name, the selectSingle() method
    // throws a RecordNotFoundException if no record is found and a
    // MultipleRecordsFoundException if more than one are found
    Entity metallica = connection.selectSingle(Artist.NAME.equalTo("Metallica"));

    // select all albums by Metallica, by using select() with the
    // Metallica Entity as condition value, basically asking for the
    // records where the given foreign key references that specific Entity
    // select() returns an empty list if none are found
    List<Entity> albums = connection.select(Album.ARTIST_FK.equalTo(metallica));

    albums.forEach(System.out::println);

    // for queries requiring further configuration, such as order by, we use
    // a Select.Builder initialized with a condition specifying
    // the attribute we're searching by, the operator and value.
    Select selectArtists =
            Select.where(Artist.NAME.like("An%"))
                    // and we set the order by clause
                    .orderBy(OrderBy.ascending(Artist.NAME))
                    .build();

    List<Entity> artistsStartingWithAn = connection.select(selectArtists);

    artistsStartingWithAn.forEach(System.out::println);

    // create a select
    Select selectAlbums =
            Select.where(Album.ARTIST_FK.in(artistsStartingWithAn))
                    .orderBy(OrderBy.builder()
                            .ascending(Album.ARTIST_ID)
                            .descending(Album.TITLE)
                            .build())
                    .build();

    List<Entity> albumsByArtistsStartingWithAn = connection.select(selectAlbums);

    albumsByArtistsStartingWithAn.forEach(System.out::println);
  }

  /**
   * Demonstrates how to use a {@link EntityConnection} to modify Entity instances.
   * @throws DatabaseException in case of an exception
   */
  private static void modifyingEntities(EntityConnectionProvider connectionProvider) throws DatabaseException {
    EntityConnection connection = connectionProvider.connection();

    //this Entities instance serves as a factory for Entity instances
    Entities entities = connectionProvider.entities();

    // we create a new band
    Entity myBand = entities.builder(Artist.TYPE)
            // and give the band a name
            .with(Artist.NAME, "My band name")
            .build();

    // and insert the band
    myBand = connection.insertSelect(myBand);

    // now for our first album
    Entity album = entities.builder(Album.TYPE)
            // set the album artist to my band
            .with(Album.ARTIST_FK, myBand)
            // and set the title
            .with(Album.TITLE, "My first album")
            .build();

    // and insert the album
    album = connection.insertSelect(album);

    // let's rename our album and our band as well
    myBand.put(Artist.NAME, "A proper bandname");
    album.put(Album.TITLE, "A proper title");

    // and perform the update
    connection.update(List.of(myBand, album));

    // finally, we clean up after ourselves by deleting our imaginary band and album,
    // note that the order of the entities matters, since we can't delete
    // the artist before the album.
    connection.delete(Entity.primaryKeys(List.of(album, myBand)));
  }

  public static void main(String[] args) throws DatabaseException {
    // Configure the database
    Database.DATABASE_URL.set("jdbc:h2:mem:h2db");
    Database.DATABASE_INIT_SCRIPTS.set("src/main/sql/create_schema.sql");
    // initialize a connection provider, this class is responsible
    // for supplying a valid connection or throwing an exception
    // in case a connection can not be established
    EntityConnectionProvider connectionProvider =
            LocalEntityConnectionProvider.builder()
                    .domain(new Chinook())
                    .user(User.parse("scott:tiger"))
                    .build();

    selectingEntities(connectionProvider);

    modifyingEntities(connectionProvider);

    connectionProvider.close();
  }
}