User-defined types

Cassandra 2.1 introduced support for User-defined types (UDT). A user-defined type simplifies handling a group of related properties.

A quick example is a user account table that contains address details described through a set of columns: street, city, zip code. With the addition of UDTs, you can define this group of properties as a type and access them as a single entity or separately.

User-defined types are declared at the keyspace level.

In your application, you can map your UDTs to application entities. For example, given the following UDT:

CREATE TYPE address (
   street text,
   city text,
   zip int,
   phones list<text>
);

You create a C# class that maps to the UDT:

public class Address
{
   public string Street { get; set; }
   public string City { get; set; }
   public int Zip { get; set; }
   public IEnumerable<string> Phones { get; set;}
}

You declare the mapping once at the session level:

await session.UserDefinedTypes.DefineAsync(
      UdtMap.For<Phone>()
            .Map(v => v.Alias, "alias")
            .Map(v => v.CountryCode, "country_code")
            .Map(v => v.Number, "number")).ConfigureAwait(false);

You can also provide the keyspace when declaring a UDT. This is useful for these situations:

  • If the UDT is defined on a keyspace which is not the default - which can be set via Session.Connect(string) or Builder.WithDefaultKeyspace(string)
  • if you don’t declare a default keyspace with the methods mentioned above

To provide the keyspace when declaring a UDT:

await session.UserDefinedTypes.DefineAsync(
      UdtMap.For<Phone>(keyspace: "keyspace")
            .Map(v => v.Alias, "alias")
            .Map(v => v.CountryCode, "country_code")
            .Map(v => v.Number, "number")).ConfigureAwait(false);

Once declared the mapping will be available for the lifetime of the application:

var results = session.Execute("SELECT id, name, address FROM users where id = 756716f7-2e54-4715-9f00-91dcbea6cf50");
var row = results.First();
// You retrieve the field as a value of type Address
var userAddress = row.GetValue<Address>("address");
Console.WriteLine("The user lives on {0} Street", userAddress.Street);

CQL column and C# class properties mismatch

For the automatic mapping to work, the table column names and the class properties must match (note that column-to-field matching is case-insensitive). For example, in the UDT and the C# class examples above were changed:

CREATE TYPE address (
   street text,
   city text,
   zip_code int,
   phones list<text>
);
public class Address
{
   public string Street { get; set; }
   public string City { get; set; }
   public int ZipCode { get; set; }
   public IEnumerable<string> Phones { get; set;}
}

You can also define the properties manually:

session.UserDefinedTypes.Define(
   UdtMap.For<Address>()
      .Map(a => a.Street, "street")
      .Map(a => a.City, "city")
      .Map(a => a.Zip, "zip")
      .Map(a => a.Phones, "phones")
);

You can still use automatic mapping, but you must add a call to the Map method. For example:

session.UserDefinedTypes.Define(
   UdtMap.For<Address>()
      .Automap()
      .Map(a => a.Zipcode, "zip_code")
);

Nesting User-defined types In CQL

UDTs can be nested relatively arbitrarily. For the C# driver you have to define the mapping to all the user-defined types used.

Based on the previous example, let’s change the phones column from set<text> to a set<phone>, where phone contains an alias, a number and a country code.

Phone UDT

CREATE TYPE phone (
   alias text,
   number text,
   country_code int
);

Address UDT

CREATE TYPE address (
   street text,
   city text,
   zip_code int,
   phones list<phone>
);

Now we can update the Address class to use the Phone class:

public class Address
{
   public string Street { get; set; }
   public string City { get; set; }
   public int ZipCode { get; set; }
   public IEnumerable<Phone> Phones { get; set; }
}

You have to define the mapping for both classes

session.UserDefinedTypes.Define(
   UdtMap.For<Phone>(),
   UdtMap.For<Address>()
      .Automap()
      .Map(a => a.ZipCode, "zip_code")
);

After that, you can reuse the mapping within your application.

var userAddress = row.GetValue<Address>("address");
var mainPhone = userAddress.Phones.First();
Console.WriteLine("User main phone is {0}", mainPhone.Alias);

Frozen UDTs and Collections

If you provide the whole object when inserting and updating entities, it is recommended to use frozen UDTs over non-frozen UDTs. If you are using Mapper or Linq2Cql, this is the case when you use Mapper.Insert<T>(T obj), Mapper.Update<T>(T obj) orTable<T>.Insert(T obj). The same applies to frozen collections over non-frozen collections.

Frozen collections and UDTs in Cassandra are serialized as a single cell value where non-frozen collections and UDTs serialize each individual element/field as a cell.

Methods like Mapper.Update<T>(T obj) provide the entire UDT or collection for an entity field value on each invocation, so it is more efficient to use frozen UDTs and collections when every update and insert is done this way.

Also, when using non-frozen collections, on INSERT Cassandra must create a tombstone to invalidate all existing collection elements, even if there are none. When using frozen collections, no such tombstone is needed.

See Freezing collection types for more information about frozen collections.

See Creating a user-defined type for more information about frozen UDTs.