Fenix Documentation

repository·develop·Indexed 18 days ago

https://github.com/blinkfox/fenix

A lightweight Spring Data JPA extension designed to simplify the creation and maintenance of complex dynamic SQL. Fenix bridges the gap between basic CRUD and powerful dynamic SQL capabilities via four query methods: XML (using MVEL), a Java fluent API, dynamic condition annotations, and dynamic Specification construction. It features ActiveRecord pattern support, enhanced batch operations, Snowflake and NanoId generation, and flexible result mapping.

Tokens
38.1K
Snippets
116
Records
127
Agent score
62%

What's inside Fenix

  1. Overview of Fenix

    develop

    Fenix is a Spring Data JPA extension library designed to simplify the writing of complex and dynamic JPQL (Java Persistence Query Language). It aims to bridge the gap between the simplicity of Spring Data JPA for basic CRUD and the powerful dynamic SQL capabilities found in MyBatis.

    Key capabilities include:

    • Four ways to write dynamic SQL: Using XML, Java fluent (chaining) API, dynamic condition annotations, and dynamic Specification construction.
    • ActiveRecord pattern support.
    • Enhanced Batch Operations: Faster support for batch 'create/delete/update' operations, including incremental updates for non-null properties.
    • ID Generation: Built-in support for Snowflake algorithm and NanoId primary key generation strategies.
    • Flexible Result Mapping: SQL execution results can be mapped to any custom entity object, offering a simpler alternative to standard JPA projections.
    • Extensibility: Allows for custom XML semantic tags and tag handlers to generate custom SQL fragments and parameters.
  2. Overview of Primary Key ID Generation Strategies

    develop

    Fenix provides several primary key ID generation strategies for JPA entities or manual usage via Java API. The recommended priority for selection is: Snowflake Algorithm > NanoId > UUID.

    Available strategies include:

    • Snowflake ID (Long): Ordered, long integer.
    • 36-base Snowflake ID (String): Shortened snowflake ID using 36-base encoding.
    • 62-base Snowflake ID (String): Shortened snowflake ID using 62-base encoding.
    • 21-character NanoId (String): Fast, compact string ID.
    • 62-base UUID (String): Shortened 19-character UUID using 62-base encoding.
  3. Create custom XML semantic tags in Fenix

    develop

    Fenix allows you to extend its XML SQL capabilities by creating custom semantic tags. This is useful for encapsulating complex, reusable dynamic SQL logic (such as data permission checks or complex business rules) into a single, readable XML tag.

    To implement a custom tag, you must:

    1. Design the XML tag structure: Decide on the tag name and the attributes it will accept (e.g., field, userId).
    2. Implement a FenixHandler: Create a Java class that implements the FenixHandler interface to define how the tag translates into SQL fragments and parameters.
    3. Map the tag using @Tagger: Use the @Tagger annotation on your handler class to link it to specific XML tag names and optional prefixes.
    4. Configure scanning: Tell Fenix where to find your handler classes via configuration.

    This approach is superior to using standard logic control (@if/@else) or <choose> tags when the logic is complex and needs to be reused across many different queries.

    <!-- Example of a custom tag usage -->
    <regionAuth field="x.region" userId="searchMap.userId"/>
    
    <!-- Example of a custom tag with a prefix -->
    <andRegionAuth field="x.region" userId="searchMap.userId"/>
  4. Use built-in result transformers for custom Beans

    develop

    Fenix provides several built-in resultTransformer implementations to handle different SQL naming conventions when mapping results to Java Beans. You specify the transformer in the @QueryFenix annotation using the resultTransformer attribute.

    Available Transformers:

    • FenixResultTransformer: (Default) Maps results where the SQL as alias matches the Bean property name exactly (case-insensitive).
    • UnderscoreTransformer: Maps results by converting snake_case column names to lowerCamelCase property names.
    • PrefixUnderscoreTransformer: Converts snake_case to lowerCamelCase and strips specific prefixes like c_, n_, or dt_.
    • ColumnAnnotationTransformer: Maps results based on the @Column(name = "xxx") annotation present on the Bean's fields.
  5. How Fenix handles dynamic SQL

    develop

    Fenix provides multiple mental models for handling dynamic queries depending on your use case:

    1. XML-based SQL (Recommended for long/complex SQL): Decouples SQL from Java code. It uses MVEL expression syntax and template engine logic (similar to MyBatis) to support if/else, foreach, and other expressions. It also introduces semantic XML tags to solve the problem of repetitive SQL fragments, allowing you to reuse common SQL logic via custom or built-in tags.
    2. Java Fluent API: A chainable API for writing dynamic SQL directly in Java code, providing high readability and compactness.
    3. Dynamic Condition Annotations: Allows you to mark fields in an entity bean with annotations to automatically derive dynamic query conditions.
    4. Dynamic Specification: Uses Java chaining to build dynamic JPA Specification objects.
  6. How Fenix determines if a bean property should generate a query condition

    develop

    By default, Fenix only generates a query condition for a property if its value is not empty.

    Default 'Empty' Logic

    A property is considered empty (and thus ignored in the query) if:

    • It is a standard Java object that is null.
    • It is a String that is blank (isBlank()).
    • It is an array or collection that is null or empty.

    Warning: Avoid using primitive types (like int, long) in your parameter beans. Since primitives cannot be null, they will always be treated as non-empty (e.g., 0 will be treated as a valid query value). Use wrapper classes like Integer or Long instead.

    Customizing the Match Logic

    If you need specific logic to decide whether a condition should be generated, add a no-argument method to your Java Bean that has the same name as the property and returns a boolean.

    If the method returns true, the annotated condition is generated. If false, it is skipped.

    @Getter
    @Setter
    public class BookMatch {
        @Equals
        private String id;
    
        /**
         * Custom logic: Only generate the 'id' condition if id is not null and not "1".
         */
        public boolean id() {
            return id != null && !id.equals("1");
        }
    }
  7. Define dynamic SQL in Fenix XML

    develop

    Fenix XML files are used to define JPQL statements with dynamic predicates. By default, Fenix scans the src/main/resources/fenix directory.

    XML Structure:

    1. The root element <fenixs> must define a namespace that matches the repository name used in @QueryFenix.
    2. The <fenix> element must have an id that matches the method identifier used in @QueryFenix.
    3. Inside <fenix>, use predicate tags like <in>, <andLike>, and <andBetween> to build dynamic WHERE clauses.

    Example BlogRepository.xml:

    <fenixs namespace="BlogRepository">
        <fenix id="queryMyBlogs">
            SELECT
                b
            FROM
                Blog AS b
            WHERE
            <in field="b.id" value="ids" match="ids != empty"/>
            <andLike field="b.author" value="blog.author" match="blog.author != empty"/>
            <andLike field="b.title" value="blog.title" match="blog.title != empty"/>
            <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime" match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
        </fenix>
    </fenixs>
    <fenixs namespace="BlogRepository">
        <fenix id="queryMyBlogs">
            SELECT b FROM Blog AS b
            WHERE
            <in field="b.id" value="ids" match="ids != empty"/>
            <andLike field="b.author" value="blog.author" match="blog.author != empty"/>
        </fenix>
    </fenixs>
  8. Implement the ActiveRecord pattern in Fenix

    develop

    Fenix implements the ActiveRecord pattern using interface composition rather than inheritance. This allows your domain entities to maintain their own inheritance hierarchy while gaining database access capabilities. To use this pattern, your entity must implement specific Model interfaces provided by Fenix, which link the entity to its corresponding Repository.

    // The entity implements a Model interface to enable ActiveRecord methods
    public class Blog implements JpaModel<Blog, String, BlogRepository> {
        @Id
        private String id;
        // ...
    }
    
    // The repository is linked to the entity via the Model interface
    @Repository
    public interface BlogRepository extends JpaRepository<Blog, String> {
    }
  9. How Fenix SQL semantic tags work

    develop

    Fenix uses XML semantic tags to generate dynamic SQL, replacing verbose if/else or foreach logic found in other frameworks. These tags leverage the MVEL template engine to evaluate conditions.

    Key concepts:

    • Conditional Generation: Most tags include a match attribute. This attribute accepts an MVEL expression. If the expression evaluates to true, the SQL fragment is generated; otherwise, it is omitted. If match is omitted, the fragment is always generated.
    • Logical Prefixes: Most tags support AND, OR, and NOT prefixes (e.g., <andEqual>, <orEqual>, <notEqual>) to simplify dynamic query construction.
    • Parameter Mapping: Tags automatically map values to named parameters in JPQL. You can use the name attribute to explicitly define the parameter name used in the generated SQL.
    <!-- Example of the core idea: conditional generation via match -->
    <andEqual field="email" value="user.email" match="?email != empty" />
    <!-- If email is not empty, generates: AND email = :user_email -->
  10. How Fenix maps Java interfaces to XML files

    develop

    Fenix uses a lookup mechanism to connect Java repository interfaces with their corresponding XML configuration files.

    To use the simplified @QueryFenix annotation without extra parameters, follow these naming conventions:

    1. Namespace: The namespace attribute in your BlogRepository.xml must match the fully qualified name of your BlogRepository.java interface.
    2. ID Matching: The fenixId in the XML must match the method name in the Java interface.

    If these do not match, you must explicitly provide the mapping information in the annotation (as shown in the quickstart example), but adhering to these conventions is the recommended best practice for cleaner code.

  11. Use ParamWrapper to build context maps

    develop

    ParamWrapper is a utility class designed to simplify the creation of Map<String, Object> context parameters. It provides a fluent, chainable API to wrap multiple individual parameters or existing maps into a single map object.

    // Traditional way
    Map<String, Object> context = new HashMap<String, Object>();
    context.put("sex", "1");
    context.put("stuId", "123");
    
    // Using ParamWrapper (Fluent API)
    Map<String, Object> context = ParamWrapper.newInstance("sex", "1").put("stuId", "123").toMap();
  12. Use between matching methods and handle boundary degradation

    develop

    The between methods allow for range matching. They take a fieldName, a startValue, and an endValue.

    Degradation Behavior

    If one of the boundary values is null, the method automatically degrades to a simpler comparison:

    • Both non-null: Generates a between ... and ... condition.
    • Start non-null, End is null: Degrades to a Greater Than or Equal (>=) condition.
    • Start is null, End non-null: Degrades to a Less Than or Equal (<=) condition.
    • Both are null: Throws an exception.

    API Methods

    • andBetween(String fieldName, Object startValue, Object endValue, [boolean match])
    • orBetween(String fieldName, Object startValue, Object endValue, [boolean match])
    • andNotBetween(String fieldName, Object startValue, Object endValue, [boolean match])
    • orNotBetween(String fieldName, Object startValue, Object endValue, [boolean match])
    // Standard range match
    builder.andBetween("totalPage", minTotalPage, maxTotalPage)
    
    // Degrades to >= minTotalPage
    builder.andBetween("totalPage", minTotalPage, null, minTotalPage != null)
    
    // Degrades to <= maxTotalPage
    builder.andBetween("totalPage", null, maxTotalPage)