All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.molcikas.photon.PhotonTransaction Maven / Gradle / Ivy

The newest version!
package com.github.molcikas.photon;

import com.github.molcikas.photon.query.*;
import lombok.extern.slf4j.Slf4j;
import com.github.molcikas.photon.exceptions.PhotonException;
import com.github.molcikas.photon.blueprints.AggregateBlueprint;

import java.io.Closeable;
import java.sql.*;
import java.util.*;

/**
 * Contains a database transaction for Photon.
 */
@Slf4j
public class PhotonTransaction implements Closeable
{
    private final Connection connection;
    private final Map registeredAggregates;
    private final Map registeredViewModelAggregates;
    private final Photon photon;
    private final PhotonEntityState photonEntityState;
    private boolean hasUncommittedChanges = false;

    public PhotonTransaction(
        Connection connection,
        boolean autoCommit,
        Map registeredAggregates,
        Map registeredViewModelAggregates,
        Photon photon)
    {
        this.connection = connection;
        this.registeredAggregates = registeredAggregates;
        this.registeredViewModelAggregates = registeredViewModelAggregates;
        this.photon = photon;
        this.photonEntityState = new PhotonEntityState();

        try
        {
            connection.setAutoCommit(autoCommit);
        }
        catch (Exception ex)
        {
            throw new PhotonException(ex, "Error starting transaction.");
        }
    }

    /**
     * Shortcut method for creating a query and executing an update.
     *
     * @param sqlText - the plain sql text
     * @return - the number of rows updated
     */
    public int executeUpdate(String sqlText)
    {
        verifyConnectionIsAvailable("update", false);
        PhotonQuery query = query(sqlText);
        return query.executeUpdate();
    }

    /**
     * Create a photon query. Can be used for ad-hoc queries that return custom read models, or to execute ad-hoc
     * SQL commands.
     *
     * @param sqlText - the plain parameterized SQL text
     * @return - the photon query
     */
    public PhotonQuery query(String sqlText)
    {
        verifyConnectionIsAvailable("query", true);
        return query(sqlText, false);
    }

    /**
     * Create a photon query. Can be used for ad-hoc queries that return custom read models, or to execute ad-hoc
     * SQL commands. Set populateGeneratedKeys to true if running an insert statement into a table with an auto
     * incrementing primary key and the generated key needs to be retrieved.
     *
     * @param sqlText - the plain parameterized SQL text
     * @param populateGeneratedKeys - whether to retrieve the generated keys from the query
     * @return - the photon query
     */
    public PhotonQuery query(String sqlText, boolean populateGeneratedKeys)
    {
        verifyConnectionIsAvailable("query", true);
        return new PhotonQuery(sqlText, populateGeneratedKeys, connection, photon);
    }

