borinquenkid commented on code in PR #16701:
URL: https://github.com/apache/grails-core/pull/16701#discussion_r4365820792


##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionKeyColumnUpdater.java:
##########
@@ -36,28 +43,11 @@ public CollectionKeyColumnUpdater(CollectionKeyBinder 
collectionKeyBinder) {
         this.collectionKeyBinder = collectionKeyBinder;
     }
 
-    /** Creates the key, sets it on the collection, and updates its columns. */
+    /** Creates the key, sets it on the collection, and makes its columns 
nullable and the key updatable. */
     public void bind(HibernateToManyProperty property) {
         DependantValue key = collectionKeyBinder.bind(property);
         key.getColumns().stream().filter(Objects::nonNull).forEach(column -> 
column.setNullable(true));
-        Table collectionTable = property.getCollection().getCollectionTable();
-        long unidirectionalCount = property.getHibernateOwner()
-                .getPersistentPropertiesToBind()
-                .stream()
-                .filter(HibernateToManyProperty::isBoundAsCollection)
-                .map(HibernateToManyProperty.class::cast)
-                .filter(p -> !p.isBidirectional())
-                .filter(p -> 
Objects.equals(p.getCollection().getCollectionTable(), collectionTable))
-                .count();
-
-        // Collections of basic values or enums 
(HibernateToManyCollectionProperty) always keep an updatable key: they
-        // have no inverse side, and Hibernate writes no rows for a collection 
whose key is not updatable.
-        // For entity collections (HibernateToManyEntityProperty) the key is 
non-updatable only when the owner has
-        // two or more unidirectional to-many properties that share this 
collection's join table, the rule for issue
-        // 10811. Unidirectional collections with join tables of their own 
keep an updatable key, as they have no
-        // second key column to leave null. A to-many that is not bound as a 
collection (a serializable or user type)
-        // has no join table and no Hibernate collection, so it is neither 
counted nor asked for one.
-        key.setUpdateable(property instanceof 
HibernateToManyCollectionProperty || unidirectionalCount <= 1);
+        key.setUpdateable(true);

Review Comment:
   Fixed in 1e99b6e1ae, with a reproduction first: an account whose `roles` and 
`denyRoles` share a table and the key column `account_id` lost its denied roles 
when `roles` was cleared. Each collection of a shared table is now restricted 
to the rows that have a value in its own element columns (`where role_id is not 
null`), which limits Hibernate's delete of all the rows of a collection and its 
loads alike. `SharedJoinTableBulkDeleteSpec` clears one association and asserts 
the other remains, and fails without the restriction.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/SharedJoinTableUniqueKeyBinder.java:
##########
@@ -0,0 +1,175 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.orm.hibernate.cfg.domainbinding.secondpass;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hibernate.boot.model.naming.Identifier;
+import org.hibernate.boot.model.naming.ImplicitIndexNameSource;
+import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
+import org.hibernate.boot.model.naming.ImplicitUniqueKeyNameSource;
+import org.hibernate.boot.spi.MetadataBuildingContext;
+import org.hibernate.dialect.Dialect;
+import org.hibernate.mapping.Collection;
+import org.hibernate.mapping.Column;
+import org.hibernate.mapping.Selectable;
+import org.hibernate.mapping.Table;
+
+/**
+ * Gives every collection with a join table a uniqueness constraint of its own 
over its key column and the element,
+ * so that a join table several collections of one owner share stays unique 
per collection.
+ *
+ * <p>Two unidirectional {@code hasMany} to the same class without a {@code 
joinTable} mapping share one join table,
+ * with a key column for each of them and one element column. Hibernate 
creates the table's implicit unique key when
+ * it creates the keys of the first collection, over that collection's key 
column and the element column, and creates
+ * none for the others, as the table has a unique key already ({@code 
Set.createPrimaryKey}). This binder runs after
+ * the keys of each collection are created and completes what is missing, in 
the shape the database needs
+ * ({@link SharedJoinTableUniqueness}). A collection whose table is its own 
already has its constraint and is left as
+ * Hibernate bound it, and so is a collection without a join table or whose 
key the other side owns.</p>
+ *
+ * @since 8.0.1
+ */
+public class SharedJoinTableUniqueKeyBinder {
+
+    private final MetadataBuildingContext metadataBuildingContext;
+    private final SharedJoinTableUniqueness uniqueness;
+
+    /** Resolves the uniqueness for the dialect of the metadata being built. */
+    public SharedJoinTableUniqueKeyBinder(MetadataBuildingContext 
metadataBuildingContext) {
+        this(metadataBuildingContext, 
metadataBuildingContext.getMetadataCollector().getDatabase().getDialect());
+    }
+
+    SharedJoinTableUniqueKeyBinder(MetadataBuildingContext 
metadataBuildingContext, Dialect dialect) {
+        this.metadataBuildingContext = metadataBuildingContext;
+        this.uniqueness = SharedJoinTableUniqueness.forDialect(dialect);
+    }
+
+    /** The uniqueness this binder applies. */
+    public SharedJoinTableUniqueness getUniqueness() {
+        return uniqueness;
+    }
+
+    /**
+     * Gives a set the uniqueness constraint over its key columns and its 
element on its join table. A list or a map
+     * is keyed by its index, which its table has as part of the primary key, 
and a bag has no key at all; their
+     * elements repeat by design, so they are left as Hibernate bound them.
+     */
+    public void bind(Collection collection) {
+        Table table = collection.getCollectionTable();
+        if (!collection.isSet() || table == null || collection.isInverse() ||
+                collection.isOneToMany() || collection.getKey() == null) {
+            return;
+        }
+        List<Column> keyColumns = collection.getKey().getColumns();
+        List<Column> elementColumns = columnsOf(collection);
+        if (keyColumns.isEmpty() || elementColumns.isEmpty()) {
+            return;
+        }
+        uniqueness.apply(table, keyColumns, elementColumns, new 
ImplicitNaming(table));

Review Comment:
   Fixed in 1e99b6e1ae: the unique key is not added when an element column is a 
large object, as in `Set.createPrimaryKey`. A basic collection's element cannot 
be mapped to a LOB through the GORM mapping (`type:` applies to the property), 
so the check is tested against a real CLOB column from the metadata 
(`SharedJoinTableUniqueKeyBinderSpec`, `hasLobColumn`).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to