A Freeze map is a persistent, associative container in which the key and value types can be any primitive or user-defined Slice types. For each pair of key and value types, the developer uses a code-generation tool to produce a language-specific class that conforms to the standard conventions for maps in that language. For example, in C++ the generated class resembles a
std::map, and in Java it implements the
java.util.SortedMap interface. Most of the logic for storing and retrieving state to and from the database is implemented in a Freeze base class. The generated map classes derive from this base class, so they contain little code and therefore are efficient in terms of code size.
You can only store data types that are defined in Slice in a Freeze map. Types without a Slice definition (that is, arbitrary C++ or Java types) cannot be stored because a Freeze map reuses the Ice-generated marshaling code to create the persistent representation of the data in the database. This is especially important to remember when defining a Slice class whose instances will be stored in a Freeze map; only the “public” (Slice-defined) data members will be stored, not the private state members of any derived implementation class.
As illustrated in Figure 39.2, a Freeze map is associated with a single connection and a single database file. Connection and map objects are “single threaded”: if you want to use a connection or any of its associated maps from multiple threads, you must serialize access to them. If your application requires concurrent access to the same database file (persistent map), you must create several connections and associated maps.

You may optionally use transactions with Freeze maps. Freeze transactions provide the usual ACID (atomicity, concurrency, isolation, durability) properties. For example, a transaction allows you to group several database updates in one atomic unit: either all or none of the updates within the transaction occur.
A transaction is started using the beginTransaction operation on the
Connection object. Once a connection has an associated transaction, all operations on the map objects associated with this connection use this transaction. Eventually, you end the transaction by calling
commit or
rollback:
commit saves all your updates while
rollback undoes them.
module Freeze {
local interface Transaction {
void commit();
void rollback();
};
local interface Connection {
Transaction beginTransaction();
idempotent Transaction currentTransaction();
// ...
};
};
If you do not use transactions, every non-iterator update is enclosed in its own internal transaction, and every read-write iterator has an associated internal transaction which is committed when the iterator is closed.
Iterators allow you to traverse the contents of a Freeze map. Iterators are implemented using Berkeley DB cursors and acquire locks on the underlying database page files. In C++, both read-only (
const_iterator) and read-write iterators (
iterator) are available; in Java only read-write iterators are supported.
Locks held by an iterator are released when the iterator is closed (if you do not use transactions) or when the enclosing transaction is ended. Releasing locks held by iterators is very important to let other threads access the database file through other connection and map objects. Occasionally it is even necessary to release locks to avoid self-deadlock (waiting forever for a lock held by an iterator created by the same thread).
To improve ease of use and make self-deadlocks less likely, Freeze often closes iterators automatically. If you close a map or connection, associated iterators are closed. Similarly, when you start or end a transaction, Freeze closes all the iterators associated with the corresponding maps. If you do not use transactions, any write operation on a map (such as inserting a new element) automatically closes all iterators opened on the same map object, except for the current iterator when the write operation is performed through that iterator.
Read operations never close iterators automatically. In that situation, you need to either use transactions or explicitly close the iterator that holds the write lock.
If you use multiple threads to access a database file, Berkeley DB may acquire locks in conflicting orders (on behalf of different transactions or iterators). For example, an iterator could have a read-lock on page P1 and attempt to acquire a write-lock on page P2, while another iterator (on a different map object associated with the same database file) could have a read-lock on P2 and attempt to acquire a write-lock on P1.
When this occurs, Berkeley DB detects a deadlock and resolves it by returning a “deadlock” error to one or more threads. For all non-iterator operations performed outside any transaction, such as an insertion into a map, Freeze catches such errors and automatically retries the operation until it succeeds. (In that case, the most-recently acquired lock is released before retrying.) For other operations, Freeze reports this deadlock by raising
Freeze::DeadlockException. In that case, the associated transaction or iterator is also automatically rolled back or closed. A properly written application is expected to catch deadlock exceptions and retry the transaction or iteration.
Freeze maps support efficient reverse lookups: if you define an index when you generate your map (with
slice2freeze or
slice2freezej), the generated code provides additional methods for performing reverse lookups. If your value type is a structure or a class, you can also index on a member of the value, and several such indices can be associated with the same Freeze map.
Indexed searches are easy to use and very efficient. However, be aware that an index adds significant write overhead: with Berkeley DB, every update triggers a read from the database to get the old index entry and, if necessary, replace it.
If you later add an index to an existing map, Freeze automatically populates the index the next time the map is reopened. Freeze populates the index by instantiating each map entry, therefore it is important that you register the object factories for any class types in your map before you open the map.
Keys in Freeze maps and indexes are always sorted. By default, Freeze sorts keys according to their Ice-encoded binary representation; it is very efficient but the resulting order is rarely meaningful for the application.
Starting with Ice 3.0, Freeze offers the ability to specify your own comparator objects. In C++, you specify these comparators as options to the
slice2freeze utility (see below). In Java, the generated Java map class provides a number of constructors to accept these comparator objects.
The generated map provides the standard features from std::map (in C++) and
java.util.SortedMap (in Java). Iterators return entries according to the order you have defined for the main key with your comparator object. In C++,
lower_bound,
upper_bound and
equal_range provide range-searches; see the definition of these functions on
std::map. In Java, use
headMap,
tailMap and
subMap for range-searches; these methods come from the
java.util.SortedMap interface.
In addition to these standard features, the generated map provides additional functions and methods to perform range-searches using secondary keys. In C++, the additional functions are
lowerBoundForMember,
upperBoundForMember and
equalRangeForMember, where
Member is the name of the secondary-key member. These functions return regular iterators on the Freeze map.
public java.util.SortedMap
headMapForIndex(String indexName, Object toKey)
public java.util.SortedMap
tailMapForIndex(String indexName, Object fromKey)
public java.util.SortedMap
subMapForIndex(String indexName,
Object fromKey,
Object toKey)
The key of the returned submap is the secondary key (the index) and its value is a java.util.Set of
Map.Entry objects from the main Freeze map. This set provides all the entries in the main Freeze map with the given secondary key. When iterating over this submap, you may need to close iterators explicitly, like with iterators obtained for the main Freeze map. See
Section 39.3.3 for more information.
39.3.7 slice2freeze Command-Line Options
The Slice-to-Freeze compiler, slice2freeze, offers the following command-line options in addition to the standard options described in
Section 4.19:
•
‑‑add‑header HDR[,GUARD]
#ifndef __PRECOMPILED_H__
#define __PRECOMPILED_H__
#include <precompiled.h>
#endif
Modifies #include directives in source files to prepend the pathname of each header file with the directory
DIR. See
Section 6.15.1 for more information.
Use SYMBOL to control DLL exports or imports. See the
slice2cpp description for details.
•
‑‑dict NAME,
KEY,
VALUE[,sort[,COMPARE]]
Generate a Freeze map (C++ map) named NAME using
KEY as key and
VALUE as value. This option may be specified multiple times to generate several Freeze maps.
NAME may be a scoped C++ name, such as
Demo::Struct1ObjectMap. By default, keys are sorted using their binary Ice-encoded representation. Include
sort to sort with the
COMPARE functor class. If
COMPARE is not specified, the default value is
std::less<KEY>.
•
‑‑dict‑index MAP[,MEMBER]
[,case‑sensitive|case‑insensitive][,sort[,COMPARE]]
Add an index to the Freeze map named MAP. If
MEMBER is specified, then the map value type must be a structure or a class, and
MEMBER must be a member of this structure or class. Otherwise, the entire value is indexed. When the indexed member (or entire value) is a string, the index can be case-sensitive (default) or case-insensitive. An index adds nine member functions to the generated C++ map:
int MEMBERCount(MEMBER_TYPE) const;
When MEMBER is not specified, these functions are
findByValue (const and non-const),
lowerBoundForValue (const and non-const),
valueCount, etc. When
MEMBER is specified, its first letter is capitalized in the
findBy function name.
MEMBER_TYPE corresponds to an in-parameter of the type of
MEMBER (or the type of the value when
MEMBER is not specified). For example, if
MEMBER is a string,
MEMBER_TYPE is a
const std::string&.
•
‑‑index CLASS,TYPE,MEMBER
[,case‑sensitive|case‑insensitive]
Generate a Freeze Evictor Index (in C++). CLASS is the name of the class to be generated.
TYPE denotes the type of class to be indexed (objects of different classes are not included in this index).
MEMBER is the name of the data member in
TYPE to index. When
MEMBER has type
string, it is possible to specify whether the index is case-sensitive or not. The default is case-sensitive. See
Section 39.5.7 for more information.
Section 6.15.1 provides a discussion of the semantics of
#include directives that is also relevant for users of
slice2freeze.
39.3.8 slice2freezej Command-Line Options
The Slice-to-Freeze-for-Java compiler, slice2freezej, offers the following command-line options in addition to the standard options described in
Section 4.19:
Generate a Freeze map (Java map) named NAME using
KEY as key and
VALUE as value. This option may be specified multiple times to generate several Freeze dictionaries.
NAME may be a scoped Java name, such as
Demo.Struct1ObjectMap.
•
‑‑dict‑index MAP[,MEMBER]
[,case‑sensitive|case‑insensitive]
Add an index to the Freeze map named MAP. If
MEMBER is specified, then the map value type must be a structure or a class, and
MEMBER must be a member of this structure or class. Otherwise, the entire value is indexed. When the indexed member (or entire value) is a string, the index can be case-sensitive (default) or case-insensitive. An index adds two methods to the generated Java map:
When MEMBER is not specified, these functions are
findbyValue and
valueCount. When
MEMBER is specified, its first letter is capitalized in the
findBy function name.
MEMBER_TYPE corresponds to an in-parameter of the type of
MEMBER (or the type of the value when
MEMBER is not specified). For example, if
MEMBER is a string,
MEMBER_TYPE is a
java.lang.String.
•
‑‑index CLASS,
TYPE,
MEMBER
[,case‑sensitive|case‑insensitive]
Generate a Freeze Evictor Index (in Java). CLASS is the name of the class to be generated.
TYPE denotes the type of class to be indexed (objects of different classes are not included in this index).
MEMBER is the name of the data member in
TYPE to index. When
MEMBER has type
string, it is possible to specify whether the index is case-sensitive or not. The default is case-sensitive. See
Section 39.5.7 for more information.
$ slice2freeze ‑‑dict StringIntMap,string,int StringIntMap
The slice2freeze compiler creates C++ classes for Freeze maps. The command above directs the compiler to create a map named
StringIntMap, with the Slice key type
string and the Slice value type
int. The final argument is the base name for the output files, to which the compiler appends the
.h and
.cpp suffixes. Therefore, this command produces two C++ source files:
StringIntMap.h and
StringIntMap.cpp.
Here is a simple program that demonstrates how to use a StringIntMap to store <
string,
int> pairs in a database. You will notice that there are no explicit
read or
write operations called by the program; instead, simply using the map has the side effect of accessing the database.
#include <Freeze/Freeze.h>
#include <StringIntMap.h>
int
main(int argc, char* argv[])
{
// Initialize the Communicator.
//
Ice::CommunicatorPtr communicator =
Ice::initialize(argc, argv);
// Create a Freeze database connection.
//
Freeze::ConnectionPtr connection =
Freeze::createConnection(communicator, "db");
// Instantiate the map.
//
StringIntMap map(connection, "simple");
// Clear the map.
//
map.clear();
Ice::Int i;
StringIntMap::iterator p;
// Populate the map.
//
for (i = 0; i < 26; i++) {
std::string key(1, 'a' + i);
map.insert(make_pair(key, i));
}
// Iterate over the map and change the values.
//
for (p = map.begin(); p != map.end(); ++p)
p.set(p‑>second + 1);
// Find and erase the last element.
//
p = map.find("z");
assert(p != map.end());
map.erase(p);
// Clean up.
//
connection‑>close();
communicator‑>destroy();
return 0;
}
The second argument is the name of a Berkeley DB database environment; by default, this is also the file system directory in which Berkeley DB creates all database and administrative files.
Next, the code instantiates the StringIntMap on the connection. The constructor’s second argument supplies the name of the database file, which by default is created if it does not exist. Note that a database can only contain the persistent state of one map type. Any attempt to instantiate maps of different types on the same database results in undefined behavior.
We populate the map, using a single-character string as the key. The Freeze map supports several
insert methods for adding entries, similar to a std::map. Insertion via
operator[] is not supported.
Iterating over the map will look familiar to std::map users. However, to modify a value at the iterator’s current position, you must use the nonstandard
set method:
$ slice2freezej ‑‑dict StringIntMap,string,int
The slice2freezej compiler creates Java classes for Freeze maps. The command above directs the compiler to create a map named
StringIntMap, with the Slice key type
string and the Slice value type
int. This command produces one Java source file:
StringIntMap.java.
Here is a simple program that demonstrates how to use a StringIntMap to store <
string,
int> pairs in a database. You will notice that there are no explicit
read or
write operations called by the program; instead, simply using the map has the side effect of accessing the database.
public class Client
{
public static void
main(String[] args)
{
// Initialize the Communicator.
//
Ice.Communicator communicator = Ice.Util.initialize(args);
// Create a Freeze database connection.
//
Freeze.Connection connection =
Freeze.Util.createConnection(communicator, "db");
// Instantiate the map.
//
StringIntMap map =
new StringIntMap(connection, "simple", true);
// Clear the map.
//
map.clear();
int i;
java.util.Iterator p;
// Populate the map.
//
for (i = 0; i < 26; i++) {
final char[] ch = { (char)('a' + i) };
map.put(new String(ch), new Integer(i));
}
// Iterate over the map and change the values.
//
p = map.entrySet().iterator();
while (p.hasNext()) {
java.util.Map.Entry e = (java.util.Map.Entry)p.next();
Integer in = (Integer)e.getValue();
e.setValue(new Integer(in.intValue() + 1));
}
// Find and erase the last element.
//
boolean b;
b = map.containsKey("z");
assert(b);
b = map.fastRemove("z");
assert(b);
// Clean up.
//
map.close();
connection.close();
communicator.destroy();
System.exit(0);
}
}
The second argument is the name of a Berkeley DB database environment; by default, this is also the file system directory in which Berkeley DB creates all database and administrative files.
Next, the code instantiates the StringIntMap on the connection. The constructor’s second argument supplies the name of the database file, and the third argument is a flag indicating whether the database should be created if it does not exist. Note that a database can only contain the persistent state of one map type. Any attempt to instantiate maps of different types on the same database results in undefined behavior.
p = map.entrySet().iterator();while (p.hasNext()) {
java.util.Map.Entry e =
(java.util.Map.Entry)p.next();
Integer in = (Integer)e.getValue();
e.setValue(new Integer(in.intValue() + 1));
}
Next, the program verifies that an element exists with key z, and then removes it. Note that the program uses a non-standard method for removing the element. The
fastRemove method differs from the standard
remove method in that it does not return the value associated with the removed element; instead it returns a boolean indicating whether the element was found. By eliminating the need to return the element value, the method avoids the overhead of reading the value from the database and decoding it. The standard
remove method could also be used here; we chose to use
fastRemove instead simply to demonstrate its use: