Skip to Content
Entity Annotations

Entity Annotations

IMPORTANT: All annotations are optional! The library works with plain Java classes using convention-based mapping. Annotations are only needed when you want to override the default behavior.

JPA-Compatible Annotations

DEV1.0 Picnic uses JPA-compatible annotation names for a familiar developer experience:

  • @Table - Maps entity to database table (same name as JPA)
  • @Column - Maps field to database column (same name as JPA)
  • @Id - Marks primary key field (same name as JPA)
  • @Transient - Excludes field from mapping (same name as JPA)
  • @Temporal - Specifies temporal type for java.util.Date fields

Plus additional annotations for advanced features:

  • @CalculatedColumn - Virtual columns from SQL expressions
  • @MappedCollection - One-to-many relationships
  • @JoinCustomCondition - Custom JOIN conditions
  • @With - Common Table Expressions (CTEs)
  • @ArrayColumn - PostgreSQL ARRAY/JSONB/JSON columns

Full JPA Entity Support

DEV1.0 Picnic also supports reading JPA annotations as a fallback! If you already have JPA entities in your project, you can use them directly without any changes.

Supports both JPA versions:

  • Jakarta Persistence API (JPA 3.0+) - jakarta.persistence.* - Java EE 9+, Spring Boot 3+
  • javax.persistence API (JPA 2.x) - javax.persistence.* - Java EE 8, Spring Boot 2.x

Annotation Priority Order

  1. DEV1.0 Picnic annotations (highest priority) - com.dev10.picnic.annotations.*
  2. JPA annotations (fallback) - jakarta.persistence.* or javax.persistence.*
  3. Convention over configuration (lowest priority) - Automatic snake_case mapping

@Table (Optional)

Use this annotation only when your table name differs from the snake_case class name or you need to specify a schema.

import com.dev10.picnic.annotations.Table; // WITHOUT annotation: EmployeeRecord -> "employee_record" table public class EmployeeRecord { } // WITH annotation: Override table name @Table("t_employee") public class EmployeeRecord { } // WITH schema: Specify database schema @Table(value = "employees", schema = "hr") public class Employee { }

@Column (Optional)

Use this annotation only when your column name differs from the snake_case field name.

import com.dev10.picnic.annotations.Column; // WITHOUT annotation: firstName -> "first_name" column private String firstName; // WITH annotation: Override column name @Column("fname") private String firstName; // WITH annotation: Legacy column names @Column("EMP_DEPT_ID") private Long departmentId;

@Id (Optional)

Use this annotation only when your primary key field is not named id.

import com.dev10.picnic.annotations.Id; import com.dev10.picnic.annotations.Column; // Override primary key field @Id @Column("employee_id") private Long employeeId; // Without annotation: field named "id" is automatically the primary key private Long id;

@Temporal

Specifies the SQL temporal type for java.util.Date fields to distinguish between DATE (date-only) and TIMESTAMP (date+time) columns.

import com.dev10.picnic.annotations.Temporal; import com.dev10.picnic.annotations.Column; import java.util.Date; @Column("event_date") @Temporal(Temporal.Type.DATE) private Date eventDate; // Maps to SQL DATE - only yyyy-MM-dd @Column("created_at") @Temporal(Temporal.Type.TIMESTAMP) private Date createdAt; // Maps to SQL TIMESTAMP - full date+time

Note: Without @Temporal, java.util.Date fields default to DATE type. For new code, prefer using java.time.LocalDate or java.time.LocalDateTime which don’t require this annotation.

@CalculatedColumn

Defines a virtual column computed from a SQL expression.

@Table("employees") public class Employee { @Column("salary") private Double salary; @Column("bonus") private Double bonus; // Calculated column: total compensation @CalculatedColumn("salary + COALESCE(bonus, 0)") private Double totalCompensation; // Calculated column: full name @CalculatedColumn("CONCAT(first_name, ' ', last_name)") private String fullName; // Calculated column: age @CalculatedColumn("EXTRACT(YEAR FROM AGE(birth_date))") private Integer age; // Calculated column with schema placeholder @CalculatedColumn("(SELECT COUNT(*) FROM ${schema}.orders WHERE customer_id = customers.id)") private Integer orderCount; } // Can filter and sort by calculated columns // filter: ["totalCompensation", ">", 100000] // sort: [{"selector": "age", "desc": true}]

Security Note: Expressions are validated to prevent SQL injection.

@MappedCollection

Marks a field as a JOIN-based collection relationship (one-to-many).

public class Department { private Long id; private String name; // Employees are loaded via JOIN @JoinCustomCondition(condition = "dept.id = emp.department_id") @MappedCollection private List<Employee> employees; }

@JoinCustomCondition

Defines custom JOIN conditions for relationships, supporting complex logic beyond simple foreign key relationships.

// Simple equality join @JoinCustomCondition(condition = "parent.id = child.department_id") // Complex multi-condition join with OR logic @JoinCustomCondition(condition = "(dept.id = emp.dept_id OR dept.backup_id = emp.dept_id) AND dept.active = true") // Multi-table join condition @JoinCustomCondition(condition = "p.id = l.id_prestation OR p.id = r.id_prestation OR p.id = e.id_presta_fact")

@With

Defines a Common Table Expression (CTE) for complex queries with support for dynamic variables.

Basic Static Query

@With(query = "SELECT * FROM legacy_employees WHERE status = 'ACTIVE'") @Table("legacy_employees") public class ActiveEmployee { @Id private Long id; private String firstName; private String status; }

Dynamic Schema Names (Multi-tenant)

@With(query = "SELECT * FROM ${schema}.employees WHERE status = :status") @Table("active_employees") public class ActiveEmployee { @Id private Long id; private String firstName; private String status; } // Usage with runtime schema and parameters DevExtremeResult<ActiveEmployee> result = gridDataProvider .query(ActiveEmployee.class) .loadOptions(loadOptions) .schema("tenant_123") // Dynamic schema name .withParameter("status", "ACTIVE") // Parameters for WITH query .executeDevExtreme(); // Generated SQL: // WITH ae AS ( // SELECT * FROM tenant_123.employees WHERE status = :status // ) // SELECT * FROM "active_employees" WHERE ...

Multiple Dynamic Parameters

@With(query = """ SELECT e.id, e.first_name, e.last_name, e.salary, e.hire_date FROM ${schema}.employees e WHERE e.hire_date >= :fromDate AND e.hire_date <= :toDate AND e.salary >= :minSalary """) @Table("employee_report") public class EmployeeReport { } // Usage DevExtremeResult<EmployeeReport> result = gridDataProvider .query(EmployeeReport.class) .loadOptions(loadOptions) .schema("hr_prod") .withParameter("fromDate", LocalDate.of(2024, 1, 1)) .withParameter("toDate", LocalDate.of(2024, 12, 31)) .withParameter("minSalary", new BigDecimal("50000")) .executeDevExtreme();

Placeholder Types

PlaceholderDescriptionSecurity
${schema}Replaced with schema parameterValidated (alphanumeric, underscores, dots only)
:paramNameJDBC named parametersBound via PreparedStatement (SQL injection safe)
${customVar}Custom context variablesValidated and replaced

@Transient

Excludes a field from database mapping.

import com.dev10.picnic.annotations.Transient; @Transient private transient String temporaryValue; @Transient private String calculatedField; // Computed in Java, not from database

@ArrayColumn

Marks a field as mapping to a database-native array or JSON column.

Supported Array Types:

  • POSTGRESQL_ARRAY: Native PostgreSQL arrays (text[], integer[], uuid[], etc.)
  • JSONB: PostgreSQL JSONB column containing an array
  • JSON: PostgreSQL JSON column containing an array

PostgreSQL Array Example

@Table("employees") public class Employee { @Id private Long id; private String firstName; // PostgreSQL text[] array @ArrayColumn(type = ArrayType.POSTGRESQL_ARRAY) private List<String> skills; // PostgreSQL integer[] array with custom column name @ArrayColumn(value = "badge_ids", type = ArrayType.POSTGRESQL_ARRAY) private List<Integer> badgeIds; } // Database schema: // CREATE TABLE employees ( // id SERIAL PRIMARY KEY, // first_name TEXT, // skills TEXT[], // badge_ids INTEGER[] // );

JSONB Array with Complex Objects

public class Address { private String street; private String city; private String zipCode; } @Table("customers") public class Customer { @Id private Long id; private String name; // JSONB array containing complex objects @ArrayColumn(type = ArrayType.JSONB, elementType = Address.class) private List<Address> addresses; // JSONB array with simple types @ArrayColumn(type = ArrayType.JSONB) private List<String> tags; }

When to Use

  • Use ArrayType.POSTGRESQL_ARRAY for simple types (strings, numbers, UUIDs) - more efficient
  • Use ArrayType.JSONB for complex objects or when you need to query nested properties
  • Use ArrayType.JSON only when you need to preserve exact JSON formatting

Difference from @MappedCollection

  • @ArrayColumn: For database-native arrays (single table, single row)
  • @MappedCollection: For JOIN-based relationships (separate table, multiple rows)
Last updated on