    /**
     * Create an aggregate query. Aggregates must be registered with Photon.
     *
     * @param aggregateClass - The aggregate root's class
     * @param  - The aggregate root's class
     * @return - The photon aggregate query
     */
    public  PhotonAggregateQuery query(Class aggregateClass)
    {
        verifyConnectionIsAvailable("query", true);
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregateClass);
        return new PhotonAggregateQuery<>(aggregateBlueprint, connection, photonEntityState, photon);
    }

    /**
     * Create an aggregate query. Aggregates must be registered with Photon.
     *
     * @param aggregateClass - The aggregate root's class
     * @param viewModelAggregateBlueprintName - the name of the view model aggregate blueprint to use for querying
     * @param  - The aggregate root's class
     * @return - The photon aggregate query
     */
    public  PhotonAggregateQuery query(Class aggregateClass, String viewModelAggregateBlueprintName)
    {
        verifyConnectionIsAvailable("query", true);
        AggregateBlueprint aggregateBlueprint = getViewModelAggregateBlueprint(aggregateClass, viewModelAggregateBlueprintName);
        return new PhotonAggregateQuery<>(aggregateBlueprint, connection, photonEntityState, photon);
    }

    /**
     * Save the aggregate instance. This will perform an update first, then an insert if the entity is not in the
     * database.
     *
     * @param aggregate - The aggregate instance to save
     */
    public void save(Object aggregate)
    {
        saveWithExcludedFields(aggregate, Collections.emptyList());
    }

    /**
     * Save the aggregate instance. This will perform an update first, then an insert if the entity is not in the
     * database.
     *
     * @param aggregate - The aggregate instance to save
     * @param fieldsToExclude - A list of fields that will NOT be saved to the database. Orphaned rows will also
     *                        not be removed.
     */
    public void saveWithExcludedFields(Object aggregate, String... fieldsToExclude)
    {
        saveWithExcludedFields(aggregate, Arrays.asList(fieldsToExclude));
    }

    /**
     * Save the aggregate instance. This will perform an update first, then an insert if the entity is not in the
     * database.
     *
     * @param aggregate - The aggregate instance to save
     * @param fieldsToExclude - A list of fields that will NOT be saved to the database. Orphaned rows will also
     *                        not be removed.
     */
    public void saveWithExcludedFields(Object aggregate, Collection fieldsToExclude)
    {
        verifyConnectionIsAvailable("save", true);
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregate.getClass());
        new PhotonAggregateSave(aggregateBlueprint, connection, photonEntityState, photon.getOptions())
            .save(aggregate, fieldsToExclude);
        hasUncommittedChanges = true;
    }

    /**
     * Save a list of aggregate instances. This will perform an update first, then an insert if the entity is not in the
     * database.
     *
     * @param aggregates - The aggregate instances to save
     * @param  - The aggregate root's class
     */
    public  void saveAll(T... aggregates)
    {
        saveAllAndExcludeFields(Arrays.asList(aggregates), null);
    }

    /**
     * Save a list of aggregate instances. This will perform an update first, then an insert if the entity is not in the
     * database.
     *
     * @param aggregates - The aggregate instances to save
     */
    public void saveAll(Collection aggregates)
    {
        saveAllAndExcludeFields(aggregates, null);
    }

    /**
     * Save a list of aggregate instances. This will perform an update first, then an insert if the entity is not in the
     * database.
     *
     * @param aggregates - The aggregate instances to save
     * @param fieldsToExclude - A list of fields that will NOT be saved to the database. Orphaned rows will also
     *                        not be removed.
     */
    public void saveAllAndExcludeFields(Collection aggregates, Collection fieldsToExclude)
    {
        verifyConnectionIsAvailable("save", true);
        if(aggregates == null || aggregates.isEmpty())
        {
            return;
        }
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregates.iterator().next().getClass());
        new PhotonAggregateSave(aggregateBlueprint, connection, photonEntityState, photon.getOptions())
            .saveAll(aggregates, fieldsToExclude);
        hasUncommittedChanges = true;
    }

    /**
     * Inserts an aggregate instance. Only call this method if the aggregate does not exist in the database, otherwise
     * an error will occur when trying to insert it.
     *
     * @param aggregate - The aggregate instance to insert
     */
    public void insert(Object aggregate)
    {
        verifyConnectionIsAvailable("insert", true);
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregate.getClass());
        new PhotonAggregateSave(aggregateBlueprint, connection, photonEntityState, photon.getOptions())
            .insert(aggregate);
        hasUncommittedChanges = true;
    }

    /**
     * Inserts a list of aggregate instances. Only call this method if the aggregates do not exist in the database, otherwise
     * an error will occur when trying to insert them.
     *
     * @param aggregates - The aggregate instances to insert
     * @param  The aggregate root's class
     */
    public  void insertAll(T... aggregates)
    {
        insertAll(Arrays.asList(aggregates));
    }

    /**
     * Inserts a list of aggregate instances. Only call this method if the aggregates do not exist in the database, otherwise
     * an error will occur when trying to insert them.
     *
     * @param aggregates - The aggregate instances to insert
     */
    public void insertAll(Collection aggregates)
    {
        verifyConnectionIsAvailable("insert", true);
        if(aggregates == null || aggregates.isEmpty())
        {
            return;
        }
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregates.iterator().next().getClass());
        new PhotonAggregateSave(aggregateBlueprint, connection, photonEntityState, photon.getOptions())
            .insertAll(aggregates);
        hasUncommittedChanges = true;
    }

    /**
     * Delete an aggregate instance.
     *
     * @param aggregate - The aggregate instance to delete
     */
    public void delete(Object aggregate)
    {
        verifyConnectionIsAvailable("delete", false);
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregate.getClass());
        new PhotonAggregateDelete(aggregateBlueprint, connection, photonEntityState, photon.getOptions()).delete(aggregate);
        hasUncommittedChanges = true;
    }

    /**
     * Delete a list of aggregate instances.
     *
     * @param aggregates - The aggregate instances to delete
     * @param  - The aggregate root's class
     */
    public  void deleteAll(T... aggregates)
    {
        deleteAll(Arrays.asList(aggregates));
    }

    /**
     * Delete a list of aggregate instances.
     *
     * @param aggregates - The aggregate instances to delete
     */
    public void deleteAll(Collection aggregates)
    {
        verifyConnectionIsAvailable("delete", false);
        if(aggregates == null || aggregates.isEmpty())
        {
            return;
        }
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregates.iterator().next().getClass());
        new PhotonAggregateDelete(aggregateBlueprint, connection, photonEntityState, photon.getOptions()).deleteAll(aggregates);
        hasUncommittedChanges = true;
    }

    /**
     * Add an aggregate in its current state to the change tracker. Only changes made to the aggregate after tracking is
     * started will be saved. Therefore, this method should only be used to track aggregates that were created outside
     * the transaction. Aggregates created via a fetch query in the current transaction are already tracked by default.
     *
     * @param aggregate - The aggregate to start tracking changes for
     * @param  - The aggregate's class
     */
    public  void track(T aggregate)
    {
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregate.getClass());
        PopulatedEntity populatedEntity =
            new PopulatedEntity<>(aggregateBlueprint.getAggregateRootEntityBlueprint(), aggregate);
        photonEntityState.track(populatedEntity);
    }

    /**
     * Add an aggregate in its current state to the change tracker. Only changes made to the aggregate after tracking is
     * started will be saved. Therefore, this method should only be used to track aggregates that were created outside
     * the transaction. Aggregates created via a fetch query in the current transaction are already tracked by default.
     *
     * @param aggregates - The aggregates to start tracking changes for
     * @param  - The aggregate's class
     */
    public  void trackAll(Collection aggregates)
    {
        if(aggregates == null || aggregates.isEmpty())
        {
            return;
        }
        aggregates.forEach(this::track);
    }

    /**
     * Removes an aggregate from tracking. This is useful if the aggregate is no longer needed but the transaction
     * is still active. The aggregate should not be saved or deleted once untracked, otherwise the entire aggregate
     * will be re-saved or deleted, which could cause performance issues.
     *
     * @param aggregate - The aggregate to stop tracking changes for
     * @param  - The aggregate's class
     */
    public  void untrack(T aggregate)
    {
        AggregateBlueprint aggregateBlueprint = getAggregateBlueprint(aggregate.getClass());
        PopulatedEntity populatedEntity =
            new PopulatedEntity<>(aggregateBlueprint.getAggregateRootEntityBlueprint(), aggregate);
        photonEntityState.untrack(populatedEntity);
    }

    /**
     * Removes an aggregate from tracking. This is useful if the aggregate is no longer needed but the transaction
     * is still active. The aggregate should not be saved or deleted once untracked, otherwise the entire aggregate
     * will be re-saved or deleted, which could cause performance issues.
     *
     * @param aggregates - The aggregates to stop tracking changes for
     * @param  - The aggregate's class
     */
    public  void untrackAll(Collection aggregates)
    {
        if(aggregates == null || aggregates.isEmpty())
        {
            return;
        }
        aggregates.forEach(this::untrack);
    }

    /**
     * Commit the transaction and close the underlying connection.
     */
    public void commit()
    {
        try
        {
            connection.commit();
            hasUncommittedChanges = false;
        }
        catch(Exception ex)
        {
            throw new PhotonException(ex, "Error committing transaction.");
        }
    }

    /**
     * Close the connection without committing. If commit() was already called, calling this method will have
     * no effect.
     */
    public void close()
    {
        try
        {
            connection.close();
        }
        catch(Exception ex)
        {
            throw new PhotonException(ex, "Error closing connection.");
        }
    }

    /**
     * Roll back the current transaction.
     */
    public void rollback()
    {
        try
        {
            connection.rollback();
        }
        catch(Exception ex)
        {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Returns whether the transaction has pending changes that haven't been committed yet.
     * @return - True if there are pending changes, otherwise false.
     */
    public boolean hasUncommittedChanges()
    {
        return hasUncommittedChanges;
    }

    private  AggregateBlueprint getViewModelAggregateBlueprint(Class aggregateClass, String viewModelAggregateBlueprintName)
    {
        AggregateBlueprint aggregateBlueprint = registeredViewModelAggregates.get(viewModelAggregateBlueprintName);
        if(aggregateBlueprint == null)
        {
            throw new PhotonException(
                "The aggregate view model named '%s' is not registered with photon.",
                viewModelAggregateBlueprintName
            );
        }
        if(aggregateBlueprint.getAggregateRootClass().isAssignableFrom(aggregateClass.getClass()))
        {
            throw new PhotonException(
                "The aggregate blueprint view model '%s' is not compatible with class '%s'.",
                viewModelAggregateBlueprintName,
                aggregateClass.getName()
            );
        }
        return aggregateBlueprint;
    }

    private  AggregateBlueprint getAggregateBlueprint(Class aggregateClass)
    {
        AggregateBlueprint aggregateBlueprint = registeredAggregates.get(aggregateClass);
        Class superClass = aggregateClass;

        while(aggregateBlueprint == null && superClass != null)
        {
            superClass = superClass.getSuperclass();
            if(superClass != null)
            {
                aggregateBlueprint = registeredAggregates.get(superClass);
            }
        }

        if(aggregateBlueprint == null)
        {
            throw new PhotonException("The aggregate root class '%s' is not registered with photon.", aggregateClass.getName());
        }
        return aggregateBlueprint;
    }

    private void verifyConnectionIsAvailable(String operation, boolean useA)
    {
        try
        {
            if (connection.isClosed())
            {
                throw new PhotonException("Cannot perform %s %s operation because the connection is closed.", useA ? "a" : "an", operation);
            }
        }
        catch(SQLException ex)
        {
            throw new RuntimeException(ex);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy