0%

What does it mean by insertable=false and updatable=false in JoinTable and JoinColumn of JPA

January 7, 2026

Springboot

1. On Parent Side

Since we always mark insertable=true and updatable=true in @JoinTable of parent side by default, we only discuss the child side:

2. On Child Side

2.1. With Join Table (Using @JoinTable)

2.1.1. The Children
@Entity
@GenerateDTO
@DynamicInsert
@Table(name = "scripts_folder", indexes = [Index(columnList = "id")])
class ScriptsFolder(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int? = null,

    @Column(name = "name", nullable = false)
    var name: String = "",

    @Column(name = "ordering", nullable = false)
    var ordering: Int = 0,

    @Column(name = "created_at")
    @Generated
    val createdAt: Double? = null,

    @Column(name = "created_at_hk")
    @Generated
    val createdAtHk: String? = null
) {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinTable(
        name = "rel_workspace_folder",
        joinColumns = [JoinColumn(insertable = false, updatable = false, name = "folder_id", referencedColumnName = "id")],
        inverseJoinColumns = [JoinColumn(insertable = false, updatable = false, name = "workspace_id", referencedColumnName = "id")]
    )
    var parentWorkspace: Workspace? = null
}
2.1.2. What does insertable = updatable = false mean?

Note that we have made both JoinColumn's to have attributes:

  • insertable = false
  • updatable = false

which makes the child side completely ready-only.

For @JoinTable the dirty check for the assignement

folder.parentWorkspace = otherWorkspace
  • insertable = false will not insert a relation into the join table
  • updatable = false will not update the relation in the join table

Now the relation is completely controlled by the parent, which is usually an aggregate, via as simply as

workspace.folders.add(folder)

2.2. Without Join Table (Using @JoinColumn)

Which means that a table has a column that directly points to the primary key of another column. For example:

2.2.1. The Children
class AiScriptedTool(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int? = null,

    @Column(name = "name", nullable = false)
    var name: String = "",

    @Column(name = "tool_description", nullable = false)
    var toolDescription: String = "",

    @Column(name = "is_enabled", nullable = false)
    var isEnabled: Boolean = true,

    @Column(name = "shell_script_id", nullable = false)
    var shellScriptId: Int = 0,

    @Column(name = "created_at")
    @Generated
    val createdAt: Double? = null,

    @Column(name = "created_at_hk")
    @Generated
    val createdAtHk: String? = null
) {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "shell_script_id", insertable = false, updatable = false)
    var shellScript: ShellScript? = null
}
2.2.2. What does insertable = updatable = false mean?

For @JoinColumn now the dirty check for the assignment

aiScriptedTool.shellScript = someShellScript
  • insertable = false will not include shell_script_id in the INSERT statement of persisting aiScriptedTool
  • updatable = false will not update shell_script_id in the UPDATE statement of modifying aiScriptedTool

But then how to set the relation properly? We strictly follow the following steps:

  1. Persist the parent and get parentId.
  2. Persist the children and assign that parentId.

3. When do we want insertable=true and updatable=true?

3.1. Enforce Domain Logic by Making Constructor Private

3.1.1. Scenario

It is not rare and one common scenario is:

You want to private out the constructor of an aggregate and create factory method for your entity objects to enforce domain logics.

For example, a Message entity must be one of TextMessage, ImageMessage and VoiceMessage, therefore creating and persisting the Message object alone in the database will violate the domain logic.

In other words, Message and one of the remaining classes must appear in pair.

3.1.2. Code Example (Factory Pattern)

By privating the constructor we can write

@Entity
class Message private constructor(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int? = null,

    @Column(name = "created_at")
    @Generated
    val createdAt: Double? = null,
    ...
) {
    companion object {
        fun createTextMessage(msg: String) {
            val message = Message()
            val textMessage = TextMessage(msg)
            message.textMessage = textMessage
            textMessage.parentMessage = message
        }

        fun createImageMessage(url: String) {
            val message = Message()
            val imageMessage = ImageMessage(url)
            message.imageMessage = imageMessage
            textMessage.parentMessage = message
        }

        fun createAudioMessage(audioUrl: String, transcriptionText: String) {
            val message = Message()
            val audioMessage = AudioMessage(audioUrl, transcriptionText)
            message.audioMessage = audioMessage
            textMessage.parentMessage = message
        }
    }
}

This is known as Factory Pattern and widely used in Domain Driven Design.

Now no one can create Message entity alone, prohibiting invalid domain logic from the prospective of data integrity in coding level.

3.2. Caveat for Different Choices of Databases

3.2.1. Failure in SQLite

The save behaviour for the above bidirectionally-bound entities can vary in different databases.

Takes these lines for example:

// inside of factory method:
val message = Message()
val audioMessage = AudioMessage(audioUrl, transcriptionText)
message.audioMessage = audioMessage
audioMessage.parentMessage = message

// eventually:
messageRepository.save(message)
  • The above throws an exception for SQLite because SQLite enforces foreign key constraints immediately during each INSERT, which

    • Tries to persist audioMessage first (without our control); However
    • No available id can be assigned to AudioMessage.messageId at that time.
  • Other databases (such as PostgreSQL, MySQL) can defer Foreign-Key checks until transaction commit, unforturnately SQLite does not.

  • JPA does not change its persistence strategy (order of persistence) based on different dialects.

3.2.2. For Database that Supports Deferred Constraint Checking

If our choice of database supports the above operations, just go ahead. Otherwise the persist parent first, then persist child rule is the most reliable